UNPKG

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