reshaped 2.10.14 → 2.10.16
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.
- package/CHANGELOG.md +9 -0
- package/bundle.css +1 -1
- package/bundle.js +15 -15
- package/components/ActionBar/ActionBar.module.css +1 -1
- package/components/Actionable/Actionable.js +0 -1
- package/components/Autocomplete/Autocomplete.js +7 -2
- package/components/Badge/Badge.module.css +1 -1
- package/components/Button/Button.module.css +1 -1
- package/components/Card/Card.d.ts +1 -1
- package/components/Card/tests/Card.stories.d.ts +1 -1
- package/components/Carousel/Carousel.module.css +1 -1
- package/components/FormControl/FormControl.context.d.ts +0 -2
- package/components/Link/Link.module.css +1 -1
- package/components/Loader/Loader.module.css +1 -1
- package/components/Modal/Modal.module.css +1 -1
- package/components/Overlay/Overlay.js +5 -16
- package/components/Pagination/Pagination.module.css +1 -0
- package/components/Pagination/PaginationControlled.js +3 -2
- package/components/Reshaped/Reshaped.css +1 -1
- package/components/Scrim/Scrim.module.css +1 -1
- package/components/ScrollArea/ScrollArea.module.css +1 -1
- package/components/Skeleton/Skeleton.module.css +1 -1
- package/components/Stepper/Stepper.module.css +1 -1
- package/components/Switch/Switch.module.css +1 -1
- package/components/Table/Table.js +1 -1
- package/components/Table/Table.module.css +1 -1
- package/components/Table/tests/Table.stories.js +16 -0
- package/components/Tabs/Tabs.module.css +1 -1
- package/components/Tabs/TabsItem.js +1 -1
- package/components/Tabs/TabsList.js +9 -9
- package/components/Toast/Toast.module.css +1 -1
- package/components/Toast/ToastContainer.js +6 -6
- package/components/Toast/ToastRegion.js +1 -1
- package/components/_private/Flyout/Flyout.module.css +1 -1
- package/components/_private/Flyout/Flyout.types.d.ts +1 -1
- package/components/_private/Flyout/FlyoutControlled.js +8 -14
- package/config/postcss.d.ts +1 -0
- package/config/postcss.js +1 -1
- package/hooks/_private/useSingletonKeyboardMode.js +3 -3
- package/hooks/useHotkeys.d.ts +6 -6
- package/package.json +31 -31
- package/utilities/a11y/TrapFocus.d.ts +41 -0
- package/utilities/a11y/TrapFocus.js +127 -0
- package/utilities/a11y/TrapScreenReader.d.ts +15 -0
- package/utilities/a11y/TrapScreenReader.js +39 -0
- package/utilities/a11y/focus.d.ts +24 -0
- package/utilities/a11y/focus.js +90 -0
- package/utilities/a11y/keyboardMode.d.ts +3 -0
- package/utilities/a11y/keyboardMode.js +10 -0
- package/utilities/a11y/types.d.ts +18 -0
- package/utilities/a11y/types.js +1 -0
- package/utilities/helpers.d.ts +1 -1
- package/constants/attributes.d.ts +0 -2
- package/constants/attributes.js +0 -2
- package/utilities/a11y.d.ts +0 -36
- 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 [
|
27
|
-
const rootClassNames = classNames(s.root, size && s[`--size-${size}`], direction && s[`--direction-${direction}`], itemWidth && s[`--item-width-${itemWidth}`], variant && s[`--variant-${variant}`],
|
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
|
-
|
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
|
123
|
+
return setFadeSide("both");
|
124
124
|
if (cutOffStart)
|
125
|
-
return
|
125
|
+
return setFadeSide("start");
|
126
126
|
if (cutOffEnd)
|
127
|
-
return
|
127
|
+
return setFadeSide("end");
|
128
128
|
};
|
129
|
-
const debouncedUpdateArrowNav = throttle(updateArrowNav,
|
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
|
-
} })] }) }), (
|
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 {
|
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
|
-
|
65
|
+
trapFocus.trap({
|
65
66
|
includeTrigger: true,
|
66
67
|
mode: "content-menu",
|
67
68
|
});
|
68
69
|
}
|
69
|
-
else if (
|
70
|
-
|
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
|
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
|
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
|
-
|
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 (
|
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
|
-
|
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
|
package/config/postcss.d.ts
CHANGED
package/config/postcss.js
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
import React from "react";
|
2
|
-
import {
|
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
|
-
|
8
|
+
enableKeyboardMode();
|
9
9
|
};
|
10
10
|
const handleClick = () => {
|
11
|
-
|
11
|
+
disableKeyboardMode();
|
12
12
|
};
|
13
13
|
window.addEventListener("keydown", handleKeyDown);
|
14
14
|
window.addEventListener("mousedown", handleClick);
|
package/hooks/useHotkeys.d.ts
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
import React from "react";
|
2
|
-
declare const useHotkeys: <
|
3
|
-
ref?: React.RefObject<
|
4
|
-
disabled?: boolean
|
5
|
-
preventDefault?: boolean
|
6
|
-
}
|
7
|
-
ref: React.RefObject<
|
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.
|
4
|
+
"version": "2.10.16",
|
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 --
|
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": "
|
85
|
-
"@commitlint/config-conventional": "
|
86
|
-
"@commitlint/types": "
|
87
|
-
"@size-limit/preset-big-lib": "11.
|
88
|
-
"@storybook/addon-a11y": "
|
89
|
-
"@storybook/addon-controls": "
|
90
|
-
"@storybook/addon-docs": "
|
91
|
-
"@storybook/addon-storysource": "
|
92
|
-
"@storybook/react": "
|
93
|
-
"@storybook/react-vite": "
|
94
|
-
"@testing-library/jest-dom": "6.4.
|
95
|
-
"@testing-library/react": "14.
|
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.
|
100
|
-
"@types/react": "18.2.
|
101
|
-
"@types/react-dom": "18.2.
|
102
|
-
"@typescript-eslint/eslint-plugin": "6.
|
103
|
-
"@typescript-eslint/parser": "6.
|
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": "
|
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.
|
120
|
-
"postcss": "8.4.
|
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.
|
128
|
-
"size-limit": "11.
|
129
|
-
"storybook": "
|
130
|
-
"stylelint": "16.
|
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.
|
135
|
-
"vite": "5.
|
136
|
-
"vite-tsconfig-paths": "4.3.
|
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.
|
148
|
-
"postcss-custom-media": "10.0.
|
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;
|