@atlaskit/modal-dialog 12.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +2111 -0
  2. package/LICENSE +13 -0
  3. package/README.md +13 -0
  4. package/__perf__/default.tsx +42 -0
  5. package/__perf__/interactions.tsx +136 -0
  6. package/__perf__/scroll.tsx +98 -0
  7. package/codemods/12.0.0-lite-mode.ts +51 -0
  8. package/codemods/__tests__/12.0.0-lite-mode.test.ts +493 -0
  9. package/codemods/__tests__/handle-prop-spread.tsx +276 -0
  10. package/codemods/__tests__/inline-WidthNames-declaration.test.ts +260 -0
  11. package/codemods/__tests__/map-actions-prop.tsx +436 -0
  12. package/codemods/__tests__/map-body-from-props.test.ts +645 -0
  13. package/codemods/__tests__/map-container-from-props.test.ts +323 -0
  14. package/codemods/__tests__/map-footer-from-props.test.ts +544 -0
  15. package/codemods/__tests__/map-header-from-props.test.ts +559 -0
  16. package/codemods/__tests__/map-heading-prop.tsx +438 -0
  17. package/codemods/__tests__/remove-appearance-prop.test.ts +79 -0
  18. package/codemods/__tests__/remove-component-override-props.test.ts +153 -0
  19. package/codemods/__tests__/remove-is-chromeless.tsx +182 -0
  20. package/codemods/__tests__/rename-appearance-type.test.ts +52 -0
  21. package/codemods/__tests__/rename-inner-component-prop-types.test.ts +82 -0
  22. package/codemods/__tests__/rename-scrollBehavior-to-shouldScrollInViewport.test.ts +237 -0
  23. package/codemods/internal/constants.tsx +41 -0
  24. package/codemods/internal/utils.tsx +223 -0
  25. package/codemods/migrations/handle-prop-spread.tsx +51 -0
  26. package/codemods/migrations/inline-WidthNames-declaration.ts +92 -0
  27. package/codemods/migrations/map-actions-prop.tsx +430 -0
  28. package/codemods/migrations/map-body-from-props.ts +147 -0
  29. package/codemods/migrations/map-container-from-props.ts +72 -0
  30. package/codemods/migrations/map-footer-from-props.ts +107 -0
  31. package/codemods/migrations/map-header-from-props.ts +101 -0
  32. package/codemods/migrations/map-heading-prop.tsx +193 -0
  33. package/codemods/migrations/remove-appearance-prop.ts +27 -0
  34. package/codemods/migrations/remove-component-override-props.ts +84 -0
  35. package/codemods/migrations/remove-is-chromeless.tsx +42 -0
  36. package/codemods/migrations/rename-appearance-type.ts +9 -0
  37. package/codemods/migrations/rename-inner-component-prop-types.ts +28 -0
  38. package/codemods/migrations/rename-scrollBehavior-to-shouldScrollInViewport.ts +82 -0
  39. package/dist/cjs/hooks.js +22 -0
  40. package/dist/cjs/index.js +63 -0
  41. package/dist/cjs/internal/components/modal-dialog.js +155 -0
  42. package/dist/cjs/internal/components/positioner.js +89 -0
  43. package/dist/cjs/internal/components/scroll-container.js +138 -0
  44. package/dist/cjs/internal/constants.js +48 -0
  45. package/dist/cjs/internal/context.js +13 -0
  46. package/dist/cjs/internal/hooks/use-modal-stack.js +110 -0
  47. package/dist/cjs/internal/hooks/use-on-motion-finish.js +24 -0
  48. package/dist/cjs/internal/hooks/use-prevent-programmatic-scroll.js +55 -0
  49. package/dist/cjs/internal/hooks/use-scroll.js +20 -0
  50. package/dist/cjs/internal/utils.js +35 -0
  51. package/dist/cjs/modal-body.js +66 -0
  52. package/dist/cjs/modal-footer.js +40 -0
  53. package/dist/cjs/modal-header.js +43 -0
  54. package/dist/cjs/modal-title.js +108 -0
  55. package/dist/cjs/modal-transition.js +21 -0
  56. package/dist/cjs/modal-wrapper.js +126 -0
  57. package/dist/cjs/types.js +5 -0
  58. package/dist/cjs/version.json +5 -0
  59. package/dist/es2019/hooks.js +11 -0
  60. package/dist/es2019/index.js +7 -0
  61. package/dist/es2019/internal/components/modal-dialog.js +120 -0
  62. package/dist/es2019/internal/components/positioner.js +78 -0
  63. package/dist/es2019/internal/components/scroll-container.js +97 -0
  64. package/dist/es2019/internal/constants.js +27 -0
  65. package/dist/es2019/internal/context.js +3 -0
  66. package/dist/es2019/internal/hooks/use-modal-stack.js +85 -0
  67. package/dist/es2019/internal/hooks/use-on-motion-finish.js +17 -0
  68. package/dist/es2019/internal/hooks/use-prevent-programmatic-scroll.js +39 -0
  69. package/dist/es2019/internal/hooks/use-scroll.js +11 -0
  70. package/dist/es2019/internal/utils.js +22 -0
  71. package/dist/es2019/modal-body.js +50 -0
  72. package/dist/es2019/modal-footer.js +30 -0
  73. package/dist/es2019/modal-header.js +30 -0
  74. package/dist/es2019/modal-title.js +94 -0
  75. package/dist/es2019/modal-transition.js +10 -0
  76. package/dist/es2019/modal-wrapper.js +88 -0
  77. package/dist/es2019/types.js +1 -0
  78. package/dist/es2019/version.json +5 -0
  79. package/dist/esm/hooks.js +11 -0
  80. package/dist/esm/index.js +7 -0
  81. package/dist/esm/internal/components/modal-dialog.js +131 -0
  82. package/dist/esm/internal/components/positioner.js +76 -0
  83. package/dist/esm/internal/components/scroll-container.js +114 -0
  84. package/dist/esm/internal/constants.js +27 -0
  85. package/dist/esm/internal/context.js +3 -0
  86. package/dist/esm/internal/hooks/use-modal-stack.js +96 -0
  87. package/dist/esm/internal/hooks/use-on-motion-finish.js +16 -0
  88. package/dist/esm/internal/hooks/use-prevent-programmatic-scroll.js +44 -0
  89. package/dist/esm/internal/hooks/use-scroll.js +11 -0
  90. package/dist/esm/internal/utils.js +22 -0
  91. package/dist/esm/modal-body.js +49 -0
  92. package/dist/esm/modal-footer.js +29 -0
  93. package/dist/esm/modal-header.js +29 -0
  94. package/dist/esm/modal-title.js +93 -0
  95. package/dist/esm/modal-transition.js +10 -0
  96. package/dist/esm/modal-wrapper.js +96 -0
  97. package/dist/esm/types.js +1 -0
  98. package/dist/esm/version.json +5 -0
  99. package/dist/types/hooks.d.ts +1 -0
  100. package/dist/types/index.d.ts +8 -0
  101. package/dist/types/internal/components/modal-dialog.d.ts +3 -0
  102. package/dist/types/internal/components/positioner.d.ts +10 -0
  103. package/dist/types/internal/components/scroll-container.d.ts +20 -0
  104. package/dist/types/internal/constants.d.ts +25 -0
  105. package/dist/types/internal/context.d.ts +20 -0
  106. package/dist/types/internal/hooks/use-modal-stack.d.ts +13 -0
  107. package/dist/types/internal/hooks/use-on-motion-finish.d.ts +4 -0
  108. package/dist/types/internal/hooks/use-prevent-programmatic-scroll.d.ts +7 -0
  109. package/dist/types/internal/hooks/use-scroll.d.ts +1 -0
  110. package/dist/types/internal/utils.d.ts +3 -0
  111. package/dist/types/modal-body.d.ts +16 -0
  112. package/dist/types/modal-footer.d.ts +16 -0
  113. package/dist/types/modal-header.d.ts +16 -0
  114. package/dist/types/modal-title.d.ts +26 -0
  115. package/dist/types/modal-transition.d.ts +3 -0
  116. package/dist/types/modal-wrapper.d.ts +5 -0
  117. package/dist/types/types.d.ts +90 -0
  118. package/extract-react-types/modal-attributes.tsx +5 -0
  119. package/hooks/package.json +7 -0
  120. package/modal-body/package.json +7 -0
  121. package/modal-dialog/package.json +7 -0
  122. package/modal-footer/package.json +7 -0
  123. package/modal-header/package.json +7 -0
  124. package/modal-title/package.json +7 -0
  125. package/modal-transition/package.json +7 -0
  126. package/package.json +113 -0
  127. package/types/package.json +7 -0
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "@atlaskit/modal-dialog",
3
+ "version": "12.0.2",
4
+ "sideEffects": false
5
+ }
@@ -0,0 +1,11 @@
1
+ import { useContext } from 'react';
2
+ import { ModalContext } from './internal/context';
3
+ export const useModal = () => {
4
+ const modalContext = useContext(ModalContext);
5
+
6
+ if (modalContext == null) {
7
+ throw Error('@atlaskit/modal-dialog: Modal context unavailable – this component needs to be a child of ModalDialog.');
8
+ }
9
+
10
+ return modalContext;
11
+ };
@@ -0,0 +1,7 @@
1
+ export { default } from './modal-wrapper';
2
+ export { default as ModalTransition } from './modal-transition';
3
+ export { default as ModalHeader } from './modal-header';
4
+ export { default as ModalTitle } from './modal-title';
5
+ export { default as ModalBody } from './modal-body';
6
+ export { default as ModalFooter } from './modal-footer';
7
+ export { useModal } from './hooks';
@@ -0,0 +1,120 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
2
+
3
+ /** @jsx jsx */
4
+ import { useMemo } from 'react';
5
+ import { css, jsx } from '@emotion/core';
6
+ import { useUID } from 'react-uid';
7
+ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
8
+ import useAutoFocus from '@atlaskit/ds-lib/use-auto-focus';
9
+ import FadeIn from '@atlaskit/motion/fade-in';
10
+ import { N0, N30A, N60A } from '@atlaskit/theme/colors';
11
+ import { borderRadius, textColor } from '../constants';
12
+ import { ModalContext, ScrollContext } from '../context';
13
+ import useOnMotionFinish from '../hooks/use-on-motion-finish';
14
+ import { dialogHeight, dialogWidth } from '../utils';
15
+ import Positioner from './positioner';
16
+ const dialogStyles = css({
17
+ display: 'flex',
18
+ width: '100%',
19
+ maxWidth: '100vw',
20
+ height: '100%',
21
+ minHeight: 0,
22
+ maxHeight: '100vh',
23
+ flex: '1 1 auto',
24
+ flexDirection: 'column',
25
+ backgroundColor: N0,
26
+ color: textColor,
27
+ pointerEvents: 'auto',
28
+ '@media (min-width: 480px)': {
29
+ width: 'var(--modal-dialog-width)',
30
+ maxWidth: 'inherit',
31
+ marginRight: 'inherit',
32
+ marginLeft: 'inherit',
33
+ borderRadius,
34
+ boxShadow: `0 0 0 1px ${N30A}, 0 2px 1px ${N30A}, 0 0 20px -6px ${N60A}`
35
+ },
36
+
37
+ /**
38
+ * This is to support scrolling if the modal's children are wrapped in
39
+ * a form.
40
+ */
41
+ // eslint-disable-next-line @repo/internal/styles/no-nested-styles
42
+ '& > form:only-child': {
43
+ display: 'inherit',
44
+ maxHeight: 'inherit',
45
+ flexDirection: 'inherit'
46
+ }
47
+ });
48
+ const viewportScrollStyles = css({
49
+ /**
50
+ * This ensures that the element fills the viewport on mobile
51
+ * while also allowing it to overflow if its height is larger than
52
+ * the viewport.
53
+ */
54
+ minHeight: '100vh',
55
+ maxHeight: 'none',
56
+ '@media (min-width: 480px)': {
57
+ minHeight: 'var(--modal-dialog-height)'
58
+ }
59
+ });
60
+ const bodyScrollStyles = css({
61
+ '@media (min-width: 480px)': {
62
+ height: 'var(--modal-dialog-height)',
63
+ maxHeight: 'inherit'
64
+ }
65
+ });
66
+
67
+ const ModalDialog = props => {
68
+ const {
69
+ width = 'medium',
70
+ shouldScrollInViewport = false,
71
+ autoFocus,
72
+ stackIndex,
73
+ onClose,
74
+ onCloseComplete,
75
+ onOpenComplete,
76
+ height,
77
+ children,
78
+ testId
79
+ } = props;
80
+ const id = useUID();
81
+ const titleId = `modal-dialog-title-${id}`;
82
+ useAutoFocus(typeof autoFocus === 'object' ? autoFocus : undefined, // When a user supplies a ref to focus we enable this hook
83
+ typeof autoFocus === 'object');
84
+ const [motionRef, onMotionFinish] = useOnMotionFinish({
85
+ onOpenComplete,
86
+ onCloseComplete
87
+ });
88
+ const modalDialogContext = useMemo(() => ({
89
+ testId,
90
+ titleId,
91
+ onClose
92
+ }), [testId, titleId, onClose]);
93
+ return jsx(Positioner, {
94
+ stackIndex: stackIndex,
95
+ shouldScrollInViewport: shouldScrollInViewport,
96
+ testId: testId
97
+ }, jsx(ModalContext.Provider, {
98
+ value: modalDialogContext
99
+ }, jsx(ScrollContext.Provider, {
100
+ value: shouldScrollInViewport
101
+ }, jsx(FadeIn, {
102
+ entranceDirection: "bottom",
103
+ onFinish: onMotionFinish
104
+ }, bottomFadeInProps => jsx("section", _extends({}, bottomFadeInProps, {
105
+ ref: mergeRefs([bottomFadeInProps.ref, motionRef]),
106
+ style: {
107
+ '--modal-dialog-width': dialogWidth(width),
108
+ '--modal-dialog-height': dialogHeight(height)
109
+ },
110
+ css: [dialogStyles, shouldScrollInViewport ? viewportScrollStyles : bodyScrollStyles],
111
+ role: "dialog",
112
+ "aria-labelledby": titleId,
113
+ "data-testid": testId,
114
+ "data-modal-stack": stackIndex,
115
+ tabIndex: -1,
116
+ "aria-modal": true
117
+ }), children)))));
118
+ };
119
+
120
+ export default ModalDialog;
@@ -0,0 +1,78 @@
1
+ /** @jsx jsx */
2
+ import { css, jsx } from '@emotion/core';
3
+ import { easeInOut } from '@atlaskit/motion/curves';
4
+ import { mediumDurationMs } from '@atlaskit/motion/durations';
5
+ import { layers } from '@atlaskit/theme/constants';
6
+ import { gutter, verticalOffset } from '../constants';
7
+ const maxWidthDimensions = `calc(100vw - ${gutter * 2}px)`;
8
+ const maxHeightDimensions = `calc(100vh - ${gutter * 2 - 1}px)`;
9
+ const positionerStyles = css({
10
+ width: '100%',
11
+ maxWidth: '100%',
12
+ height: '100%',
13
+ position: 'fixed',
14
+ zIndex: layers.modal(),
15
+ top: 0,
16
+ left: 0
17
+ });
18
+ const viewportScrollStyles = css({
19
+ width: 'max-content',
20
+ height: 'auto',
21
+ position: 'relative',
22
+ '@media (min-width: 480px)': {
23
+ margin: `${gutter}px auto`
24
+ }
25
+ });
26
+ const bodyScrollStyles = css({
27
+ '@media (min-width: 480px)': {
28
+ width: 'max-content',
29
+ maxWidth: maxWidthDimensions,
30
+ maxHeight: maxHeightDimensions,
31
+ marginRight: 'auto',
32
+ marginLeft: 'auto',
33
+ position: 'absolute',
34
+ top: `${gutter}px`,
35
+ right: 0,
36
+ left: 0,
37
+ pointerEvents: 'none'
38
+ }
39
+ });
40
+ const stackTransitionStyles = css({
41
+ transitionDuration: `${mediumDurationMs}ms`,
42
+ transitionProperty: 'transform',
43
+ transitionTimingFunction: easeInOut,
44
+
45
+ /** Duplicated from @atlaskit/motion/accessibility
46
+ * because @repo/internal/styles/consistent-style-ordering
47
+ * doesn't work well with object spreading. */
48
+ '@media (prefers-reduced-motion: reduce)': {
49
+ animation: 'none',
50
+ transition: 'none'
51
+ }
52
+ });
53
+ const stackTransformStyles = css({
54
+ transform: 'translateY(var(--modal-dialog-translate-y))'
55
+ });
56
+ const stackIdleStyles = css({
57
+ transform: 'none'
58
+ });
59
+
60
+ const Positioner = props => {
61
+ const {
62
+ children,
63
+ stackIndex,
64
+ shouldScrollInViewport,
65
+ testId
66
+ } = props;
67
+ return jsx("div", {
68
+ style: {
69
+ '--modal-dialog-translate-y': `${stackIndex * (verticalOffset / 2)}px`
70
+ },
71
+ css: [positionerStyles, stackTransitionStyles,
72
+ /* We only want to apply transform on modals shifting to the back of the stack. */
73
+ stackIndex > 0 ? stackTransformStyles : stackIdleStyles, shouldScrollInViewport ? viewportScrollStyles : bodyScrollStyles],
74
+ "data-testid": testId && `${testId}--positioner`
75
+ }, children);
76
+ };
77
+
78
+ export default Positioner;
@@ -0,0 +1,97 @@
1
+ /** @jsx jsx */
2
+ import React, { forwardRef, useEffect, useRef, useState } from 'react';
3
+ import { css, jsx } from '@emotion/core';
4
+ import rafSchedule from 'raf-schd';
5
+ import mergeRefs from '@atlaskit/ds-lib/merge-refs';
6
+ import useLazyCallback from '@atlaskit/ds-lib/use-lazy-callback';
7
+ import useStateRef from '@atlaskit/ds-lib/use-state-ref';
8
+ import FocusRing from '@atlaskit/focus-ring';
9
+ import { keylineColor, keylineHeight } from '../constants';
10
+ const baseStyles = css({
11
+ /**
12
+ * We need to inherit flex styles from its parent here
13
+ * in case they're set because we're essentially being a proxy container
14
+ * between the original flex parent and its children (the modal body).
15
+ */
16
+ display: 'inherit',
17
+ margin: 0,
18
+ flex: 'inherit',
19
+ flexDirection: 'inherit',
20
+ overflowX: 'hidden',
21
+ overflowY: 'auto',
22
+ '@media (min-width: 480px)': {
23
+ height: 'unset',
24
+ overflowY: 'auto'
25
+ }
26
+ });
27
+ const topKeylineStyles = css({
28
+ borderTop: `${keylineHeight}px solid ${keylineColor}`
29
+ });
30
+ const bottomKeylineStyles = css({
31
+ borderBottom: `${keylineHeight}px solid ${keylineColor}`
32
+ });
33
+
34
+ /**
35
+ * A container that shows top and bottom keylines when the
36
+ * content overflows into the scrollable element.
37
+ */
38
+ const ScrollContainer = /*#__PURE__*/forwardRef((props, ref) => {
39
+ const {
40
+ testId,
41
+ children
42
+ } = props;
43
+ const [hasSiblings, setSiblings] = useStateRef({
44
+ previous: false,
45
+ next: false
46
+ });
47
+ const [showContentFocus, setContentFocus] = useState(false);
48
+ const [showTopKeyline, setTopKeyline] = useState(false);
49
+ const [showBottomKeyline, setBottomKeyline] = useState(false);
50
+ const scrollableRef = useRef(null);
51
+ const setLazySiblings = useLazyCallback(setSiblings);
52
+ const setLazyContentFocus = useLazyCallback(rafSchedule(() => {
53
+ const target = scrollableRef.current;
54
+ target && setContentFocus(target.scrollHeight > target.clientHeight);
55
+ }));
56
+ const setLazyKeylines = useLazyCallback(rafSchedule(() => {
57
+ const target = scrollableRef.current;
58
+
59
+ if (target) {
60
+ const scrollableDistance = target.scrollHeight - target.clientHeight;
61
+
62
+ if (hasSiblings.current.previous) {
63
+ setTopKeyline(target.scrollTop > keylineHeight);
64
+ }
65
+
66
+ if (hasSiblings.current.next) {
67
+ setBottomKeyline(target.scrollTop <= scrollableDistance - keylineHeight);
68
+ }
69
+ }
70
+ }));
71
+ useEffect(() => {
72
+ const target = scrollableRef.current;
73
+ window.addEventListener('resize', setLazyKeylines, false);
74
+ target === null || target === void 0 ? void 0 : target.addEventListener('scroll', setLazyKeylines, false);
75
+ setLazyContentFocus();
76
+ setLazyKeylines();
77
+ setLazySiblings({
78
+ previous: Boolean(target === null || target === void 0 ? void 0 : target.previousElementSibling),
79
+ next: Boolean(target === null || target === void 0 ? void 0 : target.nextElementSibling)
80
+ });
81
+ return () => {
82
+ window.removeEventListener('resize', setLazyKeylines, false);
83
+ target === null || target === void 0 ? void 0 : target.removeEventListener('scroll', setLazyKeylines, false);
84
+ };
85
+ }, [setLazyContentFocus, setLazyKeylines, setLazySiblings]);
86
+ return jsx(FocusRing, {
87
+ isInset: true
88
+ }, jsx("div", {
89
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
90
+ tabIndex: showContentFocus ? 0 : undefined,
91
+ "data-testid": testId && `${testId}--scrollable`,
92
+ ref: mergeRefs([ref, scrollableRef]),
93
+ css: [baseStyles, showTopKeyline && topKeylineStyles, showBottomKeyline && bottomKeylineStyles]
94
+ }, children));
95
+ });
96
+ ScrollContainer.displayName = 'ScrollContainer';
97
+ export default ScrollContainer;
@@ -0,0 +1,27 @@
1
+ import { text as getTextColor, N30, N800, R400, Y400 } from '@atlaskit/theme/colors';
2
+ import { borderRadius as getBorderRadius, gridSize as getGridSize } from '@atlaskit/theme/constants';
3
+ export const width = {
4
+ values: ['small', 'medium', 'large', 'x-large'],
5
+ widths: {
6
+ small: 400,
7
+ medium: 600,
8
+ large: 800,
9
+ 'x-large': 968
10
+ },
11
+ defaultValue: 'medium'
12
+ };
13
+ export const gutter = 60;
14
+ const gridSize = getGridSize();
15
+ export const borderRadius = getBorderRadius();
16
+ export const verticalOffset = gridSize * 2;
17
+ export const padding = gridSize * 3;
18
+ export const footerItemGap = gridSize;
19
+ export const titleIconMargin = gridSize;
20
+ export const keylineHeight = 2;
21
+ export const keylineColor = N30;
22
+ export const textColor = getTextColor();
23
+ export const focusOutlineColor = N800;
24
+ export const iconColor = {
25
+ danger: R400,
26
+ warning: Y400
27
+ };
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'react';
2
+ export const ModalContext = /*#__PURE__*/createContext(null);
3
+ export const ScrollContext = /*#__PURE__*/createContext(null);
@@ -0,0 +1,85 @@
1
+ import { useEffect } from 'react';
2
+ import useLazyCallback from '@atlaskit/ds-lib/use-lazy-callback';
3
+ import usePreviousValue from '@atlaskit/ds-lib/use-previous-value';
4
+ import useStateRef from '@atlaskit/ds-lib/use-state-ref';
5
+ import { useExitingPersistence } from '@atlaskit/motion/exiting-persistence';
6
+ /**
7
+ * ________________________________________________
8
+ * | MAJOR VERSIONS WILL NOT KNOW ABOUT EACH OTHER! |
9
+ * ------------------------------------------------
10
+ *
11
+ * An array which holds references to all currently open modal dialogs.
12
+ * This will only work for modal dialogs of the same major version,
13
+ * as the reference will be different between them.
14
+ *
15
+ * E.g. V11 won't know about any from V12.
16
+ */
17
+
18
+ const modalStackRegister = [];
19
+
20
+ /**
21
+ * Returns the position of the calling modal dialog in the modal dialog stack.
22
+ * Stack index of `0` is the highest position in the stack,
23
+ * with every higher number being behind in the stack.
24
+ */
25
+ export default function useModalStack({
26
+ onStackChange
27
+ }) {
28
+ const {
29
+ isExiting
30
+ } = useExitingPersistence();
31
+ const [stackIndexRef, setStackIndex] = useStateRef(0);
32
+ const currentStackIndex = stackIndexRef.current;
33
+ const previousStackIndex = usePreviousValue(stackIndexRef.current); // We want to ensure this function **never changes** during the lifecycle of this component.
34
+ // This is why it's assigned to a ref and not in a useMemo/useCallback.
35
+
36
+ const updateStack = useLazyCallback(() => {
37
+ const newStackIndex = modalStackRegister.indexOf(updateStack); // We access the stack index ref instead of state because this closure will always only
38
+ // have the initial state and not any of the later values.
39
+
40
+ if (stackIndexRef.current !== newStackIndex) {
41
+ setStackIndex(newStackIndex);
42
+ stackIndexRef.current = newStackIndex;
43
+ }
44
+ });
45
+ useEffect(() => {
46
+ const currentStackIndex = modalStackRegister.indexOf(updateStack);
47
+
48
+ if (!isExiting && currentStackIndex === -1) {
49
+ // We are opening the modal dialog.
50
+ // Add ourselves to the modal stack register!
51
+ modalStackRegister.unshift(updateStack);
52
+ }
53
+
54
+ if (isExiting && currentStackIndex !== -1) {
55
+ // We are closing the modal dialog using a wrapping modal transition component.
56
+ // Remove ourselves from the modal stack register!
57
+ // NOTE: Modal dialogs that don't have a wrapping modal transition component won't flow through here!
58
+ // For that scenario we cleanup when the component unmounts.
59
+ modalStackRegister.splice(currentStackIndex, 1);
60
+ } // Fire all registered modal dialogs to update their position in the stack.
61
+
62
+
63
+ modalStackRegister.forEach(cb => cb());
64
+ }, [updateStack, isExiting]);
65
+ useEffect(() => () => {
66
+ // Final cleanup just in case this modal dialog did not have a wrapping modal transition.
67
+ const currentStackIndex = modalStackRegister.indexOf(updateStack);
68
+
69
+ if (currentStackIndex !== -1) {
70
+ modalStackRegister.splice(currentStackIndex, 1);
71
+ modalStackRegister.forEach(cb => cb());
72
+ }
73
+ }, [updateStack]);
74
+ useEffect(() => {
75
+ if (previousStackIndex === undefined) {
76
+ // Initial case that we don't need to notify about.
77
+ return;
78
+ }
79
+
80
+ if (previousStackIndex !== currentStackIndex) {
81
+ onStackChange(currentStackIndex);
82
+ }
83
+ }, [onStackChange, previousStackIndex, currentStackIndex]);
84
+ return currentStackIndex;
85
+ }