Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[base] Add useModal hook #38187

Merged
merged 28 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2999d30
[joy] Add `useModal` internal hook
mnajdova Jul 27, 2023
d459eef
Update packages/mui-joy/src/Modal/useModal.ts
mnajdova Jul 27, 2023
8483b6f
Import ariaHidden from Base UI
mnajdova Jul 31, 2023
a77fc9b
Merge branch 'joy/useModal-hook' of https://github.com/mnajdova/mater…
mnajdova Jul 31, 2023
fa82187
wip useModal in base UI
mnajdova Aug 1, 2023
8dc8ea9
Merge branch 'master' into joy/useModal-hook
mnajdova Aug 1, 2023
0307cb0
docs:api
mnajdova Aug 1, 2023
cb78f37
Fix propagation of custom event handlers
mnajdova Aug 2, 2023
b28568f
Define the hook's types
mnajdova Aug 2, 2023
83277a7
Fix import name for unstable hooks
mnajdova Aug 2, 2023
3a1578f
Fix imports and circular dependency
mnajdova Aug 2, 2023
7979081
Add hook demo
mnajdova Aug 2, 2023
283633c
Fix isTopModal type & usage
mnajdova Aug 2, 2023
06d0475
demos fixes
mnajdova Aug 2, 2023
40d5174
Add use client directive
mnajdova Aug 2, 2023
7bd4142
prettier
mnajdova Aug 2, 2023
122cc4e
lint & docs:api
mnajdova Aug 2, 2023
50a2cfb
ci fixes
mnajdova Aug 2, 2023
2331dc3
Fix useAutocomplete issue
mnajdova Aug 2, 2023
0f6740b
Merge branch 'master' into joy/useModal-hook
mnajdova Aug 3, 2023
b8103b7
docs:api
mnajdova Aug 3, 2023
577ad5e
Merge branch 'master' into joy/useModal-hook
mnajdova Aug 4, 2023
e54dfda
Review comments
mnajdova Aug 4, 2023
ebe554d
ref -> rootRef
mnajdova Aug 4, 2023
707fe36
fixes
mnajdova Aug 4, 2023
f1c79bf
more fixes
mnajdova Aug 4, 2023
1e7ced7
Update packages/mui-base/src/unstable_useModal/useModal.ts
mnajdova Aug 9, 2023
5ce713a
simplify condition
mnajdova Aug 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 11 additions & 142 deletions packages/mui-joy/src/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,17 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { OverridableComponent } from '@mui/types';
import {
elementAcceptingRef,
HTMLElementType,
unstable_ownerDocument as ownerDocument,
unstable_useForkRef as useForkRef,
unstable_useEventCallback as useEventCallback,
} from '@mui/utils';
import { elementAcceptingRef, HTMLElementType } from '@mui/utils';
import composeClasses from '@mui/base/composeClasses';
import Portal from '@mui/base/Portal';
import FocusTrap from '@mui/base/FocusTrap';
import { ModalManager } from '@mui/base/Modal';
import useModal from './useModal';
import { styled, useThemeProps } from '../styles';
import useSlot from '../utils/useSlot';
import { getModalUtilityClass } from './modalClasses';
import { ModalOwnerState, ModalTypeMap } from './ModalProps';
import CloseModalContext from './CloseModalContext';

function ariaHidden(element: Element, show: boolean): void {
if (show) {
element.setAttribute('aria-hidden', 'true');
} else {
element.removeAttribute('aria-hidden');
}
}

const useUtilityClasses = (ownerState: ModalOwnerState) => {
const { open } = ownerState;

Expand All @@ -38,14 +24,6 @@ const useUtilityClasses = (ownerState: ModalOwnerState) => {
return composeClasses(slots, getModalUtilityClass, {});
};

function getContainer(container: ModalOwnerState['container']) {
return (typeof container === 'function' ? container() : container) as HTMLElement;
}

// A modal manager used to track and manage the state of open Modals.
// Modals don't open on the server so this won't conflict with concurrent requests.
const manager = new ModalManager();

const ModalRoot = styled('div', {
name: 'JoyModal',
slot: 'Root',
Expand Down Expand Up @@ -120,81 +98,6 @@ const Modal = React.forwardRef(function ModalU(inProps, ref) {
...other
} = props;

// @ts-ignore internal logic
const modal = React.useRef<{ modalRef: HTMLDivElement; mount: HTMLElement }>({});
const mountNodeRef = React.useRef<null | HTMLElement>(null);
const modalRef = React.useRef<null | HTMLDivElement>(null);
const handleRef = useForkRef(modalRef, ref);

let ariaHiddenProp = true;
if (
props['aria-hidden'] === 'false' ||
(typeof props['aria-hidden'] === 'boolean' && !props['aria-hidden'])
) {
ariaHiddenProp = false;
}

const getDoc = () => ownerDocument(mountNodeRef.current);
const getModal = () => {
modal.current.modalRef = modalRef.current as HTMLDivElement;
modal.current.mount = mountNodeRef.current as HTMLElement;
return modal.current;
};

const handleMounted = () => {
manager.mount(getModal(), { disableScrollLock });

// Fix a bug on Chrome where the scroll isn't initially 0.
if (modalRef.current) {
modalRef.current.scrollTop = 0;
}
};

const handleOpen = useEventCallback(() => {
const resolvedContainer = getContainer(container) || getDoc().body;

manager.add(getModal(), resolvedContainer);

// The element was already mounted.
if (modalRef.current) {
handleMounted();
}
});

const isTopModal = () => manager.isTopModal(getModal());

const handlePortalRef = useEventCallback((node: HTMLElement) => {
mountNodeRef.current = node;

if (!node) {
return;
}

if (open && isTopModal()) {
handleMounted();
} else if (modalRef.current) {
ariaHidden(modalRef.current, ariaHiddenProp);
}
});

const handleClose = React.useCallback(() => {
manager.remove(getModal(), ariaHiddenProp);
}, [ariaHiddenProp]);

React.useEffect(() => {
return () => {
handleClose();
};
}, [handleClose]);

React.useEffect(() => {
if (open) {
handleOpen();
} else {
handleClose();
}
}, [open, handleClose, handleOpen]);

const ownerState = {
...props,
disableAutoFocus,
Expand All @@ -207,62 +110,28 @@ const Modal = React.forwardRef(function ModalU(inProps, ref) {
keepMounted,
};

const { getRootProps, getBackdropProps, rootRef, portalRef, isTopModal } = useModal({
...ownerState,
ref,
});

const classes = useUtilityClasses(ownerState);
const externalForwardedProps = { ...other, component, slots, slotProps };

const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.target !== event.currentTarget) {
return;
}

if (onClose) {
onClose(event, 'backdropClick');
}
};

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (onKeyDown) {
onKeyDown(event);
}

// The handler doesn't take event.defaultPrevented into account:
//
// event.preventDefault() is meant to stop default behaviors like
// clicking a checkbox to check it, hitting a button to submit a form,
// and hitting left arrow to move the cursor in a text input etc.
// Only special HTML elements have these default behaviors.
if (event.key !== 'Escape' || !isTopModal()) {
return;
}

if (!disableEscapeKeyDown) {
// Swallow the event, in case someone is listening for the escape key on the body.
event.stopPropagation();

if (onClose) {
onClose(event, 'escapeKeyDown');
}
}
};

const [SlotRoot, rootProps] = useSlot('root', {
additionalProps: { role: 'presentation', onKeyDown: handleKeyDown },
ref: handleRef,
ref: rootRef,
className: classes.root,
elementType: ModalRoot,
externalForwardedProps,
getSlotProps: getRootProps,
ownerState,
});

const [SlotBackdrop, backdropProps] = useSlot('backdrop', {
additionalProps: {
'aria-hidden': true,
onClick: handleBackdropClick,
open,
},
className: classes.backdrop,
elementType: ModalBackdrop,
externalForwardedProps,
getSlotProps: getBackdropProps,
ownerState,
});

Expand All @@ -272,7 +141,7 @@ const Modal = React.forwardRef(function ModalU(inProps, ref) {

return (
<CloseModalContext.Provider value={onClose}>
<Portal ref={handlePortalRef} container={container} disablePortal={disablePortal}>
<Portal ref={portalRef} container={container} disablePortal={disablePortal}>
{/*
* Marking an element with the role presentation indicates to assistive technology
* that this element should be ignored; it exists to support the web application and
Expand Down
Loading
Loading