From 4d8a4920801a8d9f13838b9ea67eb273bcbdce95 Mon Sep 17 00:00:00 2001 From: SeyyedMahdi Date: Sun, 1 Oct 2023 00:25:33 +0330 Subject: [PATCH] [Snackbar][Material You] copy snackbar files to mui-material-next --- .../src/Snackbar/Snackbar.d.ts | 130 ++++ .../src/Snackbar/Snackbar.js | 312 +++++++++ .../src/Snackbar/Snackbar.test.js | 599 ++++++++++++++++++ .../mui-material-next/src/Snackbar/index.d.ts | 5 + .../mui-material-next/src/Snackbar/index.js | 5 + .../src/Snackbar/snackbarClasses.ts | 39 ++ 6 files changed, 1090 insertions(+) create mode 100644 packages/mui-material-next/src/Snackbar/Snackbar.d.ts create mode 100644 packages/mui-material-next/src/Snackbar/Snackbar.js create mode 100644 packages/mui-material-next/src/Snackbar/Snackbar.test.js create mode 100644 packages/mui-material-next/src/Snackbar/index.d.ts create mode 100644 packages/mui-material-next/src/Snackbar/index.js create mode 100644 packages/mui-material-next/src/Snackbar/snackbarClasses.ts diff --git a/packages/mui-material-next/src/Snackbar/Snackbar.d.ts b/packages/mui-material-next/src/Snackbar/Snackbar.d.ts new file mode 100644 index 00000000000000..15180260abd96e --- /dev/null +++ b/packages/mui-material-next/src/Snackbar/Snackbar.d.ts @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { SxProps } from '@mui/system'; +import { ClickAwayListenerProps } from '@mui/base/ClickAwayListener'; +import { InternalStandardProps as StandardProps } from '@mui/material'; +import { SnackbarContentProps } from '@mui/material/SnackbarContent'; +import { TransitionProps } from '@mui/material/transitions'; +import { Theme } from '../styles'; +import { SnackbarClasses } from './snackbarClasses'; + +export interface SnackbarOrigin { + vertical: 'top' | 'bottom'; + horizontal: 'left' | 'center' | 'right'; +} + +export type SnackbarCloseReason = 'timeout' | 'clickaway' | 'escapeKeyDown'; + +export interface SnackbarProps extends StandardProps> { + /** + * The action to display. It renders after the message, at the end of the snackbar. + */ + action?: SnackbarContentProps['action']; + /** + * The anchor of the `Snackbar`. + * On smaller screens, the component grows to occupy all the available width, + * the horizontal alignment is ignored. + * @default { vertical: 'bottom', horizontal: 'left' } + */ + anchorOrigin?: SnackbarOrigin; + /** + * The number of milliseconds to wait before automatically calling the + * `onClose` function. `onClose` should then set the state of the `open` + * prop to hide the Snackbar. This behavior is disabled by default with + * the `null` value. + * @default null + */ + autoHideDuration?: number | null; + /** + * Replace the `SnackbarContent` component. + */ + children?: React.ReactElement; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * Props applied to the `ClickAwayListener` element. + */ + ClickAwayListenerProps?: Partial; + /** + * Props applied to the [`SnackbarContent`](/material-ui/api/snackbar-content/) element. + */ + ContentProps?: Partial; + /** + * If `true`, the `autoHideDuration` timer will expire even if the window is not focused. + * @default false + */ + disableWindowBlurListener?: boolean; + /** + * When displaying multiple consecutive snackbars using a single parent-rendered + * ``, add the `key` prop to ensure independent treatment of each message. + * For instance, use ``. Otherwise, messages might update + * in place, and features like `autoHideDuration` could be affected. + */ + key?: any; + /** + * The message to display. + */ + message?: SnackbarContentProps['message']; + /** + * Callback fired when the component requests to be closed. + * Typically `onClose` is used to set state in the parent component, + * which is used to control the `Snackbar` `open` prop. + * The `reason` parameter can optionally be used to control the response to `onClose`, + * for example ignoring `clickaway`. + * + * @param {React.SyntheticEvent | Event} event The event source of the callback. + * @param {string} reason Can be: `"timeout"` (`autoHideDuration` expired), `"clickaway"`, or `"escapeKeyDown"`. + */ + onClose?: (event: React.SyntheticEvent | Event, reason: SnackbarCloseReason) => void; + /** + * If `true`, the component is shown. + */ + open?: boolean; + /** + * The number of milliseconds to wait before dismissing after user interaction. + * If `autoHideDuration` prop isn't specified, it does nothing. + * If `autoHideDuration` prop is specified but `resumeHideDuration` isn't, + * we default to `autoHideDuration / 2` ms. + */ + resumeHideDuration?: number; + /** + * The system prop that allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + /** + * The component used for the transition. + * [Follow this guide](/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component. + * @default Grow + */ + TransitionComponent?: React.JSXElementConstructor< + TransitionProps & { children: React.ReactElement } + >; + /** + * 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']; + /** + * Props applied to the transition element. + * By default, the element is based on this [`Transition`](http://reactcommunity.org/react-transition-group/transition/) component. + * @default {} + */ + TransitionProps?: TransitionProps; +} + +/** + * + * Demos: + * + * - [Snackbar](https://mui.com/material-ui/react-snackbar/) + * + * API: + * + * - [Snackbar API](https://mui.com/material-ui/api/snackbar/) + */ +export default function Snackbar(props: SnackbarProps): JSX.Element; diff --git a/packages/mui-material-next/src/Snackbar/Snackbar.js b/packages/mui-material-next/src/Snackbar/Snackbar.js new file mode 100644 index 00000000000000..b730fdf04c0fb7 --- /dev/null +++ b/packages/mui-material-next/src/Snackbar/Snackbar.js @@ -0,0 +1,312 @@ +'use client'; +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { unstable_composeClasses as composeClasses, useSlotProps } from '@mui/base'; +import { ClickAwayListener } from '@mui/base/ClickAwayListener'; +import { useSnackbar } from '@mui/base/useSnackbar'; +import { unstable_capitalize as capitalize } from '@mui/utils'; +import Grow from '@mui/material/Grow'; +import SnackbarContent from '@mui/material/SnackbarContent'; +import styled from '../styles/styled'; +import useTheme from '../styles/useTheme'; +import useThemeProps from '../styles/useThemeProps'; +import { getSnackbarUtilityClass } from './snackbarClasses'; + +const useUtilityClasses = (ownerState) => { + const { classes, anchorOrigin } = ownerState; + + const slots = { + root: [ + 'root', + `anchorOrigin${capitalize(anchorOrigin.vertical)}${capitalize(anchorOrigin.horizontal)}`, + ], + }; + + return composeClasses(slots, getSnackbarUtilityClass, classes); +}; + +const SnackbarRoot = styled('div', { + name: 'MuiSnackbar', + slot: 'Root', + overridesResolver: (props, styles) => { + const { ownerState } = props; + + return [ + styles.root, + styles[ + `anchorOrigin${capitalize(ownerState.anchorOrigin.vertical)}${capitalize( + ownerState.anchorOrigin.horizontal, + )}` + ], + ]; + }, +})(({ theme, ownerState }) => { + const center = { + left: '50%', + right: 'auto', + transform: 'translateX(-50%)', + }; + + return { + zIndex: (theme.vars || theme).zIndex.snackbar, + position: 'fixed', + display: 'flex', + left: 8, + right: 8, + justifyContent: 'center', + alignItems: 'center', + ...(ownerState.anchorOrigin.vertical === 'top' ? { top: 8 } : { bottom: 8 }), + ...(ownerState.anchorOrigin.horizontal === 'left' && { justifyContent: 'flex-start' }), + ...(ownerState.anchorOrigin.horizontal === 'right' && { justifyContent: 'flex-end' }), + [theme.breakpoints.up('sm')]: { + ...(ownerState.anchorOrigin.vertical === 'top' ? { top: 24 } : { bottom: 24 }), + ...(ownerState.anchorOrigin.horizontal === 'center' && center), + ...(ownerState.anchorOrigin.horizontal === 'left' && { + left: 24, + right: 'auto', + }), + ...(ownerState.anchorOrigin.horizontal === 'right' && { + right: 24, + left: 'auto', + }), + }, + }; +}); + +const Snackbar = React.forwardRef(function Snackbar(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiSnackbar' }); + const theme = useTheme(); + const defaultTransitionDuration = { + enter: theme.transitions.duration.enteringScreen, + exit: theme.transitions.duration.leavingScreen, + }; + + const { + action, + anchorOrigin: { vertical, horizontal } = { vertical: 'bottom', horizontal: 'left' }, + autoHideDuration = null, + children, + className, + ClickAwayListenerProps, + ContentProps, + disableWindowBlurListener = false, + message, + onBlur, + onClose, + onFocus, + onMouseEnter, + onMouseLeave, + open, + resumeHideDuration, + TransitionComponent = Grow, + transitionDuration = defaultTransitionDuration, + TransitionProps: { onEnter, onExited, ...TransitionProps } = {}, + ...other + } = props; + + const ownerState = { + ...props, + anchorOrigin: { vertical, horizontal }, + autoHideDuration, + disableWindowBlurListener, + TransitionComponent, + transitionDuration, + }; + + const classes = useUtilityClasses(ownerState); + + const { getRootProps, onClickAway } = useSnackbar({ ...ownerState }); + + const [exited, setExited] = React.useState(true); + + const rootProps = useSlotProps({ + elementType: SnackbarRoot, + getSlotProps: getRootProps, + externalForwardedProps: other, + ownerState, + additionalProps: { + ref, + }, + className: [classes.root, className], + }); + + const handleExited = (node) => { + setExited(true); + + if (onExited) { + onExited(node); + } + }; + + const handleEnter = (node, isAppearing) => { + setExited(false); + + if (onEnter) { + onEnter(node, isAppearing); + } + }; + + // So we only render active snackbars. + if (!open && exited) { + return null; + } + + return ( + + + + {children || } + + + + ); +}); + +Snackbar.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The action to display. It renders after the message, at the end of the snackbar. + */ + action: PropTypes.node, + /** + * The anchor of the `Snackbar`. + * On smaller screens, the component grows to occupy all the available width, + * the horizontal alignment is ignored. + * @default { vertical: 'bottom', horizontal: 'left' } + */ + anchorOrigin: PropTypes.shape({ + horizontal: PropTypes.oneOf(['center', 'left', 'right']).isRequired, + vertical: PropTypes.oneOf(['bottom', 'top']).isRequired, + }), + /** + * The number of milliseconds to wait before automatically calling the + * `onClose` function. `onClose` should then set the state of the `open` + * prop to hide the Snackbar. This behavior is disabled by default with + * the `null` value. + * @default null + */ + autoHideDuration: PropTypes.number, + /** + * Replace the `SnackbarContent` component. + */ + children: PropTypes.element, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, + /** + * Props applied to the `ClickAwayListener` element. + */ + ClickAwayListenerProps: PropTypes.object, + /** + * Props applied to the [`SnackbarContent`](/material-ui/api/snackbar-content/) element. + */ + ContentProps: PropTypes.object, + /** + * If `true`, the `autoHideDuration` timer will expire even if the window is not focused. + * @default false + */ + disableWindowBlurListener: PropTypes.bool, + /** + * When displaying multiple consecutive snackbars using a single parent-rendered + * ``, add the `key` prop to ensure independent treatment of each message. + * For instance, use ``. Otherwise, messages might update + * in place, and features like `autoHideDuration` could be affected. + */ + key: () => null, + /** + * The message to display. + */ + message: PropTypes.node, + /** + * @ignore + */ + onBlur: PropTypes.func, + /** + * Callback fired when the component requests to be closed. + * Typically `onClose` is used to set state in the parent component, + * which is used to control the `Snackbar` `open` prop. + * The `reason` parameter can optionally be used to control the response to `onClose`, + * for example ignoring `clickaway`. + * + * @param {React.SyntheticEvent | Event} event The event source of the callback. + * @param {string} reason Can be: `"timeout"` (`autoHideDuration` expired), `"clickaway"`, or `"escapeKeyDown"`. + */ + onClose: PropTypes.func, + /** + * @ignore + */ + onFocus: PropTypes.func, + /** + * @ignore + */ + onMouseEnter: PropTypes.func, + /** + * @ignore + */ + onMouseLeave: PropTypes.func, + /** + * If `true`, the component is shown. + */ + open: PropTypes.bool, + /** + * The number of milliseconds to wait before dismissing after user interaction. + * If `autoHideDuration` prop isn't specified, it does nothing. + * If `autoHideDuration` prop is specified but `resumeHideDuration` isn't, + * we default to `autoHideDuration / 2` ms. + */ + resumeHideDuration: PropTypes.number, + /** + * 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 component used for the transition. + * [Follow this guide](/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component. + * @default Grow + */ + TransitionComponent: PropTypes.elementType, + /** + * 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, + }), + ]), + /** + * Props applied to the transition element. + * By default, the element is based on this [`Transition`](http://reactcommunity.org/react-transition-group/transition/) component. + * @default {} + */ + TransitionProps: PropTypes.object, +}; + +export default Snackbar; diff --git a/packages/mui-material-next/src/Snackbar/Snackbar.test.js b/packages/mui-material-next/src/Snackbar/Snackbar.test.js new file mode 100644 index 00000000000000..9dc3fafe426c6c --- /dev/null +++ b/packages/mui-material-next/src/Snackbar/Snackbar.test.js @@ -0,0 +1,599 @@ +import * as React from 'react'; +import { expect } from 'chai'; +import { spy } from 'sinon'; +import { describeConformance, act, createRenderer, fireEvent } from '@mui-internal/test-utils'; +import Snackbar, { snackbarClasses as classes } from '@mui/material-next/Snackbar'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +describe('', () => { + const { clock, render: clientRender } = createRenderer({ clock: 'fake' }); + /** + * @type {typeof plainRender extends (...args: infer T) => any ? T : never} args + * + * @remarks + * This is for all intents and purposes the same as our client render method. + * `plainRender` is already wrapped in act(). + * However, React has a bug that flushes effects in a portal synchronously. + * We have to defer the effect manually like `useEffect` would so we have to flush the effect manually instead of relying on `act()`. + * React bug: https://github.com/facebook/react/issues/20074 + */ + function render(...args) { + const result = clientRender(...args); + clock.tick(0); + return result; + } + + describeConformance(, () => ({ + classes, + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + muiName: 'MuiSnackbar', + skip: [ + 'componentProp', + 'componentsProp', + 'themeVariants', + // react-transition-group issue + 'reactTestRenderer', + ], + })); + + describe('prop: onClose', () => { + it('should be call when clicking away', () => { + const handleClose = spy(); + render(); + + const event = new window.Event('click', { bubbles: true, cancelable: true }); + document.body.dispatchEvent(event); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([event, 'clickaway']); + }); + + it('should be called when pressing Escape', () => { + const handleClose = spy(); + render(); + + expect(fireEvent.keyDown(document.body, { key: 'Escape' })).to.equal(true); + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0][1]).to.deep.equal('escapeKeyDown'); + }); + + it('can limit which Snackbars are closed when pressing Escape', () => { + const handleCloseA = spy((event) => event.preventDefault()); + const handleCloseB = spy(); + render( + + + + , + ); + + fireEvent.keyDown(document.body, { key: 'Escape' }); + + expect(handleCloseA.callCount).to.equal(1); + expect(handleCloseB.callCount).to.equal(0); + }); + }); + + describe('Consecutive messages', () => { + it('should support synchronous onExited callback', () => { + const messageCount = 2; + + const onClose = spy(); + const onExited = spy(); + const duration = 250; + + let setSnackbarOpen; + function Test() { + const [open, setOpen] = React.useState(false); + setSnackbarOpen = setOpen; + + function handleClose() { + setOpen(false); + onClose(); + } + + function handleExited() { + onExited(); + if (onExited.callCount < messageCount) { + setOpen(true); + } + } + + return ( + + ); + } + render( + , + ); + + expect(onClose.callCount).to.equal(0); + expect(onExited.callCount).to.equal(0); + + act(() => { + setSnackbarOpen(true); + }); + clock.tick(duration); + + expect(onClose.callCount).to.equal(1); + expect(onExited.callCount).to.equal(0); + + clock.tick(duration / 2); + + expect(onClose.callCount).to.equal(1); + expect(onExited.callCount).to.equal(1); + + clock.tick(duration); + + expect(onClose.callCount).to.equal(messageCount); + expect(onExited.callCount).to.equal(1); + + clock.tick(duration / 2); + + expect(onClose.callCount).to.equal(messageCount); + expect(onExited.callCount).to.equal(messageCount); + }); + }); + + describe('prop: autoHideDuration', () => { + it('should call onClose when the timer is done', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + const { setProps } = render( + , + ); + + setProps({ open: true }); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); + }); + + it('calls onClose at timeout even if the prop changes', () => { + const handleClose1 = spy(); + const handleClose2 = spy(); + const autoHideDuration = 2e3; + const { setProps } = render( + , + ); + + setProps({ open: true }); + clock.tick(autoHideDuration / 2); + setProps({ open: true, onClose: handleClose2 }); + clock.tick(autoHideDuration / 2); + + expect(handleClose1.callCount).to.equal(0); + expect(handleClose2.callCount).to.equal(1); + }); + + it('should not call onClose when the autoHideDuration is reset', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + const { setProps } = render( + , + ); + + setProps({ open: true }); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration / 2); + setProps({ autoHideDuration: undefined }); + clock.tick(autoHideDuration / 2); + + expect(handleClose.callCount).to.equal(0); + }); + + it('should not call onClose if autoHideDuration is undefined', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + render( + , + ); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration); + + expect(handleClose.callCount).to.equal(0); + }); + + it('should not call onClose if autoHideDuration is null', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + + render(); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration); + + expect(handleClose.callCount).to.equal(0); + }); + + it('should not call onClose when closed', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + + const { setProps } = render( + , + ); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration / 2); + setProps({ open: false }); + clock.tick(autoHideDuration / 2); + + expect(handleClose.callCount).to.equal(0); + }); + }); + + [ + { + type: 'mouse', + enter: (container) => fireEvent.mouseEnter(container.querySelector('button')), + leave: (container) => fireEvent.mouseLeave(container.querySelector('button')), + }, + { + type: 'keyboard', + enter: (container) => act(() => container.querySelector('button').focus()), + leave: (container) => act(() => container.querySelector('button').blur()), + }, + ].forEach((userInteraction) => { + describe(`interacting with ${userInteraction.type}`, () => { + it('should be able to interrupt the timer', () => { + const handleMouseEnter = spy(); + const handleMouseLeave = spy(); + const handleBlur = spy(); + const handleFocus = spy(); + const handleClose = spy(); + const autoHideDuration = 2e3; + + const { container } = render( + undo} + open + onBlur={handleBlur} + onFocus={handleFocus} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + onClose={handleClose} + message="message" + autoHideDuration={autoHideDuration} + />, + ); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration / 2); + userInteraction.enter(container.querySelector('div')); + + if (userInteraction.type === 'keyboard') { + expect(handleFocus.callCount).to.equal(1); + } else { + expect(handleMouseEnter.callCount).to.equal(1); + } + + clock.tick(autoHideDuration / 2); + userInteraction.leave(container.querySelector('div')); + + if (userInteraction.type === 'keyboard') { + expect(handleBlur.callCount).to.equal(1); + } else { + expect(handleMouseLeave.callCount).to.equal(1); + } + expect(handleClose.callCount).to.equal(0); + + clock.tick(2e3); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); + }); + + it('should not call onClose with not timeout after user interaction', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + const resumeHideDuration = 3e3; + + const { container } = render( + undo} + open + onClose={handleClose} + message="message" + autoHideDuration={autoHideDuration} + resumeHideDuration={resumeHideDuration} + />, + ); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration / 2); + userInteraction.enter(container.querySelector('div')); + clock.tick(autoHideDuration / 2); + userInteraction.leave(container.querySelector('div')); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(2e3); + + expect(handleClose.callCount).to.equal(0); + }); + + it('should call onClose when timer done after user interaction', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + const resumeHideDuration = 3e3; + + const { container } = render( + undo} + open + onClose={handleClose} + message="message" + autoHideDuration={autoHideDuration} + resumeHideDuration={resumeHideDuration} + />, + ); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration / 2); + userInteraction.enter(container.querySelector('div')); + clock.tick(autoHideDuration / 2); + userInteraction.leave(container.querySelector('div')); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(resumeHideDuration); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); + }); + + it('should call onClose immediately after user interaction when 0', () => { + const handleClose = spy(); + const autoHideDuration = 6e3; + const resumeHideDuration = 0; + const { setProps, container } = render( + undo} + open + onClose={handleClose} + message="message" + autoHideDuration={autoHideDuration} + resumeHideDuration={resumeHideDuration} + />, + ); + + setProps({ open: true }); + + expect(handleClose.callCount).to.equal(0); + + userInteraction.enter(container.querySelector('div')); + clock.tick(100); + userInteraction.leave(container.querySelector('div')); + clock.tick(resumeHideDuration); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); + }); + }); + }); + + describe('prop: disableWindowBlurListener', () => { + it('should pause auto hide when not disabled and window lost focus', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + render( + , + ); + + act(() => { + const bEvent = new window.Event('blur', { + bubbles: false, + cancelable: false, + }); + window.dispatchEvent(bEvent); + }); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration); + + expect(handleClose.callCount).to.equal(0); + + act(() => { + const fEvent = new window.Event('focus', { + bubbles: false, + cancelable: false, + }); + window.dispatchEvent(fEvent); + }); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); + }); + + it('should not pause auto hide when disabled and window lost focus', () => { + const handleClose = spy(); + const autoHideDuration = 2e3; + render( + , + ); + + act(() => { + const event = new window.Event('blur', { bubbles: false, cancelable: false }); + window.dispatchEvent(event); + }); + + expect(handleClose.callCount).to.equal(0); + + clock.tick(autoHideDuration); + + expect(handleClose.callCount).to.equal(1); + expect(handleClose.args[0]).to.deep.equal([null, 'timeout']); + }); + }); + + describe('prop: open', () => { + it('should not render anything when closed', () => { + const { container } = render(); + expect(container).to.have.text(''); + }); + + it('should be able show it after mounted', () => { + const { container, setProps } = render(); + expect(container).to.have.text(''); + setProps({ open: true }); + expect(container).to.have.text('Hello, World!'); + }); + }); + + describe('prop: children', () => { + it('should render the children', () => { + const nodeRef = React.createRef(); + const children =
; + const { container } = render({children}); + expect(container).to.contain(nodeRef.current); + }); + }); + + describe('prop: TransitionComponent', () => { + it('should use a Grow by default', () => { + const childRef = React.createRef(); + render( + +
+ , + ); + expect(childRef.current.style.transform).to.contain('scale'); + }); + + it('accepts a different component that handles the transition', () => { + const transitionRef = React.createRef(); + function Transition() { + return
; + } + const { container } = render(); + expect(container).to.contain(transitionRef.current); + }); + }); + + describe('prop: transitionDuration', () => { + it('should render the default theme values by default', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const theme = createTheme(); + const enteringScreenDurationInSeconds = theme.transitions.duration.enteringScreen / 1000; + const { getByTestId } = render( + +
Foo
+
, + ); + + const child = getByTestId('child'); + expect(child).toHaveComputedStyle({ + transitionDuration: `${enteringScreenDurationInSeconds}s, 0.15s`, + }); + }); + + it('should render the custom theme values', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const theme = createTheme({ + transitions: { + duration: { + enteringScreen: 1, + }, + }, + }); + + const { getByTestId } = render( + + +
Foo
+
+
, + ); + + const child = getByTestId('child'); + expect(child).toHaveComputedStyle({ transitionDuration: '0.001s, 0.001s' }); + }); + + it('should render the values provided via prop', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + this.skip(); + } + + const { getByTestId } = render( + +
Foo
+
, + ); + + const child = getByTestId('child'); + expect(child).toHaveComputedStyle({ transitionDuration: '0.001s, 0.001s' }); + }); + }); +}); diff --git a/packages/mui-material-next/src/Snackbar/index.d.ts b/packages/mui-material-next/src/Snackbar/index.d.ts new file mode 100644 index 00000000000000..e55c472b4faa67 --- /dev/null +++ b/packages/mui-material-next/src/Snackbar/index.d.ts @@ -0,0 +1,5 @@ +export { default } from './Snackbar'; +export * from './Snackbar'; + +export { default as snackbarClasses } from './snackbarClasses'; +export * from './snackbarClasses'; diff --git a/packages/mui-material-next/src/Snackbar/index.js b/packages/mui-material-next/src/Snackbar/index.js new file mode 100644 index 00000000000000..c21dbfa25ec91d --- /dev/null +++ b/packages/mui-material-next/src/Snackbar/index.js @@ -0,0 +1,5 @@ +'use client'; +export { default } from './Snackbar'; + +export { default as snackbarClasses } from './snackbarClasses'; +export * from './snackbarClasses'; diff --git a/packages/mui-material-next/src/Snackbar/snackbarClasses.ts b/packages/mui-material-next/src/Snackbar/snackbarClasses.ts new file mode 100644 index 00000000000000..201882ecc5539c --- /dev/null +++ b/packages/mui-material-next/src/Snackbar/snackbarClasses.ts @@ -0,0 +1,39 @@ +import { + unstable_generateUtilityClasses as generateUtilityClasses, + unstable_generateUtilityClass as generateUtilityClass, +} from '@mui/utils'; + +export interface SnackbarClasses { + /** Styles applied to the root element. */ + root: string; + /** Styles applied to the root element if `anchorOrigin={{ 'top', 'center' }}`. */ + anchorOriginTopCenter: string; + /** Styles applied to the root element if `anchorOrigin={{ 'bottom', 'center' }}`. */ + anchorOriginBottomCenter: string; + /** Styles applied to the root element if `anchorOrigin={{ 'top', 'right' }}`. */ + anchorOriginTopRight: string; + /** Styles applied to the root element if `anchorOrigin={{ 'bottom', 'right' }}`. */ + anchorOriginBottomRight: string; + /** Styles applied to the root element if `anchorOrigin={{ 'top', 'left' }}`. */ + anchorOriginTopLeft: string; + /** Styles applied to the root element if `anchorOrigin={{ 'bottom', 'left' }}`. */ + anchorOriginBottomLeft: string; +} + +export type SnackbarClassKey = keyof SnackbarClasses; + +export function getSnackbarUtilityClass(slot: string): string { + return generateUtilityClass('MuiSnackbar', slot); +} + +const snackbarClasses: SnackbarClasses = generateUtilityClasses('MuiSnackbar', [ + 'root', + 'anchorOriginTopCenter', + 'anchorOriginBottomCenter', + 'anchorOriginTopRight', + 'anchorOriginBottomRight', + 'anchorOriginTopLeft', + 'anchorOriginBottomLeft', +]); + +export default snackbarClasses;