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 all commits
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
10 changes: 10 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,16 @@ module.exports = {
'no-console': 'off',
},
},
// demos - proptype generation
{
files: ['docs/data/base/components/modal/UseModal.js'],
Comment on lines +302 to +304
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe to generalize, if it's code generated, I guess this applies to everything.

rules: {
'consistent-return': 'off',
'func-names': 'off',
'no-else-return': 'off',
'prefer-template': 'off',
},
},
{
files: ['docs/data/**/*.tsx'],
excludedFiles: [
Expand Down
256 changes: 256 additions & 0 deletions docs/data/base/components/modal/UseModal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { Box, styled } from '@mui/system';
import { Portal } from '@mui/base/Portal';
import { FocusTrap } from '@mui/base/FocusTrap';
import { Button } from '@mui/base/Button';
import { unstable_useModal as useModal } from '@mui/base/unstable_useModal';
import Fade from '@mui/material/Fade';

export default function UseModal() {
const [open, setOpen] = React.useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);

return (
<div>
<TriggerButton onClick={handleOpen}>Open modal</TriggerButton>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
>
<Fade in={open}>
<Box sx={style}>
<h2 id="transition-modal-title">Text in a modal</h2>
<span id="transition-modal-description" style={{ marginTop: 16 }}>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
</span>
</Box>
</Fade>
</Modal>
</div>
);
}

const Modal = React.forwardRef(function Modal(props, forwardedRef) {
const {
children,
closeAfterTransition = false,
container,
disableAutoFocus = false,
disableEnforceFocus = false,
disableEscapeKeyDown = false,
disablePortal = false,
disableRestoreFocus = false,
disableScrollLock = false,
hideBackdrop = false,
keepMounted = false,
onClose,
open,
onTransitionEnter,
onTransitionExited,
...other
} = props;

const propsWithDefaults = {
...props,
closeAfterTransition,
disableAutoFocus,
disableEnforceFocus,
disableEscapeKeyDown,
disablePortal,
disableRestoreFocus,
disableScrollLock,
hideBackdrop,
keepMounted,
};

const {
getRootProps,
getBackdropProps,
getTransitionProps,
portalRef,
isTopModal,
exited,
hasTransition,
} = useModal({
...propsWithDefaults,
rootRef: forwardedRef,
});

const classes = {
hidden: !open && exited,
};

const childProps = {};
if (children.props.tabIndex === undefined) {
childProps.tabIndex = '-1';
}

// It's a Transition like component
if (hasTransition) {
const { onEnter, onExited } = getTransitionProps();
childProps.onEnter = onEnter;
childProps.onExited = onExited;
}

const rootProps = {
...other,
className: clsx(classes),
...getRootProps(other),
};

const backdropProps = {
open,
...getBackdropProps(),
};

if (!keepMounted && !open && (!hasTransition || exited)) {
return null;
}

return (
<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
* is not meant for humans to interact with directly.
* https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/no-static-element-interactions.md
*/}
<CustomModalRoot {...rootProps}>
{!hideBackdrop ? <CustomModalBackdrop {...backdropProps} /> : null}
<FocusTrap
disableEnforceFocus={disableEnforceFocus}
disableAutoFocus={disableAutoFocus}
disableRestoreFocus={disableRestoreFocus}
isEnabled={isTopModal}
open={open}
>
{React.cloneElement(children, childProps)}
</FocusTrap>
</CustomModalRoot>
</Portal>
);
});

Modal.propTypes = {
children: PropTypes.element.isRequired,
closeAfterTransition: PropTypes.bool,
container: PropTypes.oneOfType([
function (props, propName) {
if (props[propName] == null) {
return new Error("Prop '" + propName + "' is required but wasn't specified");
} else if (
typeof props[propName] !== 'object' ||
props[propName].nodeType !== 1
) {
return new Error("Expected prop '" + propName + "' to be of type Element");
}
},
PropTypes.func,
]),
disableAutoFocus: PropTypes.bool,
disableEnforceFocus: PropTypes.bool,
disableEscapeKeyDown: PropTypes.bool,
disablePortal: PropTypes.bool,
disableRestoreFocus: PropTypes.bool,
disableScrollLock: PropTypes.bool,
hideBackdrop: PropTypes.bool,
keepMounted: PropTypes.bool,
onClose: PropTypes.func,
onTransitionEnter: PropTypes.func,
onTransitionExited: PropTypes.func,
open: PropTypes.bool.isRequired,
};

const Backdrop = React.forwardRef((props, ref) => {
const { open, ...other } = props;
return (
<Fade in={open}>
<div ref={ref} {...other} />
</Fade>
);
});

Backdrop.propTypes = {
open: PropTypes.bool,
};

const blue = {
200: '#99CCF3',
400: '#3399FF',
500: '#007FFF',
};

const grey = {
50: '#f6f8fa',
100: '#eaeef2',
200: '#d0d7de',
300: '#afb8c1',
400: '#8c959f',
500: '#6e7781',
600: '#57606a',
700: '#424a53',
800: '#32383f',
900: '#24292f',
};

const style = (theme) => ({
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
borderRadius: '12px',
padding: '16px 32px 24px 32px',
backgroundColor: theme.palette.mode === 'dark' ? '#0A1929' : 'white',
boxShadow: `0px 2px 24px ${theme.palette.mode === 'dark' ? '#000' : '#383838'}`,
});

const CustomModalRoot = styled('div')`
position: fixed;
z-index: 1300;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
`;

const CustomModalBackdrop = styled(Backdrop)`
z-index: -1;
position: fixed;
inset: 0;
background-color: rgb(0 0 0 / 0.5);
-webkit-tap-highlight-color: transparent;
`;

const TriggerButton = styled(Button)(
({ theme }) => `
font-family: IBM Plex Sans, sans-serif;
font-size: 0.875rem;
font-weight: 600;
box-sizing: border-box;
min-height: calc(1.5em + 22px);
border-radius: 12px;
padding: 6px 12px;
line-height: 1.5;
background: transparent;
border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
color: ${theme.palette.mode === 'dark' ? grey[100] : grey[900]};

&:hover {
background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
}

&:focus-visible {
border-color: ${blue[400]};
outline: 3px solid ${theme.palette.mode === 'dark' ? blue[500] : blue[200]};
}
`,
);
Loading