1 | import _extends from "@babel/runtime/helpers/esm/extends";
|
2 | import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
|
3 | import * as React from 'react';
|
4 | import { isFragment } from 'react-is';
|
5 | import PropTypes from 'prop-types';
|
6 | import clsx from 'clsx';
|
7 | import { duration, withStyles } from '@material-ui/core/styles';
|
8 | import Zoom from '@material-ui/core/Zoom';
|
9 | import Fab from '@material-ui/core/Fab';
|
10 | import { capitalize, isMuiElement, useForkRef } from '@material-ui/core/utils';
|
11 |
|
12 | function 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 |
|
24 | function 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 |
|
36 | const dialRadius = 32;
|
37 | const spacingActions = 16;
|
38 | export const styles = theme => ({
|
39 |
|
40 | root: {
|
41 | zIndex: theme.zIndex.speedDial,
|
42 | display: 'flex',
|
43 | alignItems: 'center',
|
44 | pointerEvents: 'none'
|
45 | },
|
46 |
|
47 |
|
48 | fab: {
|
49 | pointerEvents: 'auto'
|
50 | },
|
51 |
|
52 |
|
53 | directionUp: {
|
54 | flexDirection: 'column-reverse',
|
55 | '& $actions': {
|
56 | flexDirection: 'column-reverse',
|
57 | marginBottom: -dialRadius,
|
58 | paddingBottom: spacingActions + dialRadius
|
59 | }
|
60 | },
|
61 |
|
62 |
|
63 | directionDown: {
|
64 | flexDirection: 'column',
|
65 | '& $actions': {
|
66 | flexDirection: 'column',
|
67 | marginTop: -dialRadius,
|
68 | paddingTop: spacingActions + dialRadius
|
69 | }
|
70 | },
|
71 |
|
72 |
|
73 | directionLeft: {
|
74 | flexDirection: 'row-reverse',
|
75 | '& $actions': {
|
76 | flexDirection: 'row-reverse',
|
77 | marginRight: -dialRadius,
|
78 | paddingRight: spacingActions + dialRadius
|
79 | }
|
80 | },
|
81 |
|
82 |
|
83 | directionRight: {
|
84 | flexDirection: 'row',
|
85 | '& $actions': {
|
86 | flexDirection: 'row',
|
87 | marginLeft: -dialRadius,
|
88 | paddingLeft: spacingActions + dialRadius
|
89 | }
|
90 | },
|
91 |
|
92 |
|
93 | actions: {
|
94 | display: 'flex',
|
95 | pointerEvents: 'auto'
|
96 | },
|
97 |
|
98 |
|
99 | actionsClosed: {
|
100 | transition: 'top 0s linear 0.2s',
|
101 | pointerEvents: 'none'
|
102 | }
|
103 | });
|
104 | const SpeedDial = 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 |
|
142 |
|
143 |
|
144 | const focusedAction = React.useRef(0);
|
145 | |
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 |
|
152 |
|
153 | const nextItemArrowKey = React.useRef();
|
154 | |
155 |
|
156 |
|
157 |
|
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 |
|
168 |
|
169 |
|
170 |
|
171 |
|
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;
|
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 |
|
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 | }
|
269 |
|
270 |
|
271 |
|
272 |
|
273 | clearTimeout(eventTimer.current);
|
274 |
|
275 | if (onOpen && !open) {
|
276 | event.persist();
|
277 |
|
278 | eventTimer.current = setTimeout(() => {
|
279 | const eventMap = {
|
280 | focus: 'focus',
|
281 | mouseenter: 'mouseEnter'
|
282 | };
|
283 | onOpen(event, eventMap[event.type]);
|
284 | });
|
285 | }
|
286 | };
|
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 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 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 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), React.createElement(TransitionComponent, _extends({
|
326 | in: !hidden,
|
327 | timeout: transitionDuration,
|
328 | unmountOnExit: true
|
329 | }, TransitionProps), 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 | }), React.isValidElement(icon) && isMuiElement(icon, ['SpeedDialIcon']) ? React.cloneElement(icon, {
|
340 | open
|
341 | }) : icon)), 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 | });
|
348 | process.env.NODE_ENV !== "production" ? SpeedDial.propTypes = {
|
349 |
|
350 |
|
351 |
|
352 |
|
353 |
|
354 | |
355 |
|
356 |
|
357 |
|
358 | ariaLabel: PropTypes.string.isRequired,
|
359 |
|
360 | |
361 |
|
362 |
|
363 | children: PropTypes.node,
|
364 |
|
365 | |
366 |
|
367 |
|
368 |
|
369 | classes: PropTypes.object,
|
370 |
|
371 | |
372 |
|
373 |
|
374 | className: PropTypes.string,
|
375 |
|
376 | |
377 |
|
378 |
|
379 | direction: PropTypes.oneOf(['down', 'left', 'right', 'up']),
|
380 |
|
381 | |
382 |
|
383 |
|
384 | FabProps: PropTypes.object,
|
385 |
|
386 | |
387 |
|
388 |
|
389 | hidden: PropTypes.bool,
|
390 |
|
391 | |
392 |
|
393 |
|
394 |
|
395 | icon: PropTypes.node,
|
396 |
|
397 | |
398 |
|
399 |
|
400 | onBlur: PropTypes.func,
|
401 |
|
402 | |
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 | onClose: PropTypes.func,
|
409 |
|
410 | |
411 |
|
412 |
|
413 | onFocus: PropTypes.func,
|
414 |
|
415 | |
416 |
|
417 |
|
418 | onKeyDown: PropTypes.func,
|
419 |
|
420 | |
421 |
|
422 |
|
423 | onMouseEnter: PropTypes.func,
|
424 |
|
425 | |
426 |
|
427 |
|
428 | onMouseLeave: PropTypes.func,
|
429 |
|
430 | |
431 |
|
432 |
|
433 |
|
434 |
|
435 |
|
436 | onOpen: PropTypes.func,
|
437 |
|
438 | |
439 |
|
440 |
|
441 | open: PropTypes.bool.isRequired,
|
442 |
|
443 | |
444 |
|
445 |
|
446 | openIcon: PropTypes.node,
|
447 |
|
448 | |
449 |
|
450 |
|
451 |
|
452 | TransitionComponent: PropTypes.elementType,
|
453 |
|
454 | |
455 |
|
456 |
|
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 |
|
466 |
|
467 | TransitionProps: PropTypes.object
|
468 | } : void 0;
|
469 | export default withStyles(styles, {
|
470 | name: 'MuiSpeedDial'
|
471 | })(SpeedDial); |
\ | No newline at end of file |