Skip to content

Commit

Permalink
feat(modal)!: use floating ui in modal (#1995)
Browse files Browse the repository at this point in the history
Co-authored-by: oynikishin <[email protected]>
  • Loading branch information
2 people authored and amje committed Dec 19, 2024
1 parent b673030 commit 58dcfaa
Show file tree
Hide file tree
Showing 25 changed files with 638 additions and 822 deletions.
22 changes: 7 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,11 @@
},
"dependencies": {
"@bem-react/classname": "^1.6.0",
"@floating-ui/react": "^0.26.28",
"@floating-ui/react": "^0.27.0",
"@gravity-ui/i18n": "^1.7.0",
"@gravity-ui/icons": "^2.11.0",
"@tanstack/react-virtual": "^3.10.8",
"blueimp-md5": "^2.19.0",
"focus-trap": "^7.6.2",
"lodash": "^4.17.21",
"rc-slider": "^11.1.7",
"react-beautiful-dnd": "^13.1.1",
Expand Down
220 changes: 118 additions & 102 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,136 +12,152 @@ import {DialogBody} from './DialogBody/DialogBody';
import {DialogDivider} from './DialogDivider/DialogDivider';
import {DialogFooter} from './DialogFooter/DialogFooter';
import {DialogHeader} from './DialogHeader/DialogHeader';
import {DialogPrivateContext} from './DialogPrivateContext';
import type {DialogPrivateContextProps} from './DialogPrivateContext';

import './Dialog.scss';

const b = block('dialog');

interface DialogOwnProps extends QAProps {
export interface DialogProps extends QAProps {
open: boolean;
children: React.ReactNode;
onOpenChange?: ModalProps['onOpenChange'];
onEnterKeyDown?: (event: KeyboardEvent) => void;
onEscapeKeyDown?: ModalProps['onEscapeKeyDown'];
onEnterKeyDown?: ModalProps['onEnterKeyDown'];
onOutsideClick?: ModalProps['onOutsideClick'];
onClose: (
event: MouseEvent | KeyboardEvent,
reason: ModalCloseReason | 'closeButtonClick',
) => void;
onTransitionEnter?: ModalProps['onTransitionEnter'];
onTransitionEntered?: ModalProps['onTransitionEntered'];
onTransitionExit?: ModalProps['onTransitionExit'];
onTransitionExited?: ModalProps['onTransitionExited'];
onTransitionIn?: ModalProps['onTransitionIn'];
onTransitionInComplete?: ModalProps['onTransitionInComplete'];
onTransitionOut?: ModalProps['onTransitionOut'];
onTransitionOutComplete?: ModalProps['onTransitionOutComplete'];
className?: string;
modalClassName?: string;
size?: 's' | 'm' | 'l';
'aria-label'?: string;
'aria-labelledby'?: string;
container?: HTMLElement;
disableFocusTrap?: boolean;
disableAutoFocus?: boolean;
restoreFocusRef?: React.RefObject<HTMLElement>;
// TODO: Remove from readme disableFocusTrap disableAutoFocus
initialFocus?: ModalProps['initialFocus'] | 'cancel' | 'apply';
returnFocus?: ModalProps['returnFocus'];
contentOverflow?: 'visible' | 'auto';
disableBodyScrollLock?: boolean;
disableEscapeKeyDown?: boolean;
disableOutsideClick?: boolean;
keepMounted?: boolean;
hasCloseButton?: boolean;
}

interface DialogDefaultProps {
disableBodyScrollLock: boolean;
disableEscapeKeyDown: boolean;
disableOutsideClick: boolean;
keepMounted: boolean;
hasCloseButton: boolean;
}
export function Dialog({
container,
children,
open,
disableBodyScrollLock = false,
disableEscapeKeyDown = false,
disableOutsideClick = false,
initialFocus,
returnFocus,
keepMounted = false,
size,
contentOverflow = 'visible',
className,
modalClassName,
hasCloseButton = true,
onEscapeKeyDown,
onEnterKeyDown,
onOpenChange,
onOutsideClick,
onClose,
onTransitionIn,
onTransitionInComplete,
onTransitionOut,
onTransitionOutComplete,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
qa,
}: DialogProps) {
const handleCloseButtonClick = React.useCallback(
(event: React.MouseEvent) => {
onClose(event.nativeEvent, 'closeButtonClick');
},
[onClose],
);

const footerAutoFocusRef = React.useRef<HTMLElement | null>(null);

export type DialogProps = DialogOwnProps & Partial<DialogDefaultProps>;
type DialogInnerProps = DialogOwnProps & DialogDefaultProps;
const privateContextProps = React.useMemo(() => {
const result: DialogPrivateContextProps = {
onTooltipEscapeKeyDown: (event: KeyboardEvent) => {
onOpenChange?.(false, event, 'escape-key');
onEscapeKeyDown?.(event);
onClose?.(event, 'escapeKeyDown');
},
};

export class Dialog extends React.Component<DialogInnerProps> {
static defaultProps: DialogDefaultProps = {
disableBodyScrollLock: false,
disableEscapeKeyDown: false,
disableOutsideClick: false,
keepMounted: false,
hasCloseButton: true,
};
if (typeof initialFocus === 'string') {
result.initialFocusRef = footerAutoFocusRef;
result.initialFocusAction = initialFocus;
}

static Footer = DialogFooter;
static Header = DialogHeader;
static Body = DialogBody;
static Divider = DialogDivider;
return result;
}, [initialFocus, onEscapeKeyDown, onClose, onOpenChange]);

render() {
const {
container,
children,
open,
disableBodyScrollLock,
disableEscapeKeyDown,
disableOutsideClick,
disableFocusTrap,
disableAutoFocus,
restoreFocusRef,
keepMounted,
size,
contentOverflow = 'visible',
className,
modalClassName,
hasCloseButton,
onEscapeKeyDown,
onEnterKeyDown,
onOutsideClick,
onClose,
onTransitionEnter,
onTransitionEntered,
onTransitionExit,
onTransitionExited,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
qa,
} = this.props;
let initialFocusValue: ModalProps['initialFocus'];
if (typeof initialFocus === 'string') {
initialFocusValue = footerAutoFocusRef;
} else {
initialFocusValue = initialFocus;
}

return (
<Modal
open={open}
contentOverflow={contentOverflow}
disableBodyScrollLock={disableBodyScrollLock}
disableEscapeKeyDown={disableEscapeKeyDown}
disableOutsideClick={disableOutsideClick}
disableFocusTrap={disableFocusTrap}
disableAutoFocus={disableAutoFocus}
restoreFocusRef={restoreFocusRef}
keepMounted={keepMounted}
onEscapeKeyDown={onEscapeKeyDown}
onEnterKeyDown={onEnterKeyDown}
onOutsideClick={onOutsideClick}
onClose={onClose}
onTransitionEnter={onTransitionEnter}
onTransitionEntered={onTransitionEntered}
onTransitionExit={onTransitionExit}
onTransitionExited={onTransitionExited}
className={b('modal', modalClassName)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
container={container}
qa={qa}
return (
<Modal
open={open}
contentOverflow={contentOverflow}
disableBodyScrollLock={disableBodyScrollLock}
disableEscapeKeyDown={disableEscapeKeyDown}
disableOutsideClick={disableOutsideClick}
disableFocusVisuallyHiddenDismiss={hasCloseButton}
initialFocus={initialFocusValue}
returnFocus={returnFocus}
keepMounted={keepMounted}
onEscapeKeyDown={onEscapeKeyDown}
onOutsideClick={onOutsideClick}
onClose={onClose}
onEnterKeyDown={onEnterKeyDown}
onTransitionIn={onTransitionIn}
onTransitionInComplete={onTransitionInComplete}
onTransitionOut={onTransitionOut}
onTransitionOutComplete={onTransitionOutComplete}
className={b('modal', modalClassName)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
container={container}
qa={qa}
>
<div
className={b(
{
size,
'has-close': hasCloseButton,
'has-scroll': contentOverflow === 'auto',
},
className,
)}
>
<div
className={b(
{
size,
'has-close': hasCloseButton,
'has-scroll': contentOverflow === 'auto',
},
className,
)}
>
<DialogPrivateContext.Provider value={privateContextProps}>
{children}
{hasCloseButton && <ButtonClose onClose={this.handleCloseButtonClick} />}
</div>
</Modal>
);
}
</DialogPrivateContext.Provider>

private handleCloseButtonClick = (event: React.MouseEvent) => {
const {onClose} = this.props;
onClose(event.nativeEvent, 'closeButtonClick');
};
{hasCloseButton && <ButtonClose onClose={handleCloseButtonClick} />}
</div>
</Modal>
);
}

Dialog.Footer = DialogFooter;
Dialog.Header = DialogHeader;
Dialog.Body = DialogBody;
Dialog.Divider = DialogDivider;
Loading

0 comments on commit 58dcfaa

Please sign in to comment.