snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
209
apps/meteor/client/views/modal/uikit/ModalBlock.tsx
Normal file
209
apps/meteor/client/views/modal/uikit/ModalBlock.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import {
|
||||
Modal,
|
||||
AnimatedVisibility,
|
||||
Button,
|
||||
Box,
|
||||
ModalHeader,
|
||||
ModalThumb,
|
||||
ModalTitle,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalFooterControllers,
|
||||
} from '@rocket.chat/fuselage';
|
||||
import { UiKitComponent, UiKitModal, modalParser } from '@rocket.chat/fuselage-ui-kit';
|
||||
import type * as UiKit from '@rocket.chat/ui-kit';
|
||||
import type { FormEvent, FormEventHandler } from 'react';
|
||||
import { useId, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { getButtonStyle } from './getButtonStyle';
|
||||
import { getURL } from '../../../lib/getURL';
|
||||
|
||||
const focusableElementsString = `
|
||||
a[href]:not([tabindex="-1"]),
|
||||
area[href]:not([tabindex="-1"]),
|
||||
input:not([disabled]):not([tabindex="-1"]),
|
||||
select:not([disabled]):not([tabindex="-1"]),
|
||||
textarea:not([disabled]):not([tabindex="-1"]),
|
||||
button:not([disabled]):not([tabindex="-1"]),
|
||||
iframe,
|
||||
object,
|
||||
embed,
|
||||
[tabindex]:not([tabindex="-1"]),
|
||||
[contenteditable]`;
|
||||
|
||||
const focusableElementsStringInvalid = `
|
||||
a[href]:not([tabindex="-1"]):invalid,
|
||||
area[href]:not([tabindex="-1"]):invalid,
|
||||
input:not([disabled]):not([tabindex="-1"]):invalid,
|
||||
select:not([disabled]):not([tabindex="-1"]):invalid,
|
||||
textarea:not([disabled]):not([tabindex="-1"]):invalid,
|
||||
button:not([disabled]):not([tabindex="-1"]):invalid,
|
||||
iframe:invalid,
|
||||
object:invalid,
|
||||
embed:invalid,
|
||||
[tabindex]:not([tabindex="-1"]):invalid,
|
||||
[contenteditable]:invalid`;
|
||||
|
||||
type ModalBlockParams = {
|
||||
view: UiKit.ModalView;
|
||||
errors: any;
|
||||
appId: string;
|
||||
onSubmit: (event: FormEvent) => void;
|
||||
onClose: () => void;
|
||||
onCancel: FormEventHandler;
|
||||
};
|
||||
|
||||
const isFocusable = (element: Element | null): element is HTMLElement =>
|
||||
element !== null && 'focus' in element && typeof element.focus === 'function';
|
||||
|
||||
const KeyboardCode = new Map<string, number>([
|
||||
['ENTER', 13],
|
||||
['ESC', 27],
|
||||
['TAB', 9],
|
||||
]);
|
||||
|
||||
const ModalBlock = ({ view, errors, onSubmit, onClose, onCancel }: ModalBlockParams) => {
|
||||
const id = `modal_id_${useId()}`;
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (errors && Object.keys(errors).length) {
|
||||
const element = ref.current.querySelector<HTMLElement>(focusableElementsStringInvalid);
|
||||
element?.focus();
|
||||
} else {
|
||||
const element = ref.current.querySelector<HTMLElement>(focusableElementsString);
|
||||
element?.focus();
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
const previousFocus = useMemo(() => document.activeElement, []);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (previousFocus && isFocusable(previousFocus)) {
|
||||
return previousFocus.focus();
|
||||
}
|
||||
},
|
||||
[previousFocus],
|
||||
);
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
switch (event.keyCode) {
|
||||
case KeyboardCode.get('ENTER'):
|
||||
if ((event?.target as Node | null)?.nodeName !== 'TEXTAREA') {
|
||||
formRef.current?.submit();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
case KeyboardCode.get('ESC'):
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onClose();
|
||||
return;
|
||||
case KeyboardCode.get('TAB'):
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const elements = Array.from(ref.current.querySelectorAll<HTMLElement>(focusableElementsString));
|
||||
const [first] = elements;
|
||||
const last = elements.pop();
|
||||
|
||||
if (!ref.current.contains(document.activeElement)) {
|
||||
return first.focus();
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (!first || first === document.activeElement) {
|
||||
last?.focus();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!last || last === document.activeElement) {
|
||||
first.focus();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.querySelector('#modal-root') as HTMLElement;
|
||||
const container = element.querySelector('.rcx-modal__content') as HTMLElement;
|
||||
const close = (e: Event) => {
|
||||
if (e.target !== element) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
return false;
|
||||
};
|
||||
|
||||
const ignoreIfNotContains = (e: KeyboardEvent) => {
|
||||
if (e.target !== element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!container.contains(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
return handleKeyDown(e);
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', ignoreIfNotContains);
|
||||
element.addEventListener('click', close);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', ignoreIfNotContains);
|
||||
element.removeEventListener('click', close);
|
||||
};
|
||||
}, [handleKeyDown, onClose]);
|
||||
|
||||
return (
|
||||
<AnimatedVisibility visibility={AnimatedVisibility.UNHIDING}>
|
||||
<FocusScope contain restoreFocus autoFocus>
|
||||
<Modal aria-labelledby={`${id}-title`} open id={id} ref={ref}>
|
||||
<ModalHeader>
|
||||
{view.showIcon ? <ModalThumb url={getURL(`/api/apps/${view.appId}/icon`)} /> : null}
|
||||
<ModalTitle id={`${id}-title`}>{modalParser.renderTextObject(view.title, 0)}</ModalTitle>
|
||||
<ModalClose tabIndex={-1} onClick={onClose} />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Box ref={formRef} is='form' method='post' action='#' onSubmit={onSubmit}>
|
||||
<UiKitComponent render={UiKitModal} blocks={view.blocks} />
|
||||
</Box>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<ModalFooterControllers>
|
||||
{view.close && (
|
||||
<Button danger={view.close.style === 'danger'} onClick={onCancel}>
|
||||
{modalParser.renderTextObject(view.close.text, 0)}
|
||||
</Button>
|
||||
)}
|
||||
{view.submit && (
|
||||
<Button {...getButtonStyle(view.submit)} onClick={onSubmit}>
|
||||
{modalParser.renderTextObject(view.submit.text, 1)}
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooterControllers>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</FocusScope>
|
||||
</AnimatedVisibility>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalBlock;
|
||||
85
apps/meteor/client/views/modal/uikit/UiKitModal.tsx
Normal file
85
apps/meteor/client/views/modal/uikit/UiKitModal.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
|
||||
import { UiKitContext } from '@rocket.chat/fuselage-ui-kit';
|
||||
import { MarkupInteractionContext } from '@rocket.chat/gazzodown';
|
||||
import { useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import type * as UiKit from '@rocket.chat/ui-kit';
|
||||
import type { FormEvent } from 'react';
|
||||
|
||||
import ModalBlock from './ModalBlock';
|
||||
import { detectEmoji } from '../../../lib/utils/detectEmoji';
|
||||
import { preventSyntheticEvent } from '../../../lib/utils/preventSyntheticEvent';
|
||||
import { useModalContextValue } from '../../../uikit/hooks/useModalContextValue';
|
||||
import { useUiKitActionManager } from '../../../uikit/hooks/useUiKitActionManager';
|
||||
import { useUiKitView } from '../../../uikit/hooks/useUiKitView';
|
||||
|
||||
export type UiKitModalProps = {
|
||||
key: UiKit.ModalView['id']; // force re-mount when viewId changes
|
||||
initialView: UiKit.ModalView;
|
||||
};
|
||||
|
||||
const UiKitModal = ({ initialView }: UiKitModalProps) => {
|
||||
const actionManager = useUiKitActionManager();
|
||||
const { view, errors, values, updateValues, state } = useUiKitView(initialView);
|
||||
const contextValue = useModalContextValue({ view, errors, values, updateValues });
|
||||
const useEmoji = useUserPreference<boolean>('useEmojis', true);
|
||||
const convertAsciiToEmoji = useUserPreference<boolean>('convertAsciiEmoji', true);
|
||||
|
||||
const handleSubmit = useStableCallback((e: FormEvent) => {
|
||||
preventSyntheticEvent(e);
|
||||
void actionManager.emitInteraction(view.appId, {
|
||||
type: 'viewSubmit',
|
||||
payload: {
|
||||
view: {
|
||||
...view,
|
||||
state,
|
||||
},
|
||||
},
|
||||
viewId: view.id,
|
||||
});
|
||||
});
|
||||
|
||||
const handleCancel = useStableCallback((e: FormEvent) => {
|
||||
preventSyntheticEvent(e);
|
||||
void actionManager.emitInteraction(view.appId, {
|
||||
type: 'viewClosed',
|
||||
payload: {
|
||||
viewId: view.id,
|
||||
view: {
|
||||
...view,
|
||||
state,
|
||||
},
|
||||
isCleared: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleClose = useStableCallback(() => {
|
||||
void actionManager.emitInteraction(view.appId, {
|
||||
type: 'viewClosed',
|
||||
payload: {
|
||||
viewId: view.id,
|
||||
view: {
|
||||
...view,
|
||||
state,
|
||||
},
|
||||
isCleared: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<UiKitContext.Provider value={contextValue}>
|
||||
<MarkupInteractionContext.Provider
|
||||
value={{
|
||||
detectEmoji,
|
||||
useEmoji,
|
||||
convertAsciiToEmoji,
|
||||
}}
|
||||
>
|
||||
<ModalBlock view={view} errors={errors} appId={view.appId} onSubmit={handleSubmit} onCancel={handleCancel} onClose={handleClose} />
|
||||
</MarkupInteractionContext.Provider>
|
||||
</UiKitContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default UiKitModal;
|
||||
6
apps/meteor/client/views/modal/uikit/getButtonStyle.ts
Normal file
6
apps/meteor/client/views/modal/uikit/getButtonStyle.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { ButtonElement } from '@rocket.chat/ui-kit';
|
||||
|
||||
// TODO: Move to fuselage-ui-kit
|
||||
export const getButtonStyle = (buttonElement: ButtonElement): { danger: boolean } | { primary: boolean } => {
|
||||
return buttonElement?.style === 'danger' ? { danger: true } : { primary: true };
|
||||
};
|
||||
Reference in New Issue
Block a user