UNPKG

16 kBJavaScriptView Raw
1'use client';
2
3import * as React from 'react';
4import { isFragment } from 'react-is';
5import PropTypes from 'prop-types';
6import clsx from 'clsx';
7import composeClasses from '@mui/utils/composeClasses';
8import useTimeout from '@mui/utils/useTimeout';
9import clamp from '@mui/utils/clamp';
10import { styled, useTheme } from "../zero-styled/index.js";
11import memoTheme from "../utils/memoTheme.js";
12import { useDefaultProps } from "../DefaultPropsProvider/index.js";
13import Zoom from "../Zoom/index.js";
14import Fab from "../Fab/index.js";
15import capitalize from "../utils/capitalize.js";
16import isMuiElement from "../utils/isMuiElement.js";
17import useForkRef from "../utils/useForkRef.js";
18import useControlled from "../utils/useControlled.js";
19import speedDialClasses, { getSpeedDialUtilityClass } from "./speedDialClasses.js";
20import useSlot from "../utils/useSlot.js";
21import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
22const useUtilityClasses = ownerState => {
23 const {
24 classes,
25 open,
26 direction
27 } = ownerState;
28 const slots = {
29 root: ['root', `direction${capitalize(direction)}`],
30 fab: ['fab'],
31 actions: ['actions', !open && 'actionsClosed']
32 };
33 return composeClasses(slots, getSpeedDialUtilityClass, classes);
34};
35function getOrientation(direction) {
36 if (direction === 'up' || direction === 'down') {
37 return 'vertical';
38 }
39 if (direction === 'right' || direction === 'left') {
40 return 'horizontal';
41 }
42 return undefined;
43}
44const dialRadius = 32;
45const spacingActions = 16;
46const SpeedDialRoot = styled('div', {
47 name: 'MuiSpeedDial',
48 slot: 'Root',
49 overridesResolver: (props, styles) => {
50 const {
51 ownerState
52 } = props;
53 return [styles.root, styles[`direction${capitalize(ownerState.direction)}`]];
54 }
55})(memoTheme(({
56 theme
57}) => ({
58 zIndex: (theme.vars || theme).zIndex.speedDial,
59 display: 'flex',
60 alignItems: 'center',
61 pointerEvents: 'none',
62 variants: [{
63 props: {
64 direction: 'up'
65 },
66 style: {
67 flexDirection: 'column-reverse',
68 [`& .${speedDialClasses.actions}`]: {
69 flexDirection: 'column-reverse',
70 marginBottom: -dialRadius,
71 paddingBottom: spacingActions + dialRadius
72 }
73 }
74 }, {
75 props: {
76 direction: 'down'
77 },
78 style: {
79 flexDirection: 'column',
80 [`& .${speedDialClasses.actions}`]: {
81 flexDirection: 'column',
82 marginTop: -dialRadius,
83 paddingTop: spacingActions + dialRadius
84 }
85 }
86 }, {
87 props: {
88 direction: 'left'
89 },
90 style: {
91 flexDirection: 'row-reverse',
92 [`& .${speedDialClasses.actions}`]: {
93 flexDirection: 'row-reverse',
94 marginRight: -dialRadius,
95 paddingRight: spacingActions + dialRadius
96 }
97 }
98 }, {
99 props: {
100 direction: 'right'
101 },
102 style: {
103 flexDirection: 'row',
104 [`& .${speedDialClasses.actions}`]: {
105 flexDirection: 'row',
106 marginLeft: -dialRadius,
107 paddingLeft: spacingActions + dialRadius
108 }
109 }
110 }]
111})));
112const SpeedDialFab = styled(Fab, {
113 name: 'MuiSpeedDial',
114 slot: 'Fab',
115 overridesResolver: (props, styles) => styles.fab
116})({
117 pointerEvents: 'auto'
118});
119const SpeedDialActions = styled('div', {
120 name: 'MuiSpeedDial',
121 slot: 'Actions',
122 overridesResolver: (props, styles) => {
123 const {
124 ownerState
125 } = props;
126 return [styles.actions, !ownerState.open && styles.actionsClosed];
127 }
128})({
129 display: 'flex',
130 pointerEvents: 'auto',
131 variants: [{
132 props: ({
133 ownerState
134 }) => !ownerState.open,
135 style: {
136 transition: 'top 0s linear 0.2s',
137 pointerEvents: 'none'
138 }
139 }]
140});
141const SpeedDial = /*#__PURE__*/React.forwardRef(function SpeedDial(inProps, ref) {
142 const props = useDefaultProps({
143 props: inProps,
144 name: 'MuiSpeedDial'
145 });
146 const theme = useTheme();
147 const defaultTransitionDuration = {
148 enter: theme.transitions.duration.enteringScreen,
149 exit: theme.transitions.duration.leavingScreen
150 };
151 const {
152 ariaLabel,
153 FabProps: {
154 ref: origDialButtonRef,
155 ...FabProps
156 } = {},
157 children: childrenProp,
158 className,
159 direction = 'up',
160 hidden = false,
161 icon,
162 onBlur,
163 onClose,
164 onFocus,
165 onKeyDown,
166 onMouseEnter,
167 onMouseLeave,
168 onOpen,
169 open: openProp,
170 openIcon,
171 slots = {},
172 slotProps = {},
173 TransitionComponent: TransitionComponentProp,
174 TransitionProps: TransitionPropsProp,
175 transitionDuration = defaultTransitionDuration,
176 ...other
177 } = props;
178 const [open, setOpenState] = useControlled({
179 controlled: openProp,
180 default: false,
181 name: 'SpeedDial',
182 state: 'open'
183 });
184 const ownerState = {
185 ...props,
186 open,
187 direction
188 };
189 const classes = useUtilityClasses(ownerState);
190 const eventTimer = useTimeout();
191
192 /**
193 * an index in actions.current
194 */
195 const focusedAction = React.useRef(0);
196
197 /**
198 * pressing this key while the focus is on a child SpeedDialAction focuses
199 * the next SpeedDialAction.
200 * It is equal to the first arrow key pressed while focus is on the SpeedDial
201 * that is not orthogonal to the direction.
202 * @type {utils.ArrowKey?}
203 */
204 const nextItemArrowKey = React.useRef();
205
206 /**
207 * refs to the Button that have an action associated to them in this SpeedDial
208 * [Fab, ...(SpeedDialActions > Button)]
209 * @type {HTMLButtonElement[]}
210 */
211 const actions = React.useRef([]);
212 actions.current = [actions.current[0]];
213 const handleOwnFabRef = React.useCallback(fabFef => {
214 actions.current[0] = fabFef;
215 }, []);
216 const handleFabRef = useForkRef(origDialButtonRef, handleOwnFabRef);
217
218 /**
219 * creates a ref callback for the Button in a SpeedDialAction
220 * Is called before the original ref callback for Button that was set in buttonProps
221 *
222 * @param dialActionIndex {number}
223 * @param origButtonRef {React.RefObject?}
224 */
225 const createHandleSpeedDialActionButtonRef = (dialActionIndex, origButtonRef) => {
226 return buttonRef => {
227 actions.current[dialActionIndex + 1] = buttonRef;
228 if (origButtonRef) {
229 origButtonRef(buttonRef);
230 }
231 };
232 };
233 const handleKeyDown = event => {
234 if (onKeyDown) {
235 onKeyDown(event);
236 }
237 const key = event.key.replace('Arrow', '').toLowerCase();
238 const {
239 current: nextItemArrowKeyCurrent = key
240 } = nextItemArrowKey;
241 if (event.key === 'Escape') {
242 setOpenState(false);
243 actions.current[0].focus();
244 if (onClose) {
245 onClose(event, 'escapeKeyDown');
246 }
247 return;
248 }
249 if (getOrientation(key) === getOrientation(nextItemArrowKeyCurrent) && getOrientation(key) !== undefined) {
250 event.preventDefault();
251 const actionStep = key === nextItemArrowKeyCurrent ? 1 : -1;
252
253 // stay within array indices
254 const nextAction = clamp(focusedAction.current + actionStep, 0, actions.current.length - 1);
255 actions.current[nextAction].focus();
256 focusedAction.current = nextAction;
257 nextItemArrowKey.current = nextItemArrowKeyCurrent;
258 }
259 };
260 React.useEffect(() => {
261 // actions were closed while navigation state was not reset
262 if (!open) {
263 focusedAction.current = 0;
264 nextItemArrowKey.current = undefined;
265 }
266 }, [open]);
267 const handleClose = event => {
268 if (event.type === 'mouseleave' && onMouseLeave) {
269 onMouseLeave(event);
270 }
271 if (event.type === 'blur' && onBlur) {
272 onBlur(event);
273 }
274 eventTimer.clear();
275 if (event.type === 'blur') {
276 eventTimer.start(0, () => {
277 setOpenState(false);
278 if (onClose) {
279 onClose(event, 'blur');
280 }
281 });
282 } else {
283 setOpenState(false);
284 if (onClose) {
285 onClose(event, 'mouseLeave');
286 }
287 }
288 };
289 const handleClick = event => {
290 if (FabProps.onClick) {
291 FabProps.onClick(event);
292 }
293 eventTimer.clear();
294 if (open) {
295 setOpenState(false);
296 if (onClose) {
297 onClose(event, 'toggle');
298 }
299 } else {
300 setOpenState(true);
301 if (onOpen) {
302 onOpen(event, 'toggle');
303 }
304 }
305 };
306 const handleOpen = event => {
307 if (event.type === 'mouseenter' && onMouseEnter) {
308 onMouseEnter(event);
309 }
310 if (event.type === 'focus' && onFocus) {
311 onFocus(event);
312 }
313
314 // When moving the focus between two items,
315 // a chain if blur and focus event is triggered.
316 // We only handle the last event.
317 eventTimer.clear();
318 if (!open) {
319 // Wait for a future focus or click event
320 eventTimer.start(0, () => {
321 setOpenState(true);
322 if (onOpen) {
323 const eventMap = {
324 focus: 'focus',
325 mouseenter: 'mouseEnter'
326 };
327 onOpen(event, eventMap[event.type]);
328 }
329 });
330 }
331 };
332
333 // Filter the label for valid id characters.
334 const id = ariaLabel.replace(/^[^a-z]+|[^\w:.-]+/gi, '');
335 const allItems = React.Children.toArray(childrenProp).filter(child => {
336 if (process.env.NODE_ENV !== 'production') {
337 if (isFragment(child)) {
338 console.error(["MUI: The SpeedDial component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
339 }
340 }
341 return /*#__PURE__*/React.isValidElement(child);
342 });
343 const children = allItems.map((child, index) => {
344 const {
345 FabProps: {
346 ref: origButtonRef,
347 ...ChildFabProps
348 } = {},
349 tooltipPlacement: tooltipPlacementProp
350 } = child.props;
351 const tooltipPlacement = tooltipPlacementProp || (getOrientation(direction) === 'vertical' ? 'left' : 'top');
352 return /*#__PURE__*/React.cloneElement(child, {
353 FabProps: {
354 ...ChildFabProps,
355 ref: createHandleSpeedDialActionButtonRef(index, origButtonRef)
356 },
357 delay: 30 * (open ? index : allItems.length - index),
358 open,
359 tooltipPlacement,
360 id: `${id}-action-${index}`
361 });
362 });
363 const backwardCompatibleSlots = {
364 transition: TransitionComponentProp,
365 ...slots
366 };
367 const backwardCompatibleSlotProps = {
368 transition: TransitionPropsProp,
369 ...slotProps
370 };
371 const externalForwardedProps = {
372 slots: backwardCompatibleSlots,
373 slotProps: backwardCompatibleSlotProps
374 };
375 const [TransitionSlot, transitionProps] = useSlot('transition', {
376 elementType: Zoom,
377 externalForwardedProps,
378 ownerState
379 });
380 return /*#__PURE__*/_jsxs(SpeedDialRoot, {
381 className: clsx(classes.root, className),
382 ref: ref,
383 role: "presentation",
384 onKeyDown: handleKeyDown,
385 onBlur: handleClose,
386 onFocus: handleOpen,
387 onMouseEnter: handleOpen,
388 onMouseLeave: handleClose,
389 ownerState: ownerState,
390 ...other,
391 children: [/*#__PURE__*/_jsx(TransitionSlot, {
392 in: !hidden,
393 timeout: transitionDuration,
394 unmountOnExit: true,
395 ...transitionProps,
396 children: /*#__PURE__*/_jsx(SpeedDialFab, {
397 color: "primary",
398 "aria-label": ariaLabel,
399 "aria-haspopup": "true",
400 "aria-expanded": open,
401 "aria-controls": `${id}-actions`,
402 ...FabProps,
403 onClick: handleClick,
404 className: clsx(classes.fab, FabProps.className),
405 ref: handleFabRef,
406 ownerState: ownerState,
407 children: /*#__PURE__*/React.isValidElement(icon) && isMuiElement(icon, ['SpeedDialIcon']) ? /*#__PURE__*/React.cloneElement(icon, {
408 open
409 }) : icon
410 })
411 }), /*#__PURE__*/_jsx(SpeedDialActions, {
412 id: `${id}-actions`,
413 role: "menu",
414 "aria-orientation": getOrientation(direction),
415 className: clsx(classes.actions, !open && classes.actionsClosed),
416 ownerState: ownerState,
417 children: children
418 })]
419 });
420});
421process.env.NODE_ENV !== "production" ? SpeedDial.propTypes /* remove-proptypes */ = {
422 // ┌────────────────────────────── Warning ──────────────────────────────┐
423 // │ These PropTypes are generated from the TypeScript type definitions. │
424 // │ To update them, edit the d.ts file and run `pnpm proptypes`. │
425 // └─────────────────────────────────────────────────────────────────────┘
426 /**
427 * The aria-label of the button element.
428 * Also used to provide the `id` for the `SpeedDial` element and its children.
429 */
430 ariaLabel: PropTypes.string.isRequired,
431 /**
432 * SpeedDialActions to display when the SpeedDial is `open`.
433 */
434 children: PropTypes.node,
435 /**
436 * Override or extend the styles applied to the component.
437 */
438 classes: PropTypes.object,
439 /**
440 * @ignore
441 */
442 className: PropTypes.string,
443 /**
444 * The direction the actions open relative to the floating action button.
445 * @default 'up'
446 */
447 direction: PropTypes.oneOf(['down', 'left', 'right', 'up']),
448 /**
449 * Props applied to the [`Fab`](https://mui.com/material-ui/api/fab/) element.
450 * @default {}
451 */
452 FabProps: PropTypes.object,
453 /**
454 * If `true`, the SpeedDial is hidden.
455 * @default false
456 */
457 hidden: PropTypes.bool,
458 /**
459 * The icon to display in the SpeedDial Fab. The `SpeedDialIcon` component
460 * provides a default Icon with animation.
461 */
462 icon: PropTypes.node,
463 /**
464 * @ignore
465 */
466 onBlur: PropTypes.func,
467 /**
468 * Callback fired when the component requests to be closed.
469 *
470 * @param {object} event The event source of the callback.
471 * @param {string} reason Can be: `"toggle"`, `"blur"`, `"mouseLeave"`, `"escapeKeyDown"`.
472 */
473 onClose: PropTypes.func,
474 /**
475 * @ignore
476 */
477 onFocus: PropTypes.func,
478 /**
479 * @ignore
480 */
481 onKeyDown: PropTypes.func,
482 /**
483 * @ignore
484 */
485 onMouseEnter: PropTypes.func,
486 /**
487 * @ignore
488 */
489 onMouseLeave: PropTypes.func,
490 /**
491 * Callback fired when the component requests to be open.
492 *
493 * @param {object} event The event source of the callback.
494 * @param {string} reason Can be: `"toggle"`, `"focus"`, `"mouseEnter"`.
495 */
496 onOpen: PropTypes.func,
497 /**
498 * If `true`, the component is shown.
499 */
500 open: PropTypes.bool,
501 /**
502 * The icon to display in the SpeedDial Fab when the SpeedDial is open.
503 */
504 openIcon: PropTypes.node,
505 /**
506 * The props used for each slot inside.
507 * @default {}
508 */
509 slotProps: PropTypes.shape({
510 transition: PropTypes.oneOfType([PropTypes.func, PropTypes.object])
511 }),
512 /**
513 * The components used for each slot inside.
514 * @default {}
515 */
516 slots: PropTypes.shape({
517 transition: PropTypes.elementType
518 }),
519 /**
520 * The system prop that allows defining system overrides as well as additional CSS styles.
521 */
522 sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object]),
523 /**
524 * The component used for the transition.
525 * [Follow this guide](https://mui.com/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.
526 * @default Zoom
527 */
528 TransitionComponent: PropTypes.elementType,
529 /**
530 * The duration for the transition, in milliseconds.
531 * You may specify a single timeout for all transitions, or individually with an object.
532 * @default {
533 * enter: theme.transitions.duration.enteringScreen,
534 * exit: theme.transitions.duration.leavingScreen,
535 * }
536 */
537 transitionDuration: PropTypes.oneOfType([PropTypes.number, PropTypes.shape({
538 appear: PropTypes.number,
539 enter: PropTypes.number,
540 exit: PropTypes.number
541 })]),
542 /**
543 * Props applied to the transition element.
544 * By default, the element is based on this [`Transition`](https://reactcommunity.org/react-transition-group/transition/) component.
545 */
546 TransitionProps: PropTypes.object
547} : void 0;
548export default SpeedDial;
\No newline at end of file