@teamturing/react-kit 2.12.0 → 2.13.0

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.
@@ -6,19 +6,13 @@ import '../../packages/token-studio/esm/token/typography/index.js';
6
6
  import styled from 'styled-components';
7
7
  import IconButton from '../IconButton/index.js';
8
8
  import View from '../View/index.js';
9
+ import useFocusTrap from '../../hook/useFocusTrap.js';
9
10
  import { sx } from '../../utils/styled-system/index.js';
10
11
  import MotionView from '../MotionView/index.js';
11
12
  import { j as jsxRuntimeExports } from '../../node_modules/react/jsx-runtime.js';
12
13
  import { AnimatePresence } from '../../node_modules/framer-motion/dist/es/components/AnimatePresence/index.js';
13
14
  import { cubicBezier } from '../../node_modules/framer-motion/dist/es/easing/cubic-bezier.js';
14
15
 
15
- function visible(el) {
16
- return !el.hidden && (!el.type || el.type !== 'hidden') && (el.offsetWidth > 0 || el.offsetHeight > 0);
17
- }
18
- function focusable(el) {
19
- const inputEl = el;
20
- return inputEl.tabIndex >= 0 && !inputEl.disabled && visible(inputEl);
21
- }
22
16
  const Dialog = ({
23
17
  children,
24
18
  isOpen,
@@ -38,41 +32,19 @@ const Dialog = ({
38
32
  handleDismiss?.();
39
33
  }
40
34
  }, [handleDismiss, dialogRef, overlayRef]);
41
- const getFocusableItem = useCallback((e, movement) => {
42
- if (dialogRef.current) {
43
- const items = Array.from(dialogRef.current.querySelectorAll('*')).filter(focusable);
44
- if (items.length === 0) return;
45
- e.preventDefault();
46
- const focusedElement = document.activeElement;
47
- if (!focusedElement) {
48
- return;
49
- }
50
- const index = items.indexOf(focusedElement);
51
- const offsetIndex = index + movement;
52
- const fallbackIndex = movement === 1 ? 0 : items.length - 1;
53
- const focusableItem = items[offsetIndex] || items[fallbackIndex];
54
- return focusableItem;
55
- }
56
- }, [dialogRef]);
57
- const handleTab = useCallback(e => {
58
- const movement = e.shiftKey ? -1 : 1;
59
- const focusableItem = getFocusableItem(e, movement);
60
- if (!focusableItem) {
61
- return;
62
- }
63
- focusableItem.focus();
64
- }, [getFocusableItem]);
65
35
  const handleKeyDown = useCallback(event => {
66
36
  switch (event.key) {
67
- case 'Tab':
68
- handleTab(event);
69
- break;
70
37
  case 'Escape':
71
38
  handleDismiss?.();
72
39
  event.stopPropagation();
73
40
  break;
74
41
  }
75
42
  }, [handleDismiss]);
43
+ useFocusTrap({
44
+ containerRef: dialogRef,
45
+ initialFocusRef: closeButtonRef,
46
+ disabled: !isOpen
47
+ });
76
48
  useEffect(() => {
77
49
  if (isOpen) {
78
50
  document.addEventListener('click', handleOutsideClick);
@@ -118,7 +90,7 @@ const Dialog = ({
118
90
  height: '100%',
119
91
  zIndex: 9999
120
92
  },
121
- children: [/*#__PURE__*/jsxRuntimeExports.jsx(Overlay, {
93
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx(Blanket, {
122
94
  ref: overlayRef
123
95
  }), /*#__PURE__*/jsxRuntimeExports.jsxs(BaseDialog, {
124
96
  ref: dialogRef,
@@ -162,7 +134,7 @@ const Dialog = ({
162
134
  }) : null
163
135
  });
164
136
  };
165
- const Overlay = styled.span`
137
+ const Blanket = styled.span`
166
138
  &:before {
167
139
  position: fixed;
168
140
  top: 0;
@@ -0,0 +1,92 @@
1
+ import { forwardRef, useRef, useImperativeHandle, useCallback, useEffect } from 'react';
2
+ import styled from 'styled-components';
3
+ import '../../node_modules/styled-system/dist/index.esm.js';
4
+ import { forcePixelValue } from '../../utils/forcePixelValue.js';
5
+ import { sx } from '../../utils/styled-system/index.js';
6
+ import { j as jsxRuntimeExports } from '../../node_modules/react/jsx-runtime.js';
7
+ import { variant } from '../../node_modules/@styled-system/variant/dist/index.esm.js';
8
+ import { iterateFocusableElements } from '../../node_modules/@primer/behaviors/dist/esm/utils/iterate-focusable-elements.js';
9
+
10
+ const Overlay = ({
11
+ children,
12
+ isOpen,
13
+ onDismiss,
14
+ size = 'm',
15
+ ignoreOutsideClickRefs = [],
16
+ ...props
17
+ }, ref) => {
18
+ const overlayRef = useRef(null);
19
+ useImperativeHandle(ref, () => overlayRef.current);
20
+ const handleDismiss = useCallback(() => onDismiss?.(), [onDismiss]);
21
+ const handleOutsideClick = useCallback(e => {
22
+ if (overlayRef.current && e.target instanceof Node && !overlayRef.current.contains(e.target) && ignoreOutsideClickRefs && !ignoreOutsideClickRefs.some(({
23
+ current
24
+ }) => current?.contains(e.target))) {
25
+ handleDismiss?.();
26
+ }
27
+ }, [handleDismiss, overlayRef]);
28
+ const handleKeyDown = useCallback(event => {
29
+ switch (event.key) {
30
+ case 'Escape':
31
+ handleDismiss?.();
32
+ event.stopPropagation();
33
+ break;
34
+ }
35
+ }, [handleDismiss]);
36
+ useEffect(() => {
37
+ if (overlayRef.current) {
38
+ const firstItem = iterateFocusableElements(overlayRef.current).next().value;
39
+ firstItem?.focus();
40
+ }
41
+ }, [isOpen]);
42
+ useEffect(() => {
43
+ if (isOpen) {
44
+ document.addEventListener('keydown', handleKeyDown);
45
+ return () => {
46
+ document.removeEventListener('keydown', handleKeyDown);
47
+ };
48
+ }
49
+ }, [isOpen, handleKeyDown]);
50
+ useEffect(() => {
51
+ if (isOpen) {
52
+ document.addEventListener('click', handleOutsideClick);
53
+ return () => {
54
+ document.removeEventListener('click', handleOutsideClick);
55
+ };
56
+ }
57
+ }, [isOpen, handleOutsideClick]);
58
+ return isOpen ? /*#__PURE__*/jsxRuntimeExports.jsx(BaseOverlay, {
59
+ ref: overlayRef,
60
+ size: size,
61
+ ...props,
62
+ children: children
63
+ }) : null;
64
+ };
65
+ const BaseOverlay = styled.div`
66
+ position: absolute;
67
+ box-shadow: ${({
68
+ theme
69
+ }) => theme.shadows['shadow/overlay']};
70
+ background-color: ${({
71
+ theme
72
+ }) => theme.colors['surface/overlay']};
73
+ border-radius: ${({
74
+ theme
75
+ }) => forcePixelValue(theme.radii.s)};
76
+ overflow: hidden;
77
+ margin: auto;
78
+ z-index: 99999;
79
+
80
+ ${variant({
81
+ prop: 'size',
82
+ variants: {
83
+ m: {
84
+ width: forcePixelValue(180)
85
+ }
86
+ }
87
+ })}
88
+ ${sx}
89
+ `;
90
+ var Overlay$1 = /*#__PURE__*/forwardRef(Overlay);
91
+
92
+ export { Overlay$1 as default };
@@ -0,0 +1,69 @@
1
+ import { useFloating } from '../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.js';
2
+ import space from '../../packages/token-studio/esm/token/space/index.js';
3
+ import '../../packages/token-studio/esm/token/typography/index.js';
4
+ import { Children, cloneElement } from 'react';
5
+ import useFocusTrap from '../../hook/useFocusTrap.js';
6
+ import useFocusZone from '../../hook/useFocusZone.js';
7
+ import useToggleHandler from '../../hook/useToggleHandler.js';
8
+ import Overlay from '../Overlay/index.js';
9
+ import { j as jsxRuntimeExports } from '../../node_modules/react/jsx-runtime.js';
10
+ import { autoUpdate } from '../../node_modules/@floating-ui/dom/dist/floating-ui.dom.js';
11
+ import { offset, flip, shift } from '../../node_modules/@floating-ui/core/dist/floating-ui.core.js';
12
+
13
+ const OverlayPopper = ({
14
+ children: propChildren,
15
+ renderOverlay,
16
+ placement = 'bottom-start',
17
+ focusZoneSettings,
18
+ focusTrapSettings
19
+ }) => {
20
+ const {
21
+ refs,
22
+ floatingStyles,
23
+ isPositioned
24
+ } = useFloating({
25
+ placement,
26
+ whileElementsMounted: autoUpdate,
27
+ middleware: [offset(space[1]), flip(), shift()],
28
+ strategy: 'fixed'
29
+ });
30
+ const {
31
+ state: isOpen,
32
+ toggle: toggleOverlay,
33
+ off: closeOverlay
34
+ } = useToggleHandler({
35
+ initialState: false
36
+ });
37
+ const children = Children.map(propChildren, child => /*#__PURE__*/cloneElement(child, {
38
+ onClick: toggleOverlay,
39
+ tabIndex: 0,
40
+ ...{
41
+ ref: refs.setReference
42
+ }
43
+ }));
44
+ useFocusZone({
45
+ containerRef: refs.floating,
46
+ disabled: !isOpen || !isPositioned,
47
+ ...focusZoneSettings
48
+ });
49
+ useFocusTrap({
50
+ containerRef: refs.floating,
51
+ disabled: !isOpen || !isPositioned,
52
+ ...focusTrapSettings
53
+ });
54
+ return /*#__PURE__*/jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, {
55
+ children: [children, /*#__PURE__*/jsxRuntimeExports.jsx(Overlay, {
56
+ ref: refs.setFloating,
57
+ isOpen: isOpen,
58
+ onDismiss: closeOverlay,
59
+ ignoreOutsideClickRefs: [refs.reference],
60
+ style: floatingStyles,
61
+ children: renderOverlay({
62
+ isOpen,
63
+ closeOverlay
64
+ })
65
+ })]
66
+ });
67
+ };
68
+
69
+ export { OverlayPopper as default };
@@ -0,0 +1,39 @@
1
+ import React__default from 'react';
2
+ import useProvidedOrCreatedRef from './useProvidedOrCreatedRef.js';
3
+ import { focusTrap } from '../node_modules/@primer/behaviors/dist/esm/focus-trap.js';
4
+
5
+ const useFocusTrap = (settings, dependencies = []) => {
6
+ const containerRef = useProvidedOrCreatedRef(settings?.containerRef);
7
+ const initialFocusRef = useProvidedOrCreatedRef(settings?.initialFocusRef);
8
+ const disabled = settings?.disabled;
9
+ const abortController = React__default.useRef();
10
+ const previousFocusedElement = React__default.useRef(null);
11
+ if (!previousFocusedElement.current && !settings?.disabled) {
12
+ previousFocusedElement.current = document.activeElement;
13
+ }
14
+ const disableTrap = () => {
15
+ abortController.current?.abort();
16
+ if (settings?.restoreFocusOnCleanUp && previousFocusedElement.current instanceof HTMLElement) {
17
+ previousFocusedElement.current.focus();
18
+ previousFocusedElement.current = null;
19
+ }
20
+ };
21
+ React__default.useEffect(() => {
22
+ if (containerRef.current instanceof HTMLElement) {
23
+ if (!disabled) {
24
+ abortController.current = focusTrap(containerRef.current, initialFocusRef.current ?? undefined);
25
+ return () => {
26
+ disableTrap();
27
+ };
28
+ } else {
29
+ disableTrap();
30
+ }
31
+ }
32
+ }, [containerRef, initialFocusRef, disabled, ...dependencies]);
33
+ return {
34
+ containerRef,
35
+ initialFocusRef
36
+ };
37
+ };
38
+
39
+ export { useFocusTrap as default };
@@ -0,0 +1,35 @@
1
+ import { useRef, useEffect } from 'react';
2
+ import useProvidedOrCreatedRef from './useProvidedOrCreatedRef.js';
3
+ import { focusZone } from '../node_modules/@primer/behaviors/dist/esm/focus-zone.js';
4
+ export { FocusKeys } from '../node_modules/@primer/behaviors/dist/esm/focus-zone.js';
5
+
6
+ const useFocusZone = (settings = {}, dependencies = []) => {
7
+ const containerRef = useProvidedOrCreatedRef(settings.containerRef);
8
+ const useActiveDescendant = !!settings.activeDescendantFocus;
9
+ const passedActiveDescendantRef = typeof settings.activeDescendantFocus === 'boolean' || !settings.activeDescendantFocus ? undefined : settings.activeDescendantFocus;
10
+ const activeDescendantControlRef = useProvidedOrCreatedRef(passedActiveDescendantRef);
11
+ const disabled = settings.disabled;
12
+ const abortController = useRef();
13
+ useEffect(() => {
14
+ if (containerRef.current instanceof HTMLElement && (!useActiveDescendant || activeDescendantControlRef.current instanceof HTMLElement)) {
15
+ if (!disabled) {
16
+ const defaultSettings = {
17
+ ...settings,
18
+ activeDescendantControl: activeDescendantControlRef.current ?? undefined
19
+ };
20
+ abortController.current = focusZone(containerRef.current, defaultSettings);
21
+ return () => {
22
+ abortController.current?.abort();
23
+ };
24
+ } else {
25
+ abortController.current?.abort();
26
+ }
27
+ }
28
+ }, [disabled, ...dependencies]);
29
+ return {
30
+ containerRef,
31
+ activeDescendantControlRef
32
+ };
33
+ };
34
+
35
+ export { useFocusZone as default };
package/esm/index.js CHANGED
@@ -11,6 +11,8 @@ export { default as IconToggleButton } from './core/IconToggleButton/index.js';
11
11
  export { default as Image } from './core/Image/index.js';
12
12
  export { default as ItemList } from './core/ItemList/index.js';
13
13
  export { default as MotionView } from './core/MotionView/index.js';
14
+ export { default as Overlay } from './core/Overlay/index.js';
15
+ export { default as OverlayPopper } from './core/OverlayPopper/index.js';
14
16
  export { default as Space } from './core/Space/index.js';
15
17
  export { default as Spinner } from './core/Spinner/index.js';
16
18
  export { default as Stack } from './core/Stack/index.js';
@@ -26,6 +28,10 @@ export { default as View } from './core/View/index.js';
26
28
  export { default as UnstyledButton } from './core/_UnstyledButton.js';
27
29
  export { default as EnigmaUI } from './enigma/EnigmaUI/index.js';
28
30
  export { default as useDevice } from './hook/useDevice.js';
31
+ export { default as useDialogHandler } from './hook/useDialogHandler.js';
32
+ export { default as useFocusTrap } from './hook/useFocusTrap.js';
33
+ export { default as useFocusZone } from './hook/useFocusZone.js';
34
+ export { default as useMediaQuery } from './hook/useMediaQuery.js';
29
35
  export { default as useOutsideClick } from './hook/useOutsideClick.js';
30
36
  export { default as useProvidedOrCreatedRef } from './hook/useProvidedOrCreatedRef.js';
31
37
  export { default as useResize } from './hook/useResize.js';