init project
Some checks failed
No response / noResponse (push) Has been cancelled
CI / Continuous releases (push) Has been cancelled
CI / test-dev (macos-latest) (push) Has been cancelled
CI / test-dev (ubuntu-latest) (push) Has been cancelled
CI / test-dev (windows-latest) (push) Has been cancelled
Maintenance / main (push) Has been cancelled
Scorecards supply-chain security / Scorecards analysis (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled

This commit is contained in:
how2ice
2025-12-12 14:26:25 +09:00
commit 005cf56baf
43188 changed files with 1079531 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
import * as React from 'react';
import { SxProps } from '@mui/system';
import { Theme } from '../styles';
import { InternalStandardProps as StandardProps } from '../internal';
import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';
import { ModalProps } from '../Modal';
import { BackdropProps } from '../Backdrop';
import { SlideProps } from '../Slide';
import { PaperProps } from '../Paper';
import { TransitionProps } from '../transitions/transition';
import { DrawerClasses } from './drawerClasses';
export interface DrawerRootSlotPropsOverrides {}
export interface DrawerDockedSlotPropsOverrides {}
export interface DrawerPaperSlotPropsOverrides {}
export interface DrawerTransitionSlotPropsOverrides {}
export interface DrawerBackdropSlotPropsOverrides {}
export interface DrawerSlots {
/**
* The component used for the root when the variant is `temporary`.
* @default Modal
*/
root: React.ElementType;
/**
* The component used for the Modal backdrop.
* @default Backdrop
*/
backdrop: React.ElementType;
/**
* The component used for the root element when the variant is `permanent` or `persistent`.
* @default div
*/
docked: React.ElementType;
/**
* The component used for the paper.
* @default Paper
*/
paper: React.ElementType;
/**
* The component used for the transition.
* [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
* @default Slide
*/
transition: React.ElementType;
}
export type DrawerSlotsAndSlotProps = CreateSlotsAndSlotProps<
DrawerSlots,
{
/**
* Props forwarded to the root slot.
* By default, the available props are based on the [Modal](https://mui.com/material-ui/api/modal/#props) component.
*/
root: SlotProps<React.ElementType<ModalProps>, DrawerRootSlotPropsOverrides, DrawerOwnerState>;
/**
* Props forwarded to the backdrop slot.
* By default, the available props are based on the [Backdrop](https://mui.com/material-ui/api/backdrop/#props) component.
*/
backdrop: SlotProps<
React.ElementType<BackdropProps>,
DrawerBackdropSlotPropsOverrides,
DrawerOwnerState
>;
/**
* Props forwarded to the docked slot.
* By default, the available props are based on a div element.
*/
docked: SlotProps<'div', DrawerDockedSlotPropsOverrides, DrawerOwnerState>;
/**
* Props forwarded to the paper slot.
* By default, the available props are based on the [Paper](https://mui.com/material-ui/api/paper/#props) component.
*/
paper: SlotProps<
React.ElementType<PaperProps>,
DrawerPaperSlotPropsOverrides,
DrawerOwnerState
>;
/**
* Props forwarded to the transition slot.
* By default, the available props are based on the [Slide](https://mui.com/material-ui/api/slide/#props) component.
*/
transition: SlotProps<
React.ElementType,
TransitionProps & DrawerTransitionSlotPropsOverrides,
DrawerOwnerState
>;
}
>;
export interface DrawerProps
extends StandardProps<ModalProps, 'open' | 'children' | 'slots' | 'slotProps'>,
DrawerSlotsAndSlotProps {
/**
* Side from which the drawer will appear.
* @default 'left'
*/
anchor?: 'left' | 'top' | 'right' | 'bottom';
/**
* The content of the component.
*/
children?: React.ReactNode;
/**
* Override or extend the styles applied to the component.
*/
classes?: Partial<DrawerClasses>;
/**
* The elevation of the drawer.
* @default 16
*/
elevation?: number;
/**
* Props applied to the [`Modal`](https://mui.com/material-ui/api/modal/) element.
* @default {}
*/
ModalProps?: Partial<ModalProps>;
/**
* Callback fired when the component requests to be closed.
* The `reason` parameter can optionally be used to control the response to `onClose`.
*
* @param {object} event The event source of the callback.
* @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
*/
onClose?: ModalProps['onClose'];
/**
* If `true`, the component is shown.
* @default false
*/
open?: boolean;
/**
* Props applied to the [`Paper`](https://mui.com/material-ui/api/paper/) element.
* @deprecated use the `slotProps.paper` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
* @default {}
*/
PaperProps?: Partial<PaperProps<React.ElementType>>;
/**
* Props applied to the [`Slide`](https://mui.com/material-ui/api/slide/) element.
* @deprecated use the `slotProps.transition` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
SlideProps?: Partial<SlideProps>;
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx?: SxProps<Theme>;
/**
* The duration for the transition, in milliseconds.
* You may specify a single timeout for all transitions, or individually with an object.
* @default {
* enter: theme.transitions.duration.enteringScreen,
* exit: theme.transitions.duration.leavingScreen,
* }
*/
transitionDuration?: TransitionProps['timeout'];
/**
* The variant to use.
* @default 'temporary'
*/
variant?: 'permanent' | 'persistent' | 'temporary';
}
// omit `slots` and `slotProps` to prevent recursion
export interface DrawerOwnerState extends Omit<DrawerProps, 'slots' | 'slotProps'> {}
/**
* The props of the [Modal](https://mui.com/material-ui/api/modal/) component are available
* when `variant="temporary"` is set.
*
* Demos:
*
* - [Drawer](https://mui.com/material-ui/react-drawer/)
*
* API:
*
* - [Drawer API](https://mui.com/material-ui/api/drawer/)
*/
export default function Drawer(props: DrawerProps): React.JSX.Element;

View File

@@ -0,0 +1,452 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import integerPropType from '@mui/utils/integerPropType';
import composeClasses from '@mui/utils/composeClasses';
import { useRtl } from '@mui/system/RtlProvider';
import Modal from '../Modal';
import Slide from '../Slide';
import Paper from '../Paper';
import capitalize from '../utils/capitalize';
import rootShouldForwardProp from '../styles/rootShouldForwardProp';
import { styled, useTheme } from '../zero-styled';
import memoTheme from '../utils/memoTheme';
import { useDefaultProps } from '../DefaultPropsProvider';
import { getDrawerUtilityClass } from './drawerClasses';
import useSlot from '../utils/useSlot';
import { mergeSlotProps } from '../utils';
const overridesResolver = (props, styles) => {
const { ownerState } = props;
return [
styles.root,
(ownerState.variant === 'permanent' || ownerState.variant === 'persistent') && styles.docked,
styles.modal,
];
};
const useUtilityClasses = (ownerState) => {
const { classes, anchor, variant } = ownerState;
const slots = {
root: ['root', `anchor${capitalize(anchor)}`],
docked: [(variant === 'permanent' || variant === 'persistent') && 'docked'],
modal: ['modal'],
paper: [
'paper',
`paperAnchor${capitalize(anchor)}`,
variant !== 'temporary' && `paperAnchorDocked${capitalize(anchor)}`,
],
};
return composeClasses(slots, getDrawerUtilityClass, classes);
};
const DrawerRoot = styled(Modal, {
name: 'MuiDrawer',
slot: 'Root',
overridesResolver,
})(
memoTheme(({ theme }) => ({
zIndex: (theme.vars || theme).zIndex.drawer,
})),
);
const DrawerDockedRoot = styled('div', {
shouldForwardProp: rootShouldForwardProp,
name: 'MuiDrawer',
slot: 'Docked',
skipVariantsResolver: false,
overridesResolver,
})({
flex: '0 0 auto',
});
const DrawerPaper = styled(Paper, {
name: 'MuiDrawer',
slot: 'Paper',
overridesResolver: (props, styles) => {
const { ownerState } = props;
return [
styles.paper,
styles[`paperAnchor${capitalize(ownerState.anchor)}`],
ownerState.variant !== 'temporary' &&
styles[`paperAnchorDocked${capitalize(ownerState.anchor)}`],
];
},
})(
memoTheme(({ theme }) => ({
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
height: '100%',
flex: '1 0 auto',
zIndex: (theme.vars || theme).zIndex.drawer,
// Add iOS momentum scrolling for iOS < 13.0
WebkitOverflowScrolling: 'touch',
// temporary style
position: 'fixed',
top: 0,
// We disable the focus ring for mouse, touch and keyboard users.
// At some point, it would be better to keep it for keyboard users.
// :focus-ring CSS pseudo-class will help.
outline: 0,
variants: [
{
props: {
anchor: 'left',
},
style: {
left: 0,
},
},
{
props: {
anchor: 'top',
},
style: {
top: 0,
left: 0,
right: 0,
height: 'auto',
maxHeight: '100%',
},
},
{
props: {
anchor: 'right',
},
style: {
right: 0,
},
},
{
props: {
anchor: 'bottom',
},
style: {
top: 'auto',
left: 0,
bottom: 0,
right: 0,
height: 'auto',
maxHeight: '100%',
},
},
{
props: ({ ownerState }) =>
ownerState.anchor === 'left' && ownerState.variant !== 'temporary',
style: {
borderRight: `1px solid ${(theme.vars || theme).palette.divider}`,
},
},
{
props: ({ ownerState }) =>
ownerState.anchor === 'top' && ownerState.variant !== 'temporary',
style: {
borderBottom: `1px solid ${(theme.vars || theme).palette.divider}`,
},
},
{
props: ({ ownerState }) =>
ownerState.anchor === 'right' && ownerState.variant !== 'temporary',
style: {
borderLeft: `1px solid ${(theme.vars || theme).palette.divider}`,
},
},
{
props: ({ ownerState }) =>
ownerState.anchor === 'bottom' && ownerState.variant !== 'temporary',
style: {
borderTop: `1px solid ${(theme.vars || theme).palette.divider}`,
},
},
],
})),
);
const oppositeDirection = {
left: 'right',
right: 'left',
top: 'down',
bottom: 'up',
};
export function isHorizontal(anchor) {
return ['left', 'right'].includes(anchor);
}
export function getAnchor({ direction }, anchor) {
return direction === 'rtl' && isHorizontal(anchor) ? oppositeDirection[anchor] : anchor;
}
/**
* The props of the [Modal](/material-ui/api/modal/) component are available
* when `variant="temporary"` is set.
*/
const Drawer = React.forwardRef(function Drawer(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiDrawer' });
const theme = useTheme();
const isRtl = useRtl();
const defaultTransitionDuration = {
enter: theme.transitions.duration.enteringScreen,
exit: theme.transitions.duration.leavingScreen,
};
const {
anchor: anchorProp = 'left',
BackdropProps,
children,
className,
elevation = 16,
hideBackdrop = false,
ModalProps: { BackdropProps: BackdropPropsProp, ...ModalProps } = {},
onClose,
open = false,
PaperProps = {},
SlideProps,
// eslint-disable-next-line react/prop-types
TransitionComponent,
transitionDuration = defaultTransitionDuration,
variant = 'temporary',
slots = {},
slotProps = {},
...other
} = props;
// Let's assume that the Drawer will always be rendered on user space.
// We use this state is order to skip the appear transition during the
// initial mount of the component.
const mounted = React.useRef(false);
React.useEffect(() => {
mounted.current = true;
}, []);
const anchorInvariant = getAnchor({ direction: isRtl ? 'rtl' : 'ltr' }, anchorProp);
const anchor = anchorProp;
const ownerState = {
...props,
anchor,
elevation,
open,
variant,
...other,
};
const classes = useUtilityClasses(ownerState);
const externalForwardedProps = {
slots: {
transition: TransitionComponent,
...slots,
},
slotProps: {
paper: PaperProps,
transition: SlideProps,
...slotProps,
backdrop: mergeSlotProps(slotProps.backdrop || { ...BackdropProps, ...BackdropPropsProp }, {
transitionDuration,
}),
},
};
const [RootSlot, rootSlotProps] = useSlot('root', {
ref,
elementType: DrawerRoot,
className: clsx(classes.root, classes.modal, className),
shouldForwardComponentProp: true,
ownerState,
externalForwardedProps: {
...externalForwardedProps,
...other,
...ModalProps,
},
additionalProps: {
open,
onClose,
hideBackdrop,
slots: {
backdrop: externalForwardedProps.slots.backdrop,
},
slotProps: {
backdrop: externalForwardedProps.slotProps.backdrop,
},
},
});
const [PaperSlot, paperSlotProps] = useSlot('paper', {
elementType: DrawerPaper,
shouldForwardComponentProp: true,
className: clsx(classes.paper, PaperProps.className),
ownerState,
externalForwardedProps,
additionalProps: {
elevation: variant === 'temporary' ? elevation : 0,
square: true,
...(variant === 'temporary' && {
role: 'dialog',
'aria-modal': 'true',
}),
},
});
const [DockedSlot, dockedSlotProps] = useSlot('docked', {
elementType: DrawerDockedRoot,
ref,
className: clsx(classes.root, classes.docked, className),
ownerState,
externalForwardedProps,
additionalProps: other, // pass `other` here because `DockedSlot` is also a root slot for some variants
});
const [TransitionSlot, transitionSlotProps] = useSlot('transition', {
elementType: Slide,
ownerState,
externalForwardedProps,
additionalProps: {
in: open,
direction: oppositeDirection[anchorInvariant],
timeout: transitionDuration,
appear: mounted.current,
},
});
const drawer = <PaperSlot {...paperSlotProps}>{children}</PaperSlot>;
if (variant === 'permanent') {
return <DockedSlot {...dockedSlotProps}>{drawer}</DockedSlot>;
}
const slidingDrawer = <TransitionSlot {...transitionSlotProps}>{drawer}</TransitionSlot>;
if (variant === 'persistent') {
return <DockedSlot {...dockedSlotProps}>{slidingDrawer}</DockedSlot>;
}
// variant === temporary
return <RootSlot {...rootSlotProps}>{slidingDrawer}</RootSlot>;
});
Drawer.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the d.ts file and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* Side from which the drawer will appear.
* @default 'left'
*/
anchor: PropTypes.oneOf(['bottom', 'left', 'right', 'top']),
/**
* @ignore
*/
BackdropProps: PropTypes.object,
/**
* The content of the component.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The elevation of the drawer.
* @default 16
*/
elevation: integerPropType,
/**
* If `true`, the backdrop is not rendered.
* @default false
*/
hideBackdrop: PropTypes.bool,
/**
* Props applied to the [`Modal`](https://mui.com/material-ui/api/modal/) element.
* @default {}
*/
ModalProps: PropTypes.object,
/**
* Callback fired when the component requests to be closed.
* The `reason` parameter can optionally be used to control the response to `onClose`.
*
* @param {object} event The event source of the callback.
* @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`.
*/
onClose: PropTypes.func,
/**
* If `true`, the component is shown.
* @default false
*/
open: PropTypes.bool,
/**
* Props applied to the [`Paper`](https://mui.com/material-ui/api/paper/) element.
* @deprecated use the `slotProps.paper` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
* @default {}
*/
PaperProps: PropTypes.object,
/**
* Props applied to the [`Slide`](https://mui.com/material-ui/api/slide/) element.
* @deprecated use the `slotProps.transition` prop instead. This prop will be removed in a future major release. See [Migrating from deprecated APIs](https://mui.com/material-ui/migration/migrating-from-deprecated-apis/) for more details.
*/
SlideProps: PropTypes.object,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
backdrop: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
docked: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
paper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
transition: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
backdrop: PropTypes.elementType,
docked: PropTypes.elementType,
paper: PropTypes.elementType,
root: PropTypes.elementType,
transition: PropTypes.elementType,
}),
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
PropTypes.func,
PropTypes.object,
]),
/**
* The duration for the transition, in milliseconds.
* You may specify a single timeout for all transitions, or individually with an object.
* @default {
* enter: theme.transitions.duration.enteringScreen,
* exit: theme.transitions.duration.leavingScreen,
* }
*/
transitionDuration: PropTypes.oneOfType([
PropTypes.number,
PropTypes.shape({
appear: PropTypes.number,
enter: PropTypes.number,
exit: PropTypes.number,
}),
]),
/**
* The variant to use.
* @default 'temporary'
*/
variant: PropTypes.oneOf(['permanent', 'persistent', 'temporary']),
};
export default Drawer;

View File

@@ -0,0 +1,79 @@
import * as React from 'react';
import { expectType } from '@mui/types';
import Drawer, { DrawerProps } from '@mui/material/Drawer';
import Grow from '@mui/material/Grow';
import { PaperProps } from '@mui/material/Paper';
const paperProps: PaperProps<'span'> = {
component: 'span',
onClick: (event) => {
expectType<React.MouseEvent<HTMLSpanElement, MouseEvent>, typeof event>(event);
},
};
function Test() {
return (
<React.Fragment>
<Drawer open />;
<Drawer open PaperProps={paperProps} />;
</React.Fragment>
);
}
<Drawer
slotProps={{
root: {
disablePortal: true,
},
backdrop: {
transitionDuration: 1000,
},
paper: {
elevation: 4,
},
docked: {
'aria-hidden': true,
},
transition: {
timeout: 500,
},
}}
/>;
function Noop() {
return null;
}
<Drawer
slots={{
root: 'div',
backdrop: Noop,
docked: 'div',
paper: 'div',
transition: Grow,
}}
/>;
function Custom(props: DrawerProps) {
const { slotProps, ...dialogProps } = props;
return (
<Drawer
slotProps={{
...slotProps,
transition: (ownerState) => {
const transitionProps =
typeof slotProps?.transition === 'function'
? slotProps.transition(ownerState)
: slotProps?.transition;
return {
...transitionProps,
onExited: (node) => {
transitionProps?.onExited?.(node);
},
};
},
}}
{...dialogProps}
>
test
</Drawer>
);
}

View File

@@ -0,0 +1,453 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, screen, isJsdom } from '@mui/internal-test-utils';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import Drawer, { drawerClasses as classes } from '@mui/material/Drawer';
import { modalClasses } from '@mui/material/Modal';
import { getAnchor, isHorizontal } from './Drawer';
import describeConformance from '../../test/describeConformance';
describe('<Drawer />', () => {
const { clock, render } = createRenderer({ clock: 'fake' });
const CustomPaper = React.forwardRef(
({ className, children, ownerState, square, ...props }, ref) => (
<i className={className} ref={ref} {...props} data-testid="custom">
{children}
</i>
),
);
const CustomBackdrop = React.forwardRef(({ transitionDuration, ownerState, ...props }, ref) => (
<i ref={ref} data-testid="custom" {...props} />
));
const CustomTransition = React.forwardRef(
({ onEnter, onExit, onExited, appear, in: inProp, ownerState, ...props }, ref) => (
<i ref={ref} data-testid="custom" {...props} />
),
);
describeConformance(
<Drawer open disablePortal>
<div />
</Drawer>,
() => ({
classes,
inheritComponent: 'div',
render,
muiName: 'MuiDrawer',
testDeepOverrides: { slotName: 'paper', slotClassName: classes.paper },
refInstanceof: window.HTMLDivElement,
slots: {
root: {
expectedClassName: classes.root,
testWithComponent: null,
testWithElement: null,
},
paper: {
expectedClassName: classes.paper,
testWithComponent: CustomPaper,
testWithElement: null, // already tested with CustomPaper
},
backdrop: { expectedClassName: modalClasses.backdrop, testWithElement: CustomBackdrop },
transition: {
expectedClassName: null,
testWithComponent: CustomTransition,
testWithElement: CustomTransition,
},
},
skip: ['componentProp', 'componentsProp', 'themeVariants'],
}),
);
// For `permanent` variant, the root is a div instead of a Modal.
describeConformance(
<Drawer variant="permanent">
<div />
</Drawer>,
() => ({
classes,
inheritComponent: 'div',
render,
muiName: 'MuiDrawer',
testVariantProps: { variant: 'persistent' },
refInstanceof: window.HTMLDivElement,
slots: {
docked: {
expectedClassName: classes.docked,
},
},
skip: ['componentProp', 'componentsProp'],
}),
);
describe('prop: variant=temporary', () => {
describe('transitionDuration property', () => {
const transitionDuration = {
enter: 854,
exit: 2967,
};
it.skipIf(isJsdom())(
'should delay the slide transition to complete using default theme values by default',
function test() {
const theme = createTheme();
const enteringScreenDurationInSeconds = theme.transitions.duration.enteringScreen / 1000;
render(
<Drawer open>
<div />
</Drawer>,
);
const container = document.querySelector(`.${classes.root}`);
const backdropRoot = container.firstChild;
expect(backdropRoot).toHaveComputedStyle({
transitionDuration: `${enteringScreenDurationInSeconds}s`,
});
},
);
it.skipIf(isJsdom())(
'should delay the slide transition to complete using custom theme values',
function test() {
const theme = createTheme({
transitions: {
duration: {
enteringScreen: 1,
},
},
});
render(
<ThemeProvider theme={theme}>
<Drawer open>
<div />
</Drawer>
</ThemeProvider>,
);
const container = document.querySelector(`.${classes.root}`);
const backdropRoot = container.firstChild;
expect(backdropRoot).toHaveComputedStyle({ transitionDuration: '0.001s' });
},
);
it('delay the slide transition to complete using values provided via prop', () => {
const handleEntered = spy();
const { setProps } = render(
<Drawer
open={false}
transitionDuration={transitionDuration}
SlideProps={{ onEntered: handleEntered }}
>
<div />
</Drawer>,
);
setProps({ open: true });
expect(handleEntered.callCount).to.equal(0);
clock.tick(transitionDuration.enter);
expect(handleEntered.callCount).to.equal(1);
});
});
describe('accessibility', () => {
it('should have role="dialog" and aria-modal="true" when variant is temporary', () => {
render(
<Drawer open variant="temporary">
<div data-testid="child" />
</Drawer>,
);
const paper = document.querySelector(`.${classes.paper}`);
expect(paper).to.have.attribute('role', 'dialog');
expect(paper).to.have.attribute('aria-modal', 'true');
});
it('should not have role="dialog" and aria-modal="true" when variant is permanent', () => {
render(
<Drawer variant="permanent">
<div data-testid="child" />
</Drawer>,
);
const paper = document.querySelector(`.${classes.paper}`);
expect(paper).not.to.have.attribute('role');
expect(paper).not.to.have.attribute('aria-modal');
});
it('should not have role="dialog" and aria-modal="true" when variant is persistent', () => {
render(
<Drawer variant="persistent">
<div data-testid="child" />
</Drawer>,
);
const paper = document.querySelector(`.${classes.paper}`);
expect(paper).not.to.have.attribute('role');
expect(paper).not.to.have.attribute('aria-modal');
});
});
it('should set the custom className for Modal when variant is temporary', () => {
render(
<Drawer className="woofDrawer" open variant="temporary">
<div />
</Drawer>,
);
expect(document.querySelector(`.${classes.modal}`)).to.have.class('woofDrawer');
});
it('should set the Paper className', () => {
render(
<Drawer classes={{ paper: 'woofDrawer' }} open>
<div />
</Drawer>,
);
expect(document.querySelector(`.${classes.paper}`)).to.have.class('woofDrawer');
});
it('should be closed by default', () => {
render(
<Drawer>
<div data-testid="child" />
</Drawer>,
);
expect(screen.queryByTestId('child')).to.equal(null);
});
describe('opening and closing', () => {
const transitionDuration = 123;
const drawerElement = (
<Drawer transitionDuration={transitionDuration}>
<div data-testid="child" />
</Drawer>
);
it('should open and close', () => {
const { setProps } = render(drawerElement);
setProps({ open: true });
expect(screen.getByTestId('child')).not.to.equal(null);
setProps({ open: false });
clock.tick(transitionDuration);
expect(screen.queryByTestId('child')).to.equal(null);
});
});
});
describe('prop: variant=persistent', () => {
it('should render a div instead of a Modal when persistent', () => {
const { container } = render(
<Drawer variant="persistent">
<div />
</Drawer>,
);
expect(container.firstChild).to.have.tagName('div');
expect(container.firstChild).to.have.class(classes.docked);
});
it('should render Slide > Paper inside the div', () => {
const transitionDuration = 123;
const handleEntered = spy();
const { container, setProps } = render(
<Drawer
open={false}
transitionDuration={transitionDuration}
SlideProps={{ onEntered: handleEntered }}
variant="persistent"
>
<div />
</Drawer>,
);
setProps({ open: true });
expect(handleEntered.callCount).to.equal(0);
clock.tick(transitionDuration);
expect(handleEntered.callCount).to.equal(1);
expect(container.firstChild.firstChild).to.have.class(classes.paper);
});
});
describe('prop: variant=permanent', () => {
const drawerElement = (
<Drawer variant="permanent">
<div />
</Drawer>
);
it('should render a div instead of a Modal when permanent', () => {
const { container } = render(drawerElement);
const root = container.querySelector(`.${classes.root}`);
expect(root).not.to.equal(null);
expect(root).to.have.tagName('div');
expect(root).to.have.class(classes.docked);
});
});
describe('prop: PaperProps', () => {
it('should merge class names', () => {
const { container } = render(
<Drawer PaperProps={{ className: 'my-class' }} variant="permanent">
<div />
</Drawer>,
);
expect(container.querySelector(`.${classes.paper}`)).to.have.class('my-class');
});
});
describe('slide direction', () => {
it('should return the opposing slide direction', () => {
const MockedSlide = React.forwardRef(function MockedSlide(props, ref) {
const { children, in: inProp, direction } = props;
if (!inProp) {
return null;
}
return (
<div data-direction={direction} data-testid="slide" ref={ref} tabIndex={-1}>
{children}
</div>
);
});
const { setProps } = render(
<Drawer open TransitionComponent={MockedSlide}>
<div />
</Drawer>,
);
setProps({ anchor: 'left' });
expect(screen.getByTestId('slide')).to.have.attribute('data-direction', 'right');
setProps({ anchor: 'right' });
expect(screen.getByTestId('slide')).to.have.attribute('data-direction', 'left');
setProps({ anchor: 'top' });
expect(screen.getByTestId('slide')).to.have.attribute('data-direction', 'down');
setProps({ anchor: 'bottom' });
expect(screen.getByTestId('slide')).to.have.attribute('data-direction', 'up');
});
});
describe('Right To Left', () => {
it('should switch left and right anchor when theme is right-to-left', () => {
const MockedSlide = React.forwardRef(function MockedSlide(props, ref) {
const { children, in: inProp, direction } = props;
if (!inProp) {
return null;
}
return (
<div data-direction={direction} data-testid="slide" ref={ref} tabIndex={-1}>
{children}
</div>
);
});
const theme = createTheme({
direction: 'rtl',
});
const view = render(
<ThemeProvider theme={theme}>
<Drawer open anchor="left" TransitionComponent={MockedSlide}>
<div />
</Drawer>
</ThemeProvider>,
);
// slide direction for left is right, if left is switched to right, we should get left
expect(screen.getByTestId('slide')).to.have.attribute('data-direction', 'left');
view.rerender(
<ThemeProvider theme={theme}>
<Drawer open anchor="right" TransitionComponent={MockedSlide}>
<div />
</Drawer>
</ThemeProvider>,
);
// slide direction for right is left, if right is switched to left, we should get right
expect(screen.getByTestId('slide')).to.have.attribute('data-direction', 'right');
});
});
describe('isHorizontal', () => {
it('should recognize left and right as horizontal swiping directions', () => {
expect(isHorizontal('left')).to.equal(true);
expect(isHorizontal('right')).to.equal(true);
expect(isHorizontal('top')).to.equal(false);
expect(isHorizontal('bottom')).to.equal(false);
});
});
describe('getAnchor', () => {
it('should return the anchor', () => {
const theme = { direction: 'ltr' };
expect(getAnchor(theme, 'left')).to.equal('left');
expect(getAnchor(theme, 'right')).to.equal('right');
expect(getAnchor(theme, 'top')).to.equal('top');
expect(getAnchor(theme, 'bottom')).to.equal('bottom');
});
it('should switch left/right if RTL is enabled', () => {
const theme = { direction: 'rtl' };
expect(getAnchor(theme, 'left')).to.equal('right');
expect(getAnchor(theme, 'right')).to.equal('left');
});
});
describe('zIndex', () => {
it('should set correct zIndex on the root element', () => {
const theme = createTheme();
render(
<ThemeProvider theme={theme}>
<Drawer open>
<div />
</Drawer>
</ThemeProvider>,
);
expect(document.querySelector(`.${classes.root}`)).toHaveComputedStyle({
zIndex: String(theme.zIndex.drawer),
});
});
});
describe('prop: anchor', () => {
it('should set correct class name on the root element', () => {
const { setProps } = render(
<Drawer open anchor="left">
<div />
</Drawer>,
);
expect(document.querySelector(`.${classes.root}`)).to.have.class(classes.anchorLeft);
setProps({ anchor: 'right' });
expect(document.querySelector(`.${classes.root}`)).to.have.class(classes.anchorRight);
setProps({ anchor: 'top' });
expect(document.querySelector(`.${classes.root}`)).to.have.class(classes.anchorTop);
setProps({ anchor: 'bottom' });
expect(document.querySelector(`.${classes.root}`)).to.have.class(classes.anchorBottom);
});
});
});

View File

@@ -0,0 +1,80 @@
import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
import generateUtilityClass from '@mui/utils/generateUtilityClass';
export interface DrawerClasses {
/** Styles applied to the root element. */
root: string;
/** Styles applied to the root element if `variant="permanent or persistent"`. */
docked: string;
/** Styles applied to the Paper component. */
paper: string;
/** Styles applied to the root element if `anchor="left"`. */
anchorLeft: string;
/** Styles applied to the root element if `anchor="right"`. */
anchorRight: string;
/** Styles applied to the root element if `anchor="top"`. */
anchorTop: string;
/** Styles applied to the root element if `anchor="bottom"`. */
anchorBottom: string;
/** Styles applied to the Paper component if `anchor="left"`.
* @deprecated Combine the [.MuiDrawer-anchorLeft](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorLeft) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorLeft: string;
/** Styles applied to the Paper component if `anchor="right"`.
* @deprecated Combine the [.MuiDrawer-anchorRight](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorRight) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorRight: string;
/** Styles applied to the Paper component if `anchor="top"`.
* @deprecated Combine the [.MuiDrawer-anchorTop](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorTop) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorTop: string;
/** Styles applied to the Paper component if `anchor="bottom"`.
* @deprecated Combine the [.MuiDrawer-anchorBottom](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorBottom) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorBottom: string;
/** Styles applied to the Paper component if `anchor="left"` and `variant` is not "temporary".
* @deprecated Combine the [.MuiDrawer-anchorLeft](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorLeft), [.MuiDrawer-docked](/material-ui/api/drawer/#Drawer-css-MuiDrawer-docked) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorDockedLeft: string;
/** Styles applied to the Paper component if `anchor="top"` and `variant` is not "temporary".
* @deprecated Combine the [.MuiDrawer-anchorTop](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorTop), [.MuiDrawer-docked](/material-ui/api/drawer/#Drawer-css-MuiDrawer-docked) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorDockedTop: string;
/** Styles applied to the Paper component if `anchor="right"` and `variant` is not "temporary".
* @deprecated Combine the [.MuiDrawer-anchorRight](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorRight), [.MuiDrawer-docked](/material-ui/api/drawer/#Drawer-css-MuiDrawer-docked) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorDockedRight: string;
/** Styles applied to the Paper component if `anchor="bottom"` and `variant` is not "temporary".
* @deprecated Combine the [.MuiDrawer-anchorBottom](/material-ui/api/drawer/#drawer-classes-MuiDrawer-anchorBottom), [.MuiDrawer-docked](/material-ui/api/drawer/#Drawer-css-MuiDrawer-docked) and [.MuiDrawer-paper](/material-ui/api/drawer/#Drawer-css-MuiDrawer-paper) classes instead. [How to migrate](/material-ui/migration/migrating-from-deprecated-apis/)
*/
paperAnchorDockedBottom: string;
/** Styles applied to the Modal component. */
modal: string;
}
export type DrawerClassKey = keyof DrawerClasses;
export function getDrawerUtilityClass(slot: string): string {
return generateUtilityClass('MuiDrawer', slot);
}
const drawerClasses: DrawerClasses = generateUtilityClasses('MuiDrawer', [
'root',
'docked',
'paper',
'anchorLeft',
'anchorRight',
'anchorTop',
'anchorBottom',
'paperAnchorLeft',
'paperAnchorRight',
'paperAnchorTop',
'paperAnchorBottom',
'paperAnchorDockedLeft',
'paperAnchorDockedRight',
'paperAnchorDockedTop',
'paperAnchorDockedBottom',
'modal',
]);
export default drawerClasses;

View File

@@ -0,0 +1,5 @@
export { default } from './Drawer';
export * from './Drawer';
export { default as drawerClasses } from './drawerClasses';
export * from './drawerClasses';

View File

@@ -0,0 +1,4 @@
export { default } from './Drawer';
export { default as drawerClasses } from './drawerClasses';
export * from './drawerClasses';