reshaped 2.10.14 → 2.10.15

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 (55) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bundle.css +1 -1
  3. package/bundle.js +15 -15
  4. package/components/Actionable/Actionable.js +0 -1
  5. package/components/Autocomplete/Autocomplete.js +7 -2
  6. package/components/Badge/Badge.module.css +1 -1
  7. package/components/Button/Button.module.css +1 -1
  8. package/components/Card/Card.d.ts +1 -1
  9. package/components/Card/tests/Card.stories.d.ts +1 -1
  10. package/components/Carousel/Carousel.module.css +1 -1
  11. package/components/FormControl/FormControl.context.d.ts +0 -2
  12. package/components/Link/Link.module.css +1 -1
  13. package/components/Loader/Loader.module.css +1 -1
  14. package/components/Modal/Modal.module.css +1 -1
  15. package/components/Overlay/Overlay.js +5 -16
  16. package/components/Pagination/Pagination.module.css +1 -0
  17. package/components/Pagination/PaginationControlled.js +3 -2
  18. package/components/Reshaped/Reshaped.css +1 -1
  19. package/components/Scrim/Scrim.module.css +1 -1
  20. package/components/ScrollArea/ScrollArea.module.css +1 -1
  21. package/components/Skeleton/Skeleton.module.css +1 -1
  22. package/components/Stepper/Stepper.module.css +1 -1
  23. package/components/Switch/Switch.module.css +1 -1
  24. package/components/Table/Table.js +1 -1
  25. package/components/Table/Table.module.css +1 -1
  26. package/components/Table/tests/Table.stories.js +16 -0
  27. package/components/Tabs/Tabs.module.css +1 -1
  28. package/components/Tabs/TabsItem.js +1 -1
  29. package/components/Tabs/TabsList.js +9 -9
  30. package/components/Toast/Toast.module.css +1 -1
  31. package/components/Toast/ToastContainer.js +6 -6
  32. package/components/Toast/ToastRegion.js +1 -1
  33. package/components/_private/Flyout/Flyout.module.css +1 -1
  34. package/components/_private/Flyout/Flyout.types.d.ts +1 -1
  35. package/components/_private/Flyout/FlyoutControlled.js +8 -14
  36. package/config/postcss.d.ts +1 -0
  37. package/config/postcss.js +1 -1
  38. package/hooks/_private/useSingletonKeyboardMode.js +3 -3
  39. package/hooks/useHotkeys.d.ts +6 -6
  40. package/package.json +31 -31
  41. package/utilities/a11y/TrapFocus.d.ts +41 -0
  42. package/utilities/a11y/TrapFocus.js +127 -0
  43. package/utilities/a11y/TrapScreenReader.d.ts +15 -0
  44. package/utilities/a11y/TrapScreenReader.js +39 -0
  45. package/utilities/a11y/focus.d.ts +24 -0
  46. package/utilities/a11y/focus.js +90 -0
  47. package/utilities/a11y/keyboardMode.d.ts +3 -0
  48. package/utilities/a11y/keyboardMode.js +10 -0
  49. package/utilities/a11y/types.d.ts +18 -0
  50. package/utilities/a11y/types.js +1 -0
  51. package/utilities/helpers.d.ts +1 -1
  52. package/constants/attributes.d.ts +0 -2
  53. package/constants/attributes.js +0 -2
  54. package/utilities/a11y.d.ts +0 -36
  55. package/utilities/a11y.js +0 -220
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { classNames, throttle } from "../../utilities/helpers.js";
5
5
  import useRTL from "../../hooks/useRTL.js";
6
- import { focusNextElement, focusPreviousElement, focusFirstElement, focusLastElement, } from "../../utilities/a11y.js";
6
+ import { focusNextElement, focusPreviousElement, focusFirstElement, focusLastElement, } from "../../utilities/a11y/focus.js";
7
7
  import useIsomorphicLayoutEffect from "../../hooks/useIsomorphicLayoutEffect.js";
8
8
  import useHotkeys from "../../hooks/useHotkeys.js";
9
9
  import Button from "../Button/index.js";
@@ -23,8 +23,8 @@ const TabsList = (props) => {
23
23
  const { children, className, attributes } = props;
24
24
  const { value, setDefaultValue, itemWidth, variant, name, direction, size, selection, setSelection, elActiveRef, elPrevActiveRef, elScrollableRef, } = useTabs();
25
25
  const [rtl] = useRTL();
26
- const [cutOffSide, setCutOffSide] = React.useState(null);
27
- const rootClassNames = classNames(s.root, size && s[`--size-${size}`], direction && s[`--direction-${direction}`], itemWidth && s[`--item-width-${itemWidth}`], variant && s[`--variant-${variant}`], cutOffSide && s[`--cut-off-${cutOffSide}`], className);
26
+ const [fadeSide, setFadeSide] = React.useState(null);
27
+ const rootClassNames = classNames(s.root, size && s[`--size-${size}`], direction && s[`--direction-${direction}`], itemWidth && s[`--item-width-${itemWidth}`], variant && s[`--variant-${variant}`], (fadeSide === "start" || fadeSide === "both") && s["--fade-start"], (fadeSide === "end" || fadeSide === "both") && s["--fade-end"], className);
28
28
  const selectorClassNames = classNames(s.selector, selection.status === "idle" && s["--selector-hidden"], selection.status === "animated" && s["--selector-animated"]);
29
29
  const handleNextClick = () => {
30
30
  elScrollableRef.current.scrollBy({
@@ -114,19 +114,19 @@ const TabsList = (props) => {
114
114
  const updateArrowNav = () => {
115
115
  const isScrollable = elScrollable.clientWidth < elScrollable.scrollWidth;
116
116
  if (!isScrollable)
117
- setCutOffSide(null);
117
+ setFadeSide(null);
118
118
  // scrollLeft in RTL starts from 1 instead of 0, so we compare values using this delta
119
119
  const scrollLeft = elScrollable.scrollLeft * (rtl ? -1 : 1);
120
120
  const cutOffStart = scrollLeft > 1;
121
121
  const cutOffEnd = scrollLeft + elScrollable.clientWidth < elScrollable.scrollWidth - 1;
122
122
  if (cutOffEnd && cutOffStart)
123
- return setCutOffSide("both");
123
+ return setFadeSide("both");
124
124
  if (cutOffStart)
125
- return setCutOffSide("start");
125
+ return setFadeSide("start");
126
126
  if (cutOffEnd)
127
- return setCutOffSide("end");
127
+ return setFadeSide("end");
128
128
  };
129
- const debouncedUpdateArrowNav = throttle(updateArrowNav, 100);
129
+ const debouncedUpdateArrowNav = throttle(updateArrowNav, 16);
130
130
  // Use RaF when scroll to have scrollWidth calculated correctly on the first effect
131
131
  // For example: And edge case inside the complex flexbox layout
132
132
  requestAnimationFrame(() => {
@@ -148,6 +148,6 @@ const TabsList = (props) => {
148
148
  "--rs-tab-selection-y": selection.top,
149
149
  "--rs-tab-selection-scale-x": selection.scaleX,
150
150
  "--rs-tab-selection-scale-y": selection.scaleY,
151
- } })] }) }), (cutOffSide === "start" || cutOffSide === "both") && (_jsx("span", { className: s.prev, children: _jsx(Button, { onClick: handlePrevClick, size: "small", icon: IconChevronLeft, rounded: true, attributes: { "aria-hidden": true, tabIndex: -1 } }) })), (cutOffSide === "end" || cutOffSide === "both") && (_jsx("span", { className: s.next, children: _jsx(Button, { onClick: handleNextClick, size: "small", icon: IconChevronRight, rounded: true, attributes: { "aria-hidden": true, tabIndex: -1 } }) }))] })));
151
+ } })] }) }), (fadeSide === "start" || fadeSide === "both") && (_jsx("span", { className: s.prev, children: _jsx(Button, { onClick: handlePrevClick, size: "small", icon: IconChevronLeft, rounded: true, attributes: { "aria-hidden": true, tabIndex: -1 } }) })), (fadeSide === "end" || fadeSide === "both") && (_jsx("span", { className: s.next, children: _jsx(Button, { onClick: handleNextClick, size: "small", icon: IconChevronRight, rounded: true, attributes: { "aria-hidden": true, tabIndex: -1 } }) }))] })));
152
152
  };
153
153
  export default TabsList;
@@ -1 +1 @@
1
- .container{display:block;opacity:0;position:relative;transition:var(--rs-duration-medium) ease-out;transition-property:transform,height,opacity;width:100%}.container--visible{opacity:1}.container--visible .wrapper{height:calc(100% - var(--rs-unit-x2))}.container--index-0{z-index:var(--rs-z-index-raised)}.container--index-1{height:var(--rs-unit-x2)!important}.container--index-1 .wrapper{height:100%;transform:translateY(calc(var(--rs-unit-x1) * -1)) translateZ(0) scaleX(.9)}.container--index-2{height:var(--rs-unit-x2)!important}.container--index-2 .wrapper{height:100%;transform:translateY(calc(var(--rs-unit-x2) * -1)) translateZ(0) scaleX(.8)}.container--index-overflow{height:0!important}.container--index-overflow .wrapper{height:100%;opacity:0;transform:translateY(calc(var(--rs-unit-x3) * -1)) translateZ(0) scaleX(.8)}.wrapper{border-radius:var(--rs-unit-radius-medium);box-shadow:var(--rs-shadow-overlay);height:100%;margin-top:var(--rs-unit-x2);overflow:hidden;transform-origin:50% 0;transition:var(--rs-duration-medium) ease-out;transition-property:height,transform,opacity}.region,.wrapper{display:flex;flex-direction:column}.region{max-width:100%;padding:var(--rs-unit-x4);position:fixed;width:100%;z-index:var(--rs-z-index-notification)}.region--nested{position:absolute}.region--position-top{align-items:center;left:50%;top:0;transform:translateX(-50%)}.region--position-top-start{align-items:start;inset-inline-start:0;top:0}.region--position-top-end{inset-inline-end:0;top:0}.region--position-top,.region--position-top-end,.region--position-top-start{flex-direction:column-reverse}.region--position-top .wrapper,.region--position-top-end .wrapper,.region--position-top-start .wrapper{justify-content:flex-end;margin-bottom:var(--rs-unit-x2);margin-top:0;transform-origin:bottom}.region--position-top .container--index-2 .wrapper,.region--position-top-end .container--index-2 .wrapper,.region--position-top-start .container--index-2 .wrapper{transform:translateY(0) translateZ(0) scaleX(.8)}.region--position-top .container--index-overflow .wrapper,.region--position-top-end .container--index-overflow .wrapper,.region--position-top-start .container--index-overflow .wrapper{transform:translateY(var(--rs-unit-x1)) translateZ(0) scaleX(.8)}.region--position-bottom{align-items:center;bottom:0;left:50%;transform:translateX(-50%)}.region--position-bottom-start{align-items:start;bottom:0;inset-inline-start:0}.region--position-bottom-end{align-items:end;bottom:0;inset-inline-end:0}@media (--rs-viewport-m ){.region{width:360px}}
1
+ .container{display:block;opacity:0;position:relative;transition:var(--rs-duration-medium) ease-out;transition-property:transform,height,opacity;width:100%}.container--visible{opacity:1}.container--visible .wrapper{height:calc(100% - var(--rs-unit-x2))}.container--index-0{z-index:var(--rs-z-index-raised)}.container--index-1{height:var(--rs-unit-x2)!important}.container--index-1 .wrapper{height:100%;transform:translateY(calc(var(--rs-unit-x1) * -1)) translateZ(0) scaleX(0.9)}.container--index-2{height:var(--rs-unit-x2)!important}.container--index-2 .wrapper{height:100%;transform:translateY(calc(var(--rs-unit-x2) * -1)) translateZ(0) scaleX(0.8)}.container--index-overflow{height:0!important}.container--index-overflow .wrapper{height:100%;opacity:0;transform:translateY(calc(var(--rs-unit-x3) * -1)) translateZ(0) scaleX(0.8)}.wrapper{border-radius:var(--rs-unit-radius-medium);box-shadow:var(--rs-shadow-overlay);height:100%;margin-top:var(--rs-unit-x2);overflow:hidden;transform-origin:50% 0;transition:var(--rs-duration-medium) ease-out;transition-property:height,transform,opacity}.region,.wrapper{display:flex;flex-direction:column}.region{max-width:100%;padding:var(--rs-unit-x4);position:fixed;width:100%;z-index:var(--rs-z-index-notification)}.region--nested{position:absolute}.region--position-top{align-items:center;left:50%;top:0;transform:translateX(-50%)}.region--position-top-start{align-items:start;inset-inline-start:0;top:0}.region--position-top-end{inset-inline-end:0;top:0}.region--position-top,.region--position-top-end,.region--position-top-start{flex-direction:column-reverse}.region--position-top .wrapper,.region--position-top-end .wrapper,.region--position-top-start .wrapper{justify-content:flex-end;margin-bottom:var(--rs-unit-x2);margin-top:0;transform-origin:bottom}.region--position-top .container--index-2 .wrapper,.region--position-top-end .container--index-2 .wrapper,.region--position-top-start .container--index-2 .wrapper{transform:translateY(0) translateZ(0) scaleX(0.8)}.region--position-top .container--index-overflow .wrapper,.region--position-top-end .container--index-overflow .wrapper,.region--position-top-start .container--index-overflow .wrapper{transform:translateY(var(--rs-unit-x1)) translateZ(0) scaleX(0.8)}.region--position-bottom{align-items:center;bottom:0;left:50%;transform:translateX(-50%)}.region--position-bottom-start{align-items:start;bottom:0;inset-inline-start:0}.region--position-bottom-end{align-items:end;bottom:0;inset-inline-end:0}@media (--rs-viewport-m ){.region{width:360px}}
@@ -3,7 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { classNames } from "../../utilities/helpers.js";
5
5
  import { onNextFrame } from "../../utilities/animation.js";
6
- import { trapFocus, isKeyboardMode } from "../../utilities/a11y.js";
6
+ import { checkKeyboardMode } from "../../utilities/a11y/keyboardMode.js";
7
+ import TrapFocus from "../../utilities/a11y/TrapFocus.js";
7
8
  import Toast from "./Toast.js";
8
9
  import ToastContext from "./Toast.context.js";
9
10
  import { timeouts } from "./Toast.constants.js";
@@ -15,7 +16,6 @@ const ToastContainer = (props) => {
15
16
  const [toastHeight, setToastHeight] = React.useState();
16
17
  const timeoutRef = React.useRef();
17
18
  const resizingRef = React.useRef(false);
18
- const trapFocusRef = React.useRef(null);
19
19
  const wrapperRef = React.useRef(null);
20
20
  const visible = status === "entered";
21
21
  const containerClassNames = classNames(s.container, visible && s[`container--visible`], index === 0 && s[`container--index-${index}`], !inspected && (index === 1 || index === 2) && s[`container--index-${index}`], !inspected && index >= 3 && s["container--index-overflow"]);
@@ -60,15 +60,15 @@ const ToastContainer = (props) => {
60
60
  React.useEffect(() => {
61
61
  if (!wrapperRef.current)
62
62
  return;
63
+ const trapFocus = new TrapFocus(wrapperRef.current);
63
64
  if (visible) {
64
- trapFocusRef.current = trapFocus(wrapperRef.current, {
65
+ trapFocus.trap({
65
66
  includeTrigger: true,
66
67
  mode: "content-menu",
67
68
  });
68
69
  }
69
- else if (trapFocusRef.current && isKeyboardMode()) {
70
- trapFocusRef.current();
71
- trapFocusRef.current = null;
70
+ else if (checkKeyboardMode()) {
71
+ trapFocus.release();
72
72
  }
73
73
  }, [visible]);
74
74
  React.useEffect(() => {
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { classNames } from "../../utilities/helpers.js";
5
- import { focusableSelector } from "../../utilities/a11y.js";
5
+ import { focusableSelector } from "../../utilities/a11y/focus.js";
6
6
  import ToastContainer from "./ToastContainer.js";
7
7
  import ToastContext from "./Toast.context.js";
8
8
  import s from "./Toast.module.css";
@@ -1 +1 @@
1
- .content{--rs-flyout-gap:2;--rs-flyout-origin-x:50%;--rs-flyout-origin-y:50%;position:absolute}.inner{opacity:0;transform:scale(.8) translateY(0);transform-origin:var(--rs-flyout-origin-x) var(--rs-flyout-origin-y)}.content.--width-trigger .inner{transform:scale(1) translateY(var(--rs-unit-x2))}.content.--position-top,.content.--position-top-end,.content.--position-top-start{--rs-flyout-origin-y:100%;padding-bottom:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-bottom,.content.--position-bottom-end,.content.--position-bottom-start{--rs-flyout-origin-y:0%;padding-top:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-bottom-start,.content.--position-top-start{--rs-flyout-origin-x:0%}.content.--position-bottom-end,.content.--position-top-end{--rs-flyout-origin-x:100%}.content.--position-start,.content.--position-start-bottom,.content.--position-start-top{--rs-flyout-origin-x:100%;padding-right:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-end,.content.--position-end-bottom,.content.--position-end-top{--rs-flyout-origin-x:0%;padding-left:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-end-top,.content.--position-start-top{--rs-flyout-origin-y:0%}.content.--position-end-bottom,.content.--position-start-bottom{--rs-flyout-origin-y:100%}.content.--visible .inner{opacity:1;transform:scale(1) translateY(0)}.content.--animated .inner{transition:var(--rs-duration-fast) var(--rs-easing-accelerate);transition-property:opacity,transform}.content.--animated.--visible .inner{transition-timing-function:var(--rs-easing-decelerate)}
1
+ .content{--rs-flyout-gap:2;--rs-flyout-origin-x:50%;--rs-flyout-origin-y:50%;position:absolute}.inner{opacity:0;transform:scale(0.8) translateY(0);transform-origin:var(--rs-flyout-origin-x) var(--rs-flyout-origin-y)}.content.--width-trigger .inner{transform:scale(1) translateY(var(--rs-unit-x2))}.content.--position-top,.content.--position-top-end,.content.--position-top-start{--rs-flyout-origin-y:100%;padding-bottom:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-bottom,.content.--position-bottom-end,.content.--position-bottom-start{--rs-flyout-origin-y:0%;padding-top:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-bottom-start,.content.--position-top-start{--rs-flyout-origin-x:0%}.content.--position-bottom-end,.content.--position-top-end{--rs-flyout-origin-x:100%}.content.--position-start,.content.--position-start-bottom,.content.--position-start-top{--rs-flyout-origin-x:100%;padding-right:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-end,.content.--position-end-bottom,.content.--position-end-top{--rs-flyout-origin-x:0%;padding-left:calc(var(--rs-unit-x1) * var(--rs-flyout-gap))}.content.--position-end-top,.content.--position-start-top{--rs-flyout-origin-y:0%}.content.--position-end-bottom,.content.--position-start-bottom{--rs-flyout-origin-y:100%}.content.--visible .inner{opacity:1;transform:scale(1) translateY(0)}.content.--animated .inner{transition:var(--rs-duration-fast) var(--rs-easing-accelerate);transition-property:opacity,transform}.content.--animated.--visible .inner{transition-timing-function:var(--rs-easing-decelerate)}
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import type * as G from "../../../types/global";
3
- import type { TrapMode } from "../../../utilities/a11y";
3
+ import type { TrapMode } from "../../../utilities/a11y/types";
4
4
  import useFlyout, { FlyoutPosition, FlyoutWidth } from "../../../hooks/_private/useFlyout";
5
5
  export type InstanceRef = {
6
6
  open: () => void;
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { debounce } from "../../../utilities/helpers.js";
5
- import { trapFocus } from "../../../utilities/a11y.js";
5
+ import TrapFocus from "../../../utilities/a11y/TrapFocus.js";
6
6
  import * as timeouts from "../../../constants/timeouts.js";
7
7
  import useIsDismissible from "../../../hooks/_private/useIsDismissible.js";
8
8
  import useElementId from "../../../hooks/useElementId.js";
@@ -20,7 +20,7 @@ const FlyoutRoot = (props) => {
20
20
  const flyoutElRef = React.useRef(null);
21
21
  const id = useElementId(passedId);
22
22
  const timerRef = React.useRef();
23
- const releaseFocusRef = React.useRef(null);
23
+ const trapFocusRef = React.useRef(null);
24
24
  const lockedRef = React.useRef(false);
25
25
  const lockedBlurEffects = React.useRef(false);
26
26
  const shouldReturnFocusRef = React.useRef(true);
@@ -134,21 +134,22 @@ const FlyoutRoot = (props) => {
134
134
  useIsomorphicLayoutEffect(() => {
135
135
  if (status !== "visible" || !flyoutElRef.current)
136
136
  return;
137
- releaseFocusRef.current = trapFocus(flyoutElRef.current, {
137
+ trapFocusRef.current = new TrapFocus(flyoutElRef.current);
138
+ trapFocusRef.current.trap({
138
139
  mode: trapFocusMode,
139
140
  includeTrigger: triggerType === "hover" && trapFocusMode === "content-menu",
140
141
  onNavigateOutside: () => {
141
- releaseFocusRef.current = null;
142
142
  handleClose();
143
143
  },
144
144
  });
145
145
  }, [status, triggerType, handleClose, trapFocusMode]);
146
146
  React.useEffect(() => {
147
+ var _a;
147
148
  if (!disableHideAnimation && status !== "hidden")
148
149
  return;
149
150
  if (disableHideAnimation && status !== "idle")
150
151
  return;
151
- if (releaseFocusRef.current) {
152
+ if ((_a = trapFocusRef.current) === null || _a === void 0 ? void 0 : _a.trapped) {
152
153
  /* Locking the popover to not open it again on trigger focus */
153
154
  if (triggerType === "hover") {
154
155
  lockedRef.current = true;
@@ -156,10 +157,7 @@ const FlyoutRoot = (props) => {
156
157
  lockedRef.current = false;
157
158
  }, 100);
158
159
  }
159
- releaseFocusRef.current({
160
- withoutFocusReturn: !shouldReturnFocusRef.current,
161
- });
162
- releaseFocusRef.current = null;
160
+ trapFocusRef.current.release({ withoutFocusReturn: !shouldReturnFocusRef.current });
163
161
  shouldReturnFocusRef.current = true;
164
162
  }
165
163
  }, [status, triggerType, disableHideAnimation]);
@@ -167,11 +165,7 @@ const FlyoutRoot = (props) => {
167
165
  * Release focus trapping on unmount
168
166
  */
169
167
  React.useEffect(() => {
170
- return () => {
171
- if (releaseFocusRef.current)
172
- releaseFocusRef.current();
173
- releaseFocusRef.current = null;
174
- };
168
+ return () => { var _a; return (_a = trapFocusRef.current) === null || _a === void 0 ? void 0 : _a.release(); };
175
169
  }, []);
176
170
  /**
177
171
  * Update position on resize or RTL
@@ -22,6 +22,7 @@ export declare const getConfig: (options: {
22
22
  cssnano: {
23
23
  preset: (string | {
24
24
  calc: boolean;
25
+ convertValues: boolean;
25
26
  })[];
26
27
  };
27
28
  };
package/config/postcss.js CHANGED
@@ -26,7 +26,7 @@ const getConfig = (options) => {
26
26
  files: [themeMediaCSSPath],
27
27
  },
28
28
  "postcss-custom-media": {},
29
- cssnano: { preset: ["default", { calc: false }] },
29
+ cssnano: { preset: ["default", { calc: false, convertValues: false }] },
30
30
  },
31
31
  };
32
32
  };
@@ -1,14 +1,14 @@
1
1
  import React from "react";
2
- import { keyboardModeAttribute } from "../../constants/attributes.js";
2
+ import { enableKeyboardMode, disableKeyboardMode } from "../../utilities/a11y/keyboardMode.js";
3
3
  const useSingletonKeyboardMode = () => {
4
4
  React.useEffect(() => {
5
5
  const handleKeyDown = (e) => {
6
6
  if (e.metaKey || e.altKey || e.ctrlKey)
7
7
  return;
8
- document.documentElement.setAttribute(keyboardModeAttribute, "true");
8
+ enableKeyboardMode();
9
9
  };
10
10
  const handleClick = () => {
11
- document.documentElement.removeAttribute(keyboardModeAttribute);
11
+ disableKeyboardMode();
12
12
  };
13
13
  window.addEventListener("keydown", handleKeyDown);
14
14
  window.addEventListener("mousedown", handleClick);
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
- declare const useHotkeys: <Element_1 extends HTMLElement>(hotkeys: Record<string, ((e: KeyboardEvent) => void) | null>, deps?: unknown[], options?: {
3
- ref?: React.RefObject<Element_1> | undefined;
4
- disabled?: boolean | undefined;
5
- preventDefault?: boolean | undefined;
6
- } | undefined) => {
7
- ref: React.RefObject<Element_1>;
2
+ declare const useHotkeys: <Element extends HTMLElement>(hotkeys: Record<string, ((e: KeyboardEvent) => void) | null>, deps?: unknown[], options?: {
3
+ ref?: React.RefObject<Element>;
4
+ disabled?: boolean;
5
+ preventDefault?: boolean;
6
+ }) => {
7
+ ref: React.RefObject<Element>;
8
8
  checkHotkeyState: (key: string) => boolean;
9
9
  };
10
10
  export default useHotkeys;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reshaped",
3
3
  "description": "Professionally crafted design system in React & Figma for building products of any scale and complexity",
4
- "version": "2.10.14",
4
+ "version": "2.10.15",
5
5
  "license": "MIT",
6
6
  "email": "hello@reshaped.so",
7
7
  "homepage": "https://reshaped.so",
@@ -73,7 +73,7 @@
73
73
  "test:unit": "jest --config tools/jest/jest.config.js",
74
74
  "test:size": "size-limit",
75
75
  "lint": "yarn lint:js && yarn lint:css",
76
- "lint:js": "eslint --ext .ts,.tsx src/**/*.ts src/**/*.tsx --quiet --fix",
76
+ "lint:js": "eslint --quiet --fix",
77
77
  "lint:css": "stylelint 'src/**/*.css'",
78
78
  "commit": "git-cz"
79
79
  },
@@ -81,28 +81,28 @@
81
81
  "defaults and not IE 11"
82
82
  ],
83
83
  "devDependencies": {
84
- "@commitlint/cli": "18.6.0",
85
- "@commitlint/config-conventional": "18.6.0",
86
- "@commitlint/types": "18.6.0",
87
- "@size-limit/preset-big-lib": "11.0.2",
88
- "@storybook/addon-a11y": "7.6.12",
89
- "@storybook/addon-controls": "7.6.12",
90
- "@storybook/addon-docs": "7.6.12",
91
- "@storybook/addon-storysource": "7.6.12",
92
- "@storybook/react": "7.6.12",
93
- "@storybook/react-vite": "7.6.12",
94
- "@testing-library/jest-dom": "6.4.1",
95
- "@testing-library/react": "14.2.1",
84
+ "@commitlint/cli": "19.2.1",
85
+ "@commitlint/config-conventional": "19.1.0",
86
+ "@commitlint/types": "19.0.3",
87
+ "@size-limit/preset-big-lib": "11.1.2",
88
+ "@storybook/addon-a11y": "8.0.6",
89
+ "@storybook/addon-controls": "8.0.6",
90
+ "@storybook/addon-docs": "8.0.6",
91
+ "@storybook/addon-storysource": "8.0.6",
92
+ "@storybook/react": "8.0.6",
93
+ "@storybook/react-vite": "8.0.6",
94
+ "@testing-library/jest-dom": "6.4.2",
95
+ "@testing-library/react": "14.3.0",
96
96
  "@testing-library/user-event": "14.5.2",
97
97
  "@types/events": "3.0.3",
98
98
  "@types/jest": "29.5.12",
99
- "@types/node": "20.11.16",
100
- "@types/react": "18.2.52",
101
- "@types/react-dom": "18.2.18",
102
- "@typescript-eslint/eslint-plugin": "6.20.0",
103
- "@typescript-eslint/parser": "6.20.0",
99
+ "@types/node": "20.12.5",
100
+ "@types/react": "18.2.74",
101
+ "@types/react-dom": "18.2.24",
102
+ "@typescript-eslint/eslint-plugin": "7.6.0",
103
+ "@typescript-eslint/parser": "7.6.0",
104
104
  "@vitejs/plugin-react": "4.2.1",
105
- "chromatic": "10.7.1",
105
+ "chromatic": "11.3.0",
106
106
  "cz-conventional-changelog": "3.3.0",
107
107
  "eslint": "8.56.0",
108
108
  "eslint-config-airbnb-typescript": "17.1.0",
@@ -116,24 +116,24 @@
116
116
  "jest": "29.7.0",
117
117
  "jest-environment-jsdom": "29.7.0",
118
118
  "jest-matchmedia-mock": "1.1.0",
119
- "lefthook": "1.6.1",
120
- "postcss": "8.4.33",
119
+ "lefthook": "1.6.8",
120
+ "postcss": "8.4.38",
121
121
  "postcss-cli": "11.0.0",
122
122
  "postcss-each": "1.1.0",
123
123
  "postcss-nested": "6.0.1",
124
124
  "prettier": "3.2.5",
125
125
  "react": "18.2.0",
126
126
  "react-dom": "18.2.0",
127
- "resolve-tspaths": "0.8.17",
128
- "size-limit": "11.0.2",
129
- "storybook": "7.6.12",
130
- "stylelint": "16.2.1",
127
+ "resolve-tspaths": "0.8.18",
128
+ "size-limit": "11.1.2",
129
+ "storybook": "8.0.6",
130
+ "stylelint": "16.3.1",
131
131
  "stylelint-config-prettier": "9.0.5",
132
132
  "stylelint-config-standard": "36.0.0",
133
133
  "ts-jest": "29.1.2",
134
- "typescript": "5.3.3",
135
- "vite": "5.0.12",
136
- "vite-tsconfig-paths": "4.3.1"
134
+ "typescript": "5.4.4",
135
+ "vite": "5.2.8",
136
+ "vite-tsconfig-paths": "4.3.2"
137
137
  },
138
138
  "peerDependencies": {
139
139
  "postcss": "^8",
@@ -144,8 +144,8 @@
144
144
  "@csstools/postcss-global-data": "2.1.1",
145
145
  "chalk": "4.1.2",
146
146
  "commander": "11.1.0",
147
- "cssnano": "6.0.3",
148
- "postcss-custom-media": "10.0.2"
147
+ "cssnano": "6.1.2",
148
+ "postcss-custom-media": "10.0.4"
149
149
  },
150
150
  "resolutions": {
151
151
  "jackspeak": "2.1.1"
@@ -0,0 +1,41 @@
1
+ import Chain from "../Chain";
2
+ import TrapScreenReader from "./TrapScreenReader";
3
+ import type { FocusableElement, TrapMode } from "./types";
4
+ type ReleaseOptions = {
5
+ withoutFocusReturn?: boolean;
6
+ };
7
+ type TrapOptions = {
8
+ onNavigateOutside?: () => void;
9
+ includeTrigger?: boolean;
10
+ mode?: TrapMode;
11
+ };
12
+ declare class TrapFocus {
13
+ static chain: Chain<TrapFocus>;
14
+ chainId?: number;
15
+ root: HTMLElement;
16
+ trigger: FocusableElement | null;
17
+ options: TrapOptions & {
18
+ pseudoFocus?: boolean;
19
+ };
20
+ trapped?: boolean;
21
+ screenReaderTrap: TrapScreenReader;
22
+ mutationObserver: MutationObserver | null;
23
+ constructor(root: HTMLElement);
24
+ /**
25
+ * Handle keyboard navigation while focus is trapped
26
+ */
27
+ handleKeyDown: (event: KeyboardEvent) => void;
28
+ addListeners: () => void;
29
+ removeListeners: () => void;
30
+ /**
31
+ * Trap the focus, add observer and keyboard event listeners
32
+ * and create a chain item
33
+ */
34
+ trap: (options?: TrapOptions) => void;
35
+ /**
36
+ * Disabled the trap focus for the element,
37
+ * cleanup all observers/handlers and trap for the previous element in the chain
38
+ */
39
+ release: (releaseOptions?: ReleaseOptions) => void;
40
+ }
41
+ export default TrapFocus;
@@ -0,0 +1,127 @@
1
+ import Chain from "../Chain.js";
2
+ import * as keys from "../../constants/keys.js";
3
+ import TrapScreenReader from "./TrapScreenReader.js";
4
+ import { getActiveElement, getFocusableElements, focusElement, getFocusData } from "./focus.js";
5
+ import { checkKeyboardMode } from "./keyboardMode.js";
6
+ class TrapFocus {
7
+ constructor(root) {
8
+ this.trigger = null;
9
+ this.options = {};
10
+ this.mutationObserver = null;
11
+ /**
12
+ * Handle keyboard navigation while focus is trapped
13
+ */
14
+ this.handleKeyDown = (event) => {
15
+ if (TrapFocus.chain.tailId !== this.chainId)
16
+ return;
17
+ const { mode, onNavigateOutside, pseudoFocus, includeTrigger } = this.options;
18
+ let navigationMode = "tabs";
19
+ if (mode === "action-menu" || mode === "selection-menu")
20
+ navigationMode = "arrows";
21
+ const key = event.key;
22
+ const isTab = key === keys.TAB;
23
+ const isNextTab = isTab && !event.shiftKey;
24
+ const isBackTab = isTab && event.shiftKey;
25
+ const isUp = navigationMode === "arrows" && key === keys.UP;
26
+ const isDown = navigationMode === "arrows" && key === keys.DOWN;
27
+ const isPrev = (isBackTab && navigationMode === "tabs") || isUp;
28
+ const isNext = (isNextTab && navigationMode === "tabs") || isDown;
29
+ const isFocusedOnTrigger = getActiveElement() === this.trigger;
30
+ const focusData = getFocusData({
31
+ root: this.root,
32
+ target: isPrev ? "prev" : "next",
33
+ options: {
34
+ additionalElement: includeTrigger ? this.trigger : undefined,
35
+ circular: mode !== "action-menu",
36
+ },
37
+ });
38
+ // Release the trap when tab is used in navigation modes that support arrows
39
+ const hasNavigatedOutside = (isTab && navigationMode === "arrows") ||
40
+ (mode === "content-menu" && isTab && focusData.overflow);
41
+ if (hasNavigatedOutside) {
42
+ // Prevent shift + tab event to avoid focus moving after the trap release
43
+ if (isBackTab && !isFocusedOnTrigger)
44
+ event.preventDefault();
45
+ this.release();
46
+ onNavigateOutside === null || onNavigateOutside === void 0 ? void 0 : onNavigateOutside();
47
+ return;
48
+ }
49
+ if (!isPrev && !isNext)
50
+ return;
51
+ event.preventDefault();
52
+ if (!focusData.el)
53
+ return;
54
+ focusElement(focusData.el, { pseudoFocus });
55
+ };
56
+ this.addListeners = () => document.addEventListener("keydown", this.handleKeyDown);
57
+ this.removeListeners = () => document.removeEventListener("keydown", this.handleKeyDown);
58
+ /**
59
+ * Trap the focus, add observer and keyboard event listeners
60
+ * and create a chain item
61
+ */
62
+ this.trap = (options = {}) => {
63
+ const { mode = "dialog", includeTrigger } = options;
64
+ const trigger = getActiveElement();
65
+ const focusable = getFocusableElements(this.root, {
66
+ additionalElement: includeTrigger ? trigger : undefined,
67
+ });
68
+ const pseudoFocus = mode === "selection-menu";
69
+ this.options = Object.assign(Object.assign({}, options), { pseudoFocus });
70
+ this.trigger = trigger;
71
+ this.mutationObserver = new MutationObserver(() => {
72
+ const currentActiveElement = getActiveElement();
73
+ // Focus stayed inside the wrapper, no need to refocus
74
+ if (this.root.contains(currentActiveElement))
75
+ return;
76
+ const focusable = getFocusableElements(this.root, {
77
+ additionalElement: includeTrigger ? trigger : undefined,
78
+ });
79
+ if (!focusable.length)
80
+ return;
81
+ focusElement(focusable[0], { pseudoFocus });
82
+ });
83
+ this.removeListeners();
84
+ if (mode === "dialog")
85
+ this.screenReaderTrap.trap();
86
+ this.mutationObserver.observe(this.root, { childList: true, subtree: true });
87
+ if (!focusable.length)
88
+ return;
89
+ this.addListeners();
90
+ // Don't add back to the chain if we're traversing back
91
+ const tailItem = TrapFocus.chain.tailId && TrapFocus.chain.get(TrapFocus.chain.tailId);
92
+ if (!tailItem || this.root !== tailItem.data.root) {
93
+ this.chainId = TrapFocus.chain.add(this);
94
+ focusElement(focusable[0], { pseudoFocus });
95
+ }
96
+ this.trapped = true;
97
+ };
98
+ /**
99
+ * Disabled the trap focus for the element,
100
+ * cleanup all observers/handlers and trap for the previous element in the chain
101
+ */
102
+ this.release = (releaseOptions = {}) => {
103
+ var _a;
104
+ const { withoutFocusReturn } = releaseOptions;
105
+ if (!this.trapped || !this.chainId)
106
+ return;
107
+ this.trapped = false;
108
+ if (this.trigger) {
109
+ const preventScroll = withoutFocusReturn || !checkKeyboardMode();
110
+ this.trigger.focus({ preventScroll });
111
+ }
112
+ TrapFocus.chain.removePreviousTill(this.chainId, (item) => document.body.contains(item.data.trigger));
113
+ (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
114
+ this.removeListeners();
115
+ this.screenReaderTrap.release();
116
+ const previousItem = TrapFocus.chain.tailId && TrapFocus.chain.get(TrapFocus.chain.tailId);
117
+ if (previousItem) {
118
+ const trapInstance = new TrapFocus(previousItem.data.root);
119
+ trapInstance.trap(previousItem.data.options);
120
+ }
121
+ };
122
+ this.root = root;
123
+ this.screenReaderTrap = new TrapScreenReader(root);
124
+ }
125
+ }
126
+ TrapFocus.chain = new Chain();
127
+ export default TrapFocus;
@@ -0,0 +1,15 @@
1
+ declare class TrapScreenReader {
2
+ root: HTMLElement;
3
+ /**
4
+ * Elements ignored by screen reader when trap is active
5
+ */
6
+ private hiddenElements;
7
+ constructor(root: HTMLElement);
8
+ /**
9
+ * Apply aria-hidden to all elements except the passed
10
+ */
11
+ hideSiblingsFromScreenReader: (el: HTMLElement) => void;
12
+ release: () => void;
13
+ trap: () => void;
14
+ }
15
+ export default TrapScreenReader;
@@ -0,0 +1,39 @@
1
+ class TrapScreenReader {
2
+ constructor(root) {
3
+ /**
4
+ * Elements ignored by screen reader when trap is active
5
+ */
6
+ this.hiddenElements = [];
7
+ /**
8
+ * Apply aria-hidden to all elements except the passed
9
+ */
10
+ this.hideSiblingsFromScreenReader = (el) => {
11
+ let sibling = el.parentNode && el.parentNode.firstChild;
12
+ while (sibling) {
13
+ const notCurrent = sibling !== el;
14
+ const isValid = sibling.nodeType === 1 && !sibling.hasAttribute("aria-hidden");
15
+ if (notCurrent && isValid) {
16
+ sibling.setAttribute("aria-hidden", "true");
17
+ this.hiddenElements.push(sibling);
18
+ }
19
+ sibling = sibling.nextSibling;
20
+ }
21
+ };
22
+ this.release = () => {
23
+ this.hiddenElements.forEach((el) => {
24
+ el.removeAttribute("aria-hidden");
25
+ });
26
+ this.hiddenElements = [];
27
+ };
28
+ this.trap = () => {
29
+ let currentEl = this.root;
30
+ this.release();
31
+ while (currentEl !== document.body) {
32
+ this.hideSiblingsFromScreenReader(currentEl);
33
+ currentEl = currentEl.parentElement;
34
+ }
35
+ };
36
+ this.root = root;
37
+ }
38
+ }
39
+ export default TrapScreenReader;
@@ -0,0 +1,24 @@
1
+ import type { FocusableElement } from "./types";
2
+ export declare const focusableSelector = "a,button,input:not([type=\"hidden\"]),textarea,select,details,[tabindex]:not([tabindex=\"-1\"])";
3
+ export declare const getActiveElement: () => HTMLButtonElement;
4
+ export declare const focusElement: (el: FocusableElement, options?: {
5
+ pseudoFocus?: boolean;
6
+ }) => void;
7
+ export declare const getFocusableElements: (rootEl: HTMLElement, options?: {
8
+ additionalElement?: FocusableElement | null;
9
+ }) => FocusableElement[];
10
+ export declare const getFocusData: (args: {
11
+ root: HTMLElement;
12
+ target: "next" | "prev" | "first" | "last";
13
+ options?: {
14
+ circular?: boolean;
15
+ additionalElement?: FocusableElement | null;
16
+ };
17
+ }) => {
18
+ overflow: boolean;
19
+ el: FocusableElement;
20
+ };
21
+ export declare const focusNextElement: (root: HTMLElement) => void;
22
+ export declare const focusPreviousElement: (root: HTMLElement) => void;
23
+ export declare const focusFirstElement: (root: HTMLElement) => void;
24
+ export declare const focusLastElement: (root: HTMLElement) => void;