@salt-ds/lab 1.0.0-alpha.91 → 1.0.0-alpha.92
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/css/salt-lab.css +48 -42
- package/dist-cjs/side-panel/SidePanel.css.js +1 -1
- package/dist-cjs/side-panel/SidePanel.js +86 -40
- package/dist-cjs/side-panel/SidePanel.js.map +1 -1
- package/dist-cjs/side-panel/SidePanelContent.js.map +1 -1
- package/dist-cjs/side-panel/SidePanelHeader.js.map +1 -1
- package/dist-cjs/side-panel/SidePanelProvider.js +6 -17
- package/dist-cjs/side-panel/SidePanelProvider.js.map +1 -1
- package/dist-cjs/side-panel/SidePanelTitle.js.map +1 -1
- package/dist-cjs/side-panel/SidePanelTrigger.js.map +1 -1
- package/dist-cjs/side-panel/internal/SidePanelContext.js +1 -3
- package/dist-cjs/side-panel/internal/SidePanelContext.js.map +1 -1
- package/dist-cjs/side-panel/internal/useSidePanelTabOrder.js +121 -28
- package/dist-cjs/side-panel/internal/useSidePanelTabOrder.js.map +1 -1
- package/dist-cjs/side-panel/useSidePanel.js +7 -7
- package/dist-cjs/side-panel/useSidePanel.js.map +1 -1
- package/dist-es/side-panel/SidePanel.css.js +1 -1
- package/dist-es/side-panel/SidePanel.js +87 -41
- package/dist-es/side-panel/SidePanel.js.map +1 -1
- package/dist-es/side-panel/SidePanelContent.js.map +1 -1
- package/dist-es/side-panel/SidePanelHeader.js.map +1 -1
- package/dist-es/side-panel/SidePanelProvider.js +7 -18
- package/dist-es/side-panel/SidePanelProvider.js.map +1 -1
- package/dist-es/side-panel/SidePanelTitle.js.map +1 -1
- package/dist-es/side-panel/SidePanelTrigger.js.map +1 -1
- package/dist-es/side-panel/internal/SidePanelContext.js +1 -3
- package/dist-es/side-panel/internal/SidePanelContext.js.map +1 -1
- package/dist-es/side-panel/internal/useSidePanelTabOrder.js +122 -29
- package/dist-es/side-panel/internal/useSidePanelTabOrder.js.map +1 -1
- package/dist-es/side-panel/useSidePanel.js +7 -7
- package/dist-es/side-panel/useSidePanel.js.map +1 -1
- package/dist-types/side-panel/SidePanel.d.ts +3 -4
- package/dist-types/side-panel/SidePanelContent.d.ts +4 -3
- package/dist-types/side-panel/SidePanelHeader.d.ts +4 -3
- package/dist-types/side-panel/SidePanelTitle.d.ts +1 -2
- package/dist-types/side-panel/SidePanelTrigger.d.ts +2 -3
- package/dist-types/side-panel/internal/SidePanelContext.d.ts +1 -5
- package/dist-types/side-panel/useSidePanel.d.ts +22 -3
- package/package.json +1 -1
|
@@ -3,11 +3,23 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var tabbable = require('tabbable');
|
|
5
5
|
|
|
6
|
+
const ORIGINAL_TABINDEX_ATTR = "data-salt-original-tabindex";
|
|
6
7
|
function isPlainTabKey(event) {
|
|
7
8
|
return event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
8
9
|
}
|
|
9
10
|
function getPanelTabbableElements(panel) {
|
|
10
|
-
|
|
11
|
+
const win = panel.ownerDocument.defaultView;
|
|
12
|
+
const managed = Array.from(
|
|
13
|
+
panel.querySelectorAll(`[${ORIGINAL_TABINDEX_ATTR}]`)
|
|
14
|
+
).filter((element) => {
|
|
15
|
+
if (element.hasAttribute("disabled")) return false;
|
|
16
|
+
if (element.getAttribute("aria-hidden") === "true") return false;
|
|
17
|
+
return !(element.offsetParent === null && (win == null ? void 0 : win.getComputedStyle(element).display) === "none");
|
|
18
|
+
});
|
|
19
|
+
if (managed.length > 0) {
|
|
20
|
+
return managed;
|
|
21
|
+
}
|
|
22
|
+
return tabbable.tabbable(panel, { displayCheck: "none" });
|
|
11
23
|
}
|
|
12
24
|
function focusFirstPanelElement(panel) {
|
|
13
25
|
const [firstTabbableElement] = getPanelTabbableElements(panel);
|
|
@@ -25,6 +37,7 @@ function containsTarget(container, target) {
|
|
|
25
37
|
}
|
|
26
38
|
function getNextTabbableAfterReference(reference, panel) {
|
|
27
39
|
const { body } = reference.ownerDocument;
|
|
40
|
+
if (!body) return void 0;
|
|
28
41
|
const tabbableElements = tabbable.tabbable(body).filter(
|
|
29
42
|
(element) => !panel.contains(element)
|
|
30
43
|
);
|
|
@@ -36,21 +49,100 @@ function getNextTabbableAfterReference(reference, panel) {
|
|
|
36
49
|
}
|
|
37
50
|
return tabbableElements[referenceIndex + 1];
|
|
38
51
|
}
|
|
52
|
+
function detachElement(element) {
|
|
53
|
+
if (!element.hasAttribute(ORIGINAL_TABINDEX_ATTR)) {
|
|
54
|
+
const original = element.getAttribute("tabindex");
|
|
55
|
+
element.setAttribute(ORIGINAL_TABINDEX_ATTR, original ?? "");
|
|
56
|
+
}
|
|
57
|
+
if (element.getAttribute("tabindex") !== "-1") {
|
|
58
|
+
element.setAttribute("tabindex", "-1");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function reattachElement(element) {
|
|
62
|
+
if (!element.hasAttribute(ORIGINAL_TABINDEX_ATTR)) return;
|
|
63
|
+
const original = element.getAttribute(ORIGINAL_TABINDEX_ATTR);
|
|
64
|
+
element.removeAttribute(ORIGINAL_TABINDEX_ATTR);
|
|
65
|
+
if (original === null || original === "") {
|
|
66
|
+
element.removeAttribute("tabindex");
|
|
67
|
+
} else {
|
|
68
|
+
element.setAttribute("tabindex", original);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function detachPanelFromTabOrder(panel) {
|
|
72
|
+
const win = panel.ownerDocument.defaultView ?? window;
|
|
73
|
+
const detach = () => {
|
|
74
|
+
for (const element of tabbable.tabbable(panel, { displayCheck: "none" })) {
|
|
75
|
+
detachElement(element);
|
|
76
|
+
}
|
|
77
|
+
const managed = panel.querySelectorAll(
|
|
78
|
+
`[${ORIGINAL_TABINDEX_ATTR}]`
|
|
79
|
+
);
|
|
80
|
+
for (const element of Array.from(managed)) {
|
|
81
|
+
if (element.getAttribute("tabindex") !== "-1") {
|
|
82
|
+
element.setAttribute("tabindex", "-1");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
detach();
|
|
87
|
+
let scheduled = 0;
|
|
88
|
+
const scheduleDetach = () => {
|
|
89
|
+
if (scheduled) return;
|
|
90
|
+
scheduled = win.requestAnimationFrame(() => {
|
|
91
|
+
scheduled = 0;
|
|
92
|
+
detach();
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
const observer = new MutationObserver(scheduleDetach);
|
|
96
|
+
observer.observe(panel, {
|
|
97
|
+
subtree: true,
|
|
98
|
+
childList: true,
|
|
99
|
+
attributes: true,
|
|
100
|
+
attributeFilter: ["tabindex", "disabled", "contenteditable", "hidden"]
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
if (scheduled) {
|
|
104
|
+
win.cancelAnimationFrame(scheduled);
|
|
105
|
+
}
|
|
106
|
+
observer.disconnect();
|
|
107
|
+
const managed = panel.querySelectorAll(
|
|
108
|
+
`[${ORIGINAL_TABINDEX_ATTR}]`
|
|
109
|
+
);
|
|
110
|
+
for (const element of Array.from(managed)) {
|
|
111
|
+
reattachElement(element);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
39
115
|
function useSidePanelTabOrder(props) {
|
|
40
116
|
const { floating, open, reference } = props;
|
|
41
|
-
const followsReferenceOrder = react.useRef(false);
|
|
42
117
|
react.useEffect(() => {
|
|
43
118
|
if (!open || !reference || !floating) {
|
|
44
|
-
followsReferenceOrder.current = false;
|
|
45
119
|
return;
|
|
46
120
|
}
|
|
47
121
|
const { ownerDocument } = reference;
|
|
122
|
+
let redirectTriggerTab = true;
|
|
123
|
+
let programmaticReferenceFocus = false;
|
|
124
|
+
const releaseDetachment = detachPanelFromTabOrder(floating);
|
|
125
|
+
const onReferenceFocus = (event) => {
|
|
126
|
+
if (programmaticReferenceFocus) {
|
|
127
|
+
programmaticReferenceFocus = false;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const relatedTarget = event.relatedTarget;
|
|
131
|
+
if (relatedTarget && relatedTarget instanceof Node && !(reference.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_PRECEDING)) {
|
|
132
|
+
redirectTriggerTab = false;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
redirectTriggerTab = true;
|
|
136
|
+
};
|
|
48
137
|
const onReferenceKeyDown = (event) => {
|
|
49
138
|
if (event.defaultPrevented || !isPlainTabKey(event) || event.shiftKey) {
|
|
50
139
|
return;
|
|
51
140
|
}
|
|
141
|
+
if (!redirectTriggerTab) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
52
144
|
event.preventDefault();
|
|
53
|
-
|
|
145
|
+
redirectTriggerTab = false;
|
|
54
146
|
focusFirstPanelElement(floating);
|
|
55
147
|
};
|
|
56
148
|
const onFloatingKeyDown = (event) => {
|
|
@@ -59,24 +151,39 @@ function useSidePanelTabOrder(props) {
|
|
|
59
151
|
}
|
|
60
152
|
const panelTabbableElements = getPanelTabbableElements(floating);
|
|
61
153
|
const activeElement = floating.ownerDocument.activeElement;
|
|
154
|
+
const currentIndex = activeElement ? panelTabbableElements.indexOf(activeElement) : -1;
|
|
62
155
|
if (event.shiftKey) {
|
|
63
|
-
|
|
64
|
-
if (activeElement === firstPanelElement && followsReferenceOrder.current) {
|
|
156
|
+
if (currentIndex <= 0) {
|
|
65
157
|
event.preventDefault();
|
|
66
|
-
|
|
158
|
+
redirectTriggerTab = true;
|
|
159
|
+
programmaticReferenceFocus = true;
|
|
67
160
|
reference.focus();
|
|
161
|
+
return;
|
|
68
162
|
}
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
panelTabbableElements[currentIndex - 1].focus();
|
|
69
165
|
return;
|
|
70
166
|
}
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
167
|
+
const atLast = currentIndex === panelTabbableElements.length - 1 || currentIndex === -1 && panelTabbableElements.length === 0;
|
|
168
|
+
if (atLast) {
|
|
169
|
+
event.preventDefault();
|
|
73
170
|
const nextElement = getNextTabbableAfterReference(reference, floating);
|
|
74
171
|
if (nextElement) {
|
|
75
|
-
event.preventDefault();
|
|
76
|
-
followsReferenceOrder.current = false;
|
|
77
172
|
nextElement.focus();
|
|
173
|
+
} else {
|
|
174
|
+
redirectTriggerTab = false;
|
|
175
|
+
programmaticReferenceFocus = true;
|
|
176
|
+
reference.focus();
|
|
78
177
|
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (currentIndex === -1) {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
panelTabbableElements[0].focus();
|
|
183
|
+
return;
|
|
79
184
|
}
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
panelTabbableElements[currentIndex + 1].focus();
|
|
80
187
|
};
|
|
81
188
|
const onDocumentKeyDown = (event) => {
|
|
82
189
|
if (event.defaultPrevented || !isPlainTabKey(event) || !event.shiftKey) {
|
|
@@ -87,32 +194,18 @@ function useSidePanelTabOrder(props) {
|
|
|
87
194
|
return;
|
|
88
195
|
}
|
|
89
196
|
event.preventDefault();
|
|
90
|
-
followsReferenceOrder.current = true;
|
|
91
197
|
focusLastPanelElement(floating);
|
|
92
198
|
};
|
|
93
|
-
|
|
94
|
-
if (containsTarget(reference, event.relatedTarget)) {
|
|
95
|
-
followsReferenceOrder.current = true;
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const nextElement = getNextTabbableAfterReference(reference, floating);
|
|
99
|
-
if (nextElement && containsTarget(nextElement, event.relatedTarget)) {
|
|
100
|
-
followsReferenceOrder.current = true;
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (!containsTarget(floating, event.relatedTarget)) {
|
|
104
|
-
followsReferenceOrder.current = false;
|
|
105
|
-
}
|
|
106
|
-
};
|
|
199
|
+
reference.addEventListener("focus", onReferenceFocus);
|
|
107
200
|
reference.addEventListener("keydown", onReferenceKeyDown);
|
|
108
201
|
floating.addEventListener("keydown", onFloatingKeyDown);
|
|
109
|
-
floating.addEventListener("focusin", onFloatingFocusIn);
|
|
110
202
|
ownerDocument.addEventListener("keydown", onDocumentKeyDown);
|
|
111
203
|
return () => {
|
|
204
|
+
reference.removeEventListener("focus", onReferenceFocus);
|
|
112
205
|
reference.removeEventListener("keydown", onReferenceKeyDown);
|
|
113
206
|
floating.removeEventListener("keydown", onFloatingKeyDown);
|
|
114
|
-
floating.removeEventListener("focusin", onFloatingFocusIn);
|
|
115
207
|
ownerDocument.removeEventListener("keydown", onDocumentKeyDown);
|
|
208
|
+
releaseDetachment();
|
|
116
209
|
};
|
|
117
210
|
}, [floating, open, reference]);
|
|
118
211
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSidePanelTabOrder.js","sources":["../src/side-panel/internal/useSidePanelTabOrder.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { tabbable } from \"tabbable\";\n\ninterface UseSidePanelTabOrderProps {\n floating: HTMLElement | null;\n open: boolean;\n reference: HTMLElement | null;\n}\n\nfunction isPlainTabKey(event: KeyboardEvent) {\n return (\n event.key === \"Tab\" && !event.altKey && !event.ctrlKey && !event.metaKey\n );\n}\n\nfunction getPanelTabbableElements(panel: HTMLElement) {\n return tabbable(panel);\n}\n\nfunction focusFirstPanelElement(panel: HTMLElement) {\n const [firstTabbableElement] = getPanelTabbableElements(panel);\n (firstTabbableElement ?? panel).focus();\n}\n\nfunction focusLastPanelElement(panel: HTMLElement) {\n const tabbableElements = getPanelTabbableElements(panel);\n (tabbableElements[tabbableElements.length - 1] ?? panel).focus();\n}\n\nfunction containsTarget(container: Element, target: EventTarget | null) {\n const targetWindow = container.ownerDocument.defaultView;\n\n return Boolean(\n targetWindow?.Node &&\n target instanceof targetWindow.Node &&\n container.contains(target),\n );\n}\n\nfunction getNextTabbableAfterReference(\n reference: HTMLElement,\n panel: HTMLElement,\n) {\n const { body } = reference.ownerDocument;\n const tabbableElements = tabbable(body).filter(\n (element) => !panel.contains(element),\n );\n const referenceIndex = tabbableElements.findIndex(\n (element) => element === reference || reference.contains(element),\n );\n\n if (referenceIndex === -1) {\n return undefined;\n }\n\n return tabbableElements[referenceIndex + 1];\n}\n\nexport function useSidePanelTabOrder(props: UseSidePanelTabOrderProps) {\n const { floating, open, reference } = props;\n const followsReferenceOrder = useRef(false);\n\n useEffect(() => {\n if (!open || !reference || !floating) {\n followsReferenceOrder.current = false;\n return;\n }\n\n const { ownerDocument } = reference;\n\n const onReferenceKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || !isPlainTabKey(event) || event.shiftKey) {\n return;\n }\n\n event.preventDefault();\n followsReferenceOrder.current = true;\n focusFirstPanelElement(floating);\n };\n\n const onFloatingKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || !isPlainTabKey(event)) {\n return;\n }\n\n const panelTabbableElements = getPanelTabbableElements(floating);\n const activeElement = floating.ownerDocument.activeElement;\n\n if (event.shiftKey) {\n const firstPanelElement = panelTabbableElements[0] ?? floating;\n\n if (\n activeElement === firstPanelElement &&\n followsReferenceOrder.current\n ) {\n event.preventDefault();\n followsReferenceOrder.current = false;\n reference.focus();\n }\n return;\n }\n\n const lastPanelElement =\n panelTabbableElements[panelTabbableElements.length - 1] ?? floating;\n\n if (activeElement === lastPanelElement && followsReferenceOrder.current) {\n const nextElement = getNextTabbableAfterReference(reference, floating);\n\n if (nextElement) {\n event.preventDefault();\n followsReferenceOrder.current = false;\n nextElement.focus();\n }\n }\n };\n\n const onDocumentKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || !isPlainTabKey(event) || !event.shiftKey) {\n return;\n }\n\n const nextElement = getNextTabbableAfterReference(reference, floating);\n if (!nextElement || !containsTarget(nextElement, event.target)) {\n return;\n }\n\n event.preventDefault();\n followsReferenceOrder.current = true;\n focusLastPanelElement(floating);\n };\n\n const onFloatingFocusIn = (event: FocusEvent) => {\n if (containsTarget(reference, event.relatedTarget)) {\n followsReferenceOrder.current = true;\n return;\n }\n\n const nextElement = getNextTabbableAfterReference(reference, floating);\n if (nextElement && containsTarget(nextElement, event.relatedTarget)) {\n followsReferenceOrder.current = true;\n return;\n }\n\n if (!containsTarget(floating, event.relatedTarget)) {\n followsReferenceOrder.current = false;\n }\n };\n\n reference.addEventListener(\"keydown\", onReferenceKeyDown);\n floating.addEventListener(\"keydown\", onFloatingKeyDown);\n floating.addEventListener(\"focusin\", onFloatingFocusIn);\n ownerDocument.addEventListener(\"keydown\", onDocumentKeyDown);\n\n return () => {\n reference.removeEventListener(\"keydown\", onReferenceKeyDown);\n floating.removeEventListener(\"keydown\", onFloatingKeyDown);\n floating.removeEventListener(\"focusin\", onFloatingFocusIn);\n ownerDocument.removeEventListener(\"keydown\", onDocumentKeyDown);\n };\n }, [floating, open, reference]);\n}\n"],"names":["tabbable","useRef","useEffect"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSidePanelTabOrder.js","sources":["../src/side-panel/internal/useSidePanelTabOrder.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { tabbable } from \"tabbable\";\n\ninterface UseSidePanelTabOrderProps {\n floating: HTMLElement | null;\n open: boolean;\n reference: HTMLElement | null;\n}\n\nconst ORIGINAL_TABINDEX_ATTR = \"data-salt-original-tabindex\";\n\nfunction isPlainTabKey(event: KeyboardEvent) {\n return (\n event.key === \"Tab\" && !event.altKey && !event.ctrlKey && !event.metaKey\n );\n}\n\n/**\n * Returns the panel's logical tab sequence. While the panel is open we\n * strip tabindex on descendants, so the sequence is discovered via the\n * data-salt-original-tabindex markers; falls back to a fresh tabbable()\n * scan before detachment runs.\n */\nfunction getPanelTabbableElements(panel: HTMLElement): HTMLElement[] {\n const win = panel.ownerDocument.defaultView;\n const managed = Array.from(\n panel.querySelectorAll<HTMLElement>(`[${ORIGINAL_TABINDEX_ATTR}]`),\n ).filter((element) => {\n if (element.hasAttribute(\"disabled\")) return false;\n if (element.getAttribute(\"aria-hidden\") === \"true\") return false;\n // null offsetParent is also true for fixed/SVG; the display check\n // disambiguates.\n return !(\n element.offsetParent === null &&\n win?.getComputedStyle(element).display === \"none\"\n );\n });\n\n if (managed.length > 0) {\n return managed;\n }\n\n return tabbable(panel, { displayCheck: \"none\" }) as HTMLElement[];\n}\n\nfunction focusFirstPanelElement(panel: HTMLElement) {\n const [firstTabbableElement] = getPanelTabbableElements(panel);\n (firstTabbableElement ?? panel).focus();\n}\n\nfunction focusLastPanelElement(panel: HTMLElement) {\n const tabbableElements = getPanelTabbableElements(panel);\n (tabbableElements[tabbableElements.length - 1] ?? panel).focus();\n}\n\nfunction containsTarget(container: Element, target: EventTarget | null) {\n const targetWindow = container.ownerDocument.defaultView;\n\n return Boolean(\n targetWindow?.Node &&\n target instanceof targetWindow.Node &&\n container.contains(target),\n );\n}\n\n/**\n * Returns the first tabbable element after `reference` in document order,\n * skipping anything inside `panel`.\n */\nfunction getNextTabbableAfterReference(\n reference: HTMLElement,\n panel: HTMLElement,\n) {\n const { body } = reference.ownerDocument;\n if (!body) return undefined;\n const tabbableElements = tabbable(body).filter(\n (element) => !panel.contains(element),\n );\n const referenceIndex = tabbableElements.findIndex(\n (element) => element === reference || reference.contains(element),\n );\n\n if (referenceIndex === -1) {\n return undefined;\n }\n\n return tabbableElements[referenceIndex + 1];\n}\n\n/**\n * Detaches an element from the natural tab sequence, remembering its\n * original tabindex (or its absence) on a data attribute for restoration.\n */\nfunction detachElement(element: HTMLElement) {\n if (!element.hasAttribute(ORIGINAL_TABINDEX_ATTR)) {\n const original = element.getAttribute(\"tabindex\");\n element.setAttribute(ORIGINAL_TABINDEX_ATTR, original ?? \"\");\n }\n // Force re-apply: React may have rewritten the attribute on re-render.\n if (element.getAttribute(\"tabindex\") !== \"-1\") {\n element.setAttribute(\"tabindex\", \"-1\");\n }\n}\n\nfunction reattachElement(element: HTMLElement) {\n if (!element.hasAttribute(ORIGINAL_TABINDEX_ATTR)) return;\n const original = element.getAttribute(ORIGINAL_TABINDEX_ATTR);\n element.removeAttribute(ORIGINAL_TABINDEX_ATTR);\n if (original === null || original === \"\") {\n element.removeAttribute(\"tabindex\");\n } else {\n element.setAttribute(\"tabindex\", original);\n }\n}\n\n/**\n * Removes every tabbable descendant of `panel` from the natural tab order.\n * Returns a cleanup function that restores original tabindex values and\n * disconnects the observer used to track dynamically-added focusables.\n */\nfunction detachPanelFromTabOrder(panel: HTMLElement) {\n const win = panel.ownerDocument.defaultView ?? window;\n\n const detach = () => {\n for (const element of tabbable(panel, { displayCheck: \"none\" })) {\n detachElement(element as HTMLElement);\n }\n // Re-assert tabindex=\"-1\" on already-managed elements (React may\n // have written its prop value back).\n const managed = panel.querySelectorAll<HTMLElement>(\n `[${ORIGINAL_TABINDEX_ATTR}]`,\n );\n for (const element of Array.from(managed)) {\n if (element.getAttribute(\"tabindex\") !== \"-1\") {\n element.setAttribute(\"tabindex\", \"-1\");\n }\n }\n };\n\n detach();\n\n // Coalesce mutation bursts into one frame to avoid a feedback loop\n // between detach()'s tabindex writes and the observer's tabindex filter.\n let scheduled = 0;\n const scheduleDetach = () => {\n if (scheduled) return;\n scheduled = win.requestAnimationFrame(() => {\n scheduled = 0;\n detach();\n });\n };\n\n const observer = new MutationObserver(scheduleDetach);\n\n observer.observe(panel, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeFilter: [\"tabindex\", \"disabled\", \"contenteditable\", \"hidden\"],\n });\n\n return () => {\n if (scheduled) {\n win.cancelAnimationFrame(scheduled);\n }\n observer.disconnect();\n const managed = panel.querySelectorAll<HTMLElement>(\n `[${ORIGINAL_TABINDEX_ATTR}]`,\n );\n for (const element of Array.from(managed)) {\n reattachElement(element);\n }\n };\n}\n\nexport function useSidePanelTabOrder(props: UseSidePanelTabOrderProps) {\n const { floating, open, reference } = props;\n\n useEffect(() => {\n if (!open || !reference || !floating) {\n return;\n }\n\n const { ownerDocument } = reference;\n\n // True when Tab on the trigger should walk into the panel (a \"fresh\n // entry\": focus arrived from before the trigger, or from nowhere).\n // Set false when focus arrives from inside / after the panel, so the\n // user can keep tabbing forward without getting trapped.\n let redirectTriggerTab = true;\n\n // Set true just before we programmatically focus the trigger, so the\n // focusin handler can preserve our explicit redirect intent instead\n // of overwriting it from the relatedTarget heuristic.\n let programmaticReferenceFocus = false;\n\n // Make panel content unreachable via natural Tab — only the trigger\n // flow below can enter.\n const releaseDetachment = detachPanelFromTabOrder(floating);\n\n const onReferenceFocus = (event: FocusEvent) => {\n if (programmaticReferenceFocus) {\n programmaticReferenceFocus = false;\n return;\n }\n\n const relatedTarget = event.relatedTarget as Node | null;\n\n // Focus arrived from inside / after the panel — disable redirect\n // so Tab moves forward naturally and doesn't trap the user.\n if (\n relatedTarget &&\n relatedTarget instanceof Node &&\n !(\n reference.compareDocumentPosition(relatedTarget) &\n Node.DOCUMENT_POSITION_PRECEDING\n )\n ) {\n redirectTriggerTab = false;\n return;\n }\n\n redirectTriggerTab = true;\n };\n\n const onReferenceKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || !isPlainTabKey(event) || event.shiftKey) {\n return;\n }\n\n // Only redirect on a fresh entry; otherwise let the browser advance\n // to the next focusable element naturally.\n if (!redirectTriggerTab) {\n return;\n }\n\n event.preventDefault();\n redirectTriggerTab = false;\n focusFirstPanelElement(floating);\n };\n\n const onFloatingKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || !isPlainTabKey(event)) {\n return;\n }\n\n const panelTabbableElements = getPanelTabbableElements(floating);\n const activeElement = floating.ownerDocument\n .activeElement as HTMLElement | null;\n const currentIndex = activeElement\n ? panelTabbableElements.indexOf(activeElement)\n : -1;\n\n if (event.shiftKey) {\n // At the start of the panel (or on the panel container itself):\n // exit backwards to the trigger and arm the redirect so the next\n // forward Tab walks back in.\n if (currentIndex <= 0) {\n event.preventDefault();\n redirectTriggerTab = true;\n programmaticReferenceFocus = true;\n reference.focus();\n return;\n }\n\n event.preventDefault();\n panelTabbableElements[currentIndex - 1].focus();\n return;\n }\n\n // At the end of the panel: exit forwards to the element after the\n // trigger. Also handles focus on the panel container itself when\n // the panel has no tabbable items.\n const atLast =\n currentIndex === panelTabbableElements.length - 1 ||\n (currentIndex === -1 && panelTabbableElements.length === 0);\n\n if (atLast) {\n event.preventDefault();\n const nextElement = getNextTabbableAfterReference(reference, floating);\n if (nextElement) {\n nextElement.focus();\n } else {\n // Nothing after the trigger — hand focus back to the trigger\n // but disable redirect so the next Tab moves naturally.\n redirectTriggerTab = false;\n programmaticReferenceFocus = true;\n reference.focus();\n }\n return;\n }\n\n // Focus on the panel container or an unmanaged ancestor — route\n // forward into the first managed element.\n if (currentIndex === -1) {\n event.preventDefault();\n panelTabbableElements[0].focus();\n return;\n }\n\n event.preventDefault();\n panelTabbableElements[currentIndex + 1].focus();\n };\n\n const onDocumentKeyDown = (event: KeyboardEvent) => {\n if (event.defaultPrevented || !isPlainTabKey(event) || !event.shiftKey) {\n return;\n }\n\n const nextElement = getNextTabbableAfterReference(reference, floating);\n if (!nextElement || !containsTarget(nextElement, event.target)) {\n return;\n }\n\n event.preventDefault();\n focusLastPanelElement(floating);\n };\n\n reference.addEventListener(\"focus\", onReferenceFocus);\n reference.addEventListener(\"keydown\", onReferenceKeyDown);\n floating.addEventListener(\"keydown\", onFloatingKeyDown);\n ownerDocument.addEventListener(\"keydown\", onDocumentKeyDown);\n\n return () => {\n reference.removeEventListener(\"focus\", onReferenceFocus);\n reference.removeEventListener(\"keydown\", onReferenceKeyDown);\n floating.removeEventListener(\"keydown\", onFloatingKeyDown);\n ownerDocument.removeEventListener(\"keydown\", onDocumentKeyDown);\n releaseDetachment();\n };\n }, [floating, open, reference]);\n}\n"],"names":["tabbable","useEffect"],"mappings":";;;;;AASA,MAAM,sBAAA,GAAyB,6BAAA;AAE/B,SAAS,cAAc,KAAA,EAAsB;AAC3C,EAAA,OACE,KAAA,CAAM,GAAA,KAAQ,KAAA,IAAS,CAAC,KAAA,CAAM,UAAU,CAAC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,OAAA;AAErE;AAQA,SAAS,yBAAyB,KAAA,EAAmC;AACnE,EAAA,MAAM,GAAA,GAAM,MAAM,aAAA,CAAc,WAAA;AAChC,EAAA,MAAM,UAAU,KAAA,CAAM,IAAA;AAAA,IACpB,KAAA,CAAM,gBAAA,CAA8B,CAAA,CAAA,EAAI,sBAAsB,CAAA,CAAA,CAAG;AAAA,GACnE,CAAE,MAAA,CAAO,CAAC,OAAA,KAAY;AACpB,IAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA,EAAG,OAAO,KAAA;AAC7C,IAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,aAAa,CAAA,KAAM,QAAQ,OAAO,KAAA;AAG3D,IAAA,OAAO,EACL,OAAA,CAAQ,YAAA,KAAiB,SACzB,GAAA,IAAA,IAAA,GAAA,MAAA,GAAA,GAAA,CAAK,gBAAA,CAAiB,SAAS,OAAA,MAAY,MAAA,CAAA;AAAA,EAE/C,CAAC,CAAA;AAED,EAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,OAAOA,iBAAA,CAAS,KAAA,EAAO,EAAE,YAAA,EAAc,QAAQ,CAAA;AACjD;AAEA,SAAS,uBAAuB,KAAA,EAAoB;AAClD,EAAA,MAAM,CAAC,oBAAoB,CAAA,GAAI,wBAAA,CAAyB,KAAK,CAAA;AAC7D,EAAA,CAAC,oBAAA,IAAwB,OAAO,KAAA,EAAM;AACxC;AAEA,SAAS,sBAAsB,KAAA,EAAoB;AACjD,EAAA,MAAM,gBAAA,GAAmB,yBAAyB,KAAK,CAAA;AACvD,EAAA,CAAC,iBAAiB,gBAAA,CAAiB,MAAA,GAAS,CAAC,CAAA,IAAK,OAAO,KAAA,EAAM;AACjE;AAEA,SAAS,cAAA,CAAe,WAAoB,MAAA,EAA4B;AACtE,EAAA,MAAM,YAAA,GAAe,UAAU,aAAA,CAAc,WAAA;AAE7C,EAAA,OAAO,OAAA;AAAA,IAAA,CACL,6CAAc,IAAA,KACZ,MAAA,YAAkB,aAAa,IAAA,IAC/B,SAAA,CAAU,SAAS,MAAM;AAAA,GAC7B;AACF;AAMA,SAAS,6BAAA,CACP,WACA,KAAA,EACA;AACA,EAAA,MAAM,EAAE,IAAA,EAAK,GAAI,SAAA,CAAU,aAAA;AAC3B,EAAA,IAAI,CAAC,MAAM,OAAO,MAAA;AAClB,EAAA,MAAM,gBAAA,GAAmBA,iBAAA,CAAS,IAAI,CAAA,CAAE,MAAA;AAAA,IACtC,CAAC,OAAA,KAAY,CAAC,KAAA,CAAM,SAAS,OAAO;AAAA,GACtC;AACA,EAAA,MAAM,iBAAiB,gBAAA,CAAiB,SAAA;AAAA,IACtC,CAAC,OAAA,KAAY,OAAA,KAAY,SAAA,IAAa,SAAA,CAAU,SAAS,OAAO;AAAA,GAClE;AAEA,EAAA,IAAI,mBAAmB,EAAA,EAAI;AACzB,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,gBAAA,CAAiB,iBAAiB,CAAC,CAAA;AAC5C;AAMA,SAAS,cAAc,OAAA,EAAsB;AAC3C,EAAA,IAAI,CAAC,OAAA,CAAQ,YAAA,CAAa,sBAAsB,CAAA,EAAG;AACjD,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA;AAChD,IAAA,OAAA,CAAQ,YAAA,CAAa,sBAAA,EAAwB,QAAA,IAAY,EAAE,CAAA;AAAA,EAC7D;AAEA,EAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA,KAAM,IAAA,EAAM;AAC7C,IAAA,OAAA,CAAQ,YAAA,CAAa,YAAY,IAAI,CAAA;AAAA,EACvC;AACF;AAEA,SAAS,gBAAgB,OAAA,EAAsB;AAC7C,EAAA,IAAI,CAAC,OAAA,CAAQ,YAAA,CAAa,sBAAsB,CAAA,EAAG;AACnD,EAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,YAAA,CAAa,sBAAsB,CAAA;AAC5D,EAAA,OAAA,CAAQ,gBAAgB,sBAAsB,CAAA;AAC9C,EAAA,IAAI,QAAA,KAAa,IAAA,IAAQ,QAAA,KAAa,EAAA,EAAI;AACxC,IAAA,OAAA,CAAQ,gBAAgB,UAAU,CAAA;AAAA,EACpC,CAAA,MAAO;AACL,IAAA,OAAA,CAAQ,YAAA,CAAa,YAAY,QAAQ,CAAA;AAAA,EAC3C;AACF;AAOA,SAAS,wBAAwB,KAAA,EAAoB;AACnD,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,aAAA,CAAc,WAAA,IAAe,MAAA;AAE/C,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,KAAA,MAAW,WAAWA,iBAAA,CAAS,KAAA,EAAO,EAAE,YAAA,EAAc,MAAA,EAAQ,CAAA,EAAG;AAC/D,MAAA,aAAA,CAAc,OAAsB,CAAA;AAAA,IACtC;AAGA,IAAA,MAAM,UAAU,KAAA,CAAM,gBAAA;AAAA,MACpB,IAAI,sBAAsB,CAAA,CAAA;AAAA,KAC5B;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,KAAA,CAAM,IAAA,CAAK,OAAO,CAAA,EAAG;AACzC,MAAA,IAAI,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA,KAAM,IAAA,EAAM;AAC7C,QAAA,OAAA,CAAQ,YAAA,CAAa,YAAY,IAAI,CAAA;AAAA,MACvC;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAA,EAAO;AAIP,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,MAAM,iBAAiB,MAAM;AAC3B,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,SAAA,GAAY,GAAA,CAAI,sBAAsB,MAAM;AAC1C,MAAA,SAAA,GAAY,CAAA;AACZ,MAAA,MAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,MAAM,QAAA,GAAW,IAAI,gBAAA,CAAiB,cAAc,CAAA;AAEpD,EAAA,QAAA,CAAS,QAAQ,KAAA,EAAO;AAAA,IACtB,OAAA,EAAS,IAAA;AAAA,IACT,SAAA,EAAW,IAAA;AAAA,IACX,UAAA,EAAY,IAAA;AAAA,IACZ,eAAA,EAAiB,CAAC,UAAA,EAAY,UAAA,EAAY,mBAAmB,QAAQ;AAAA,GACtE,CAAA;AAED,EAAA,OAAO,MAAM;AACX,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,GAAA,CAAI,qBAAqB,SAAS,CAAA;AAAA,IACpC;AACA,IAAA,QAAA,CAAS,UAAA,EAAW;AACpB,IAAA,MAAM,UAAU,KAAA,CAAM,gBAAA;AAAA,MACpB,IAAI,sBAAsB,CAAA,CAAA;AAAA,KAC5B;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,KAAA,CAAM,IAAA,CAAK,OAAO,CAAA,EAAG;AACzC,MAAA,eAAA,CAAgB,OAAO,CAAA;AAAA,IACzB;AAAA,EACF,CAAA;AACF;AAEO,SAAS,qBAAqB,KAAA,EAAkC;AACrE,EAAA,MAAM,EAAE,QAAA,EAAU,IAAA,EAAM,SAAA,EAAU,GAAI,KAAA;AAEtC,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,IAAA,IAAQ,CAAC,SAAA,IAAa,CAAC,QAAA,EAAU;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,EAAE,eAAc,GAAI,SAAA;AAM1B,IAAA,IAAI,kBAAA,GAAqB,IAAA;AAKzB,IAAA,IAAI,0BAAA,GAA6B,KAAA;AAIjC,IAAA,MAAM,iBAAA,GAAoB,wBAAwB,QAAQ,CAAA;AAE1D,IAAA,MAAM,gBAAA,GAAmB,CAAC,KAAA,KAAsB;AAC9C,MAAA,IAAI,0BAAA,EAA4B;AAC9B,QAAA,0BAAA,GAA6B,KAAA;AAC7B,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,gBAAgB,KAAA,CAAM,aAAA;AAI5B,MAAA,IACE,aAAA,IACA,yBAAyB,IAAA,IACzB,EACE,UAAU,uBAAA,CAAwB,aAAa,CAAA,GAC/C,IAAA,CAAK,2BAAA,CAAA,EAEP;AACA,QAAA,kBAAA,GAAqB,KAAA;AACrB,QAAA;AAAA,MACF;AAEA,MAAA,kBAAA,GAAqB,IAAA;AAAA,IACvB,CAAA;AAEA,IAAA,MAAM,kBAAA,GAAqB,CAAC,KAAA,KAAyB;AACnD,MAAA,IAAI,MAAM,gBAAA,IAAoB,CAAC,cAAc,KAAK,CAAA,IAAK,MAAM,QAAA,EAAU;AACrE,QAAA;AAAA,MACF;AAIA,MAAA,IAAI,CAAC,kBAAA,EAAoB;AACvB,QAAA;AAAA,MACF;AAEA,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,kBAAA,GAAqB,KAAA;AACrB,MAAA,sBAAA,CAAuB,QAAQ,CAAA;AAAA,IACjC,CAAA;AAEA,IAAA,MAAM,iBAAA,GAAoB,CAAC,KAAA,KAAyB;AAClD,MAAA,IAAI,KAAA,CAAM,gBAAA,IAAoB,CAAC,aAAA,CAAc,KAAK,CAAA,EAAG;AACnD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,qBAAA,GAAwB,yBAAyB,QAAQ,CAAA;AAC/D,MAAA,MAAM,aAAA,GAAgB,SAAS,aAAA,CAC5B,aAAA;AACH,MAAA,MAAM,YAAA,GAAe,aAAA,GACjB,qBAAA,CAAsB,OAAA,CAAQ,aAAa,CAAA,GAC3C,EAAA;AAEJ,MAAA,IAAI,MAAM,QAAA,EAAU;AAIlB,QAAA,IAAI,gBAAgB,CAAA,EAAG;AACrB,UAAA,KAAA,CAAM,cAAA,EAAe;AACrB,UAAA,kBAAA,GAAqB,IAAA;AACrB,UAAA,0BAAA,GAA6B,IAAA;AAC7B,UAAA,SAAA,CAAU,KAAA,EAAM;AAChB,UAAA;AAAA,QACF;AAEA,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,qBAAA,CAAsB,YAAA,GAAe,CAAC,CAAA,CAAE,KAAA,EAAM;AAC9C,QAAA;AAAA,MACF;AAKA,MAAA,MAAM,MAAA,GACJ,iBAAiB,qBAAA,CAAsB,MAAA,GAAS,KAC/C,YAAA,KAAiB,EAAA,IAAM,sBAAsB,MAAA,KAAW,CAAA;AAE3D,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,MAAM,WAAA,GAAc,6BAAA,CAA8B,SAAA,EAAW,QAAQ,CAAA;AACrE,QAAA,IAAI,WAAA,EAAa;AACf,UAAA,WAAA,CAAY,KAAA,EAAM;AAAA,QACpB,CAAA,MAAO;AAGL,UAAA,kBAAA,GAAqB,KAAA;AACrB,UAAA,0BAAA,GAA6B,IAAA;AAC7B,UAAA,SAAA,CAAU,KAAA,EAAM;AAAA,QAClB;AACA,QAAA;AAAA,MACF;AAIA,MAAA,IAAI,iBAAiB,EAAA,EAAI;AACvB,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,qBAAA,CAAsB,CAAC,EAAE,KAAA,EAAM;AAC/B,QAAA;AAAA,MACF;AAEA,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,qBAAA,CAAsB,YAAA,GAAe,CAAC,CAAA,CAAE,KAAA,EAAM;AAAA,IAChD,CAAA;AAEA,IAAA,MAAM,iBAAA,GAAoB,CAAC,KAAA,KAAyB;AAClD,MAAA,IAAI,KAAA,CAAM,oBAAoB,CAAC,aAAA,CAAc,KAAK,CAAA,IAAK,CAAC,MAAM,QAAA,EAAU;AACtE,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,WAAA,GAAc,6BAAA,CAA8B,SAAA,EAAW,QAAQ,CAAA;AACrE,MAAA,IAAI,CAAC,WAAA,IAAe,CAAC,eAAe,WAAA,EAAa,KAAA,CAAM,MAAM,CAAA,EAAG;AAC9D,QAAA;AAAA,MACF;AAEA,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,qBAAA,CAAsB,QAAQ,CAAA;AAAA,IAChC,CAAA;AAEA,IAAA,SAAA,CAAU,gBAAA,CAAiB,SAAS,gBAAgB,CAAA;AACpD,IAAA,SAAA,CAAU,gBAAA,CAAiB,WAAW,kBAAkB,CAAA;AACxD,IAAA,QAAA,CAAS,gBAAA,CAAiB,WAAW,iBAAiB,CAAA;AACtD,IAAA,aAAA,CAAc,gBAAA,CAAiB,WAAW,iBAAiB,CAAA;AAE3D,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,CAAU,mBAAA,CAAoB,SAAS,gBAAgB,CAAA;AACvD,MAAA,SAAA,CAAU,mBAAA,CAAoB,WAAW,kBAAkB,CAAA;AAC3D,MAAA,QAAA,CAAS,mBAAA,CAAoB,WAAW,iBAAiB,CAAA;AACzD,MAAA,aAAA,CAAc,mBAAA,CAAoB,WAAW,iBAAiB,CAAA;AAC9D,MAAA,iBAAA,EAAkB;AAAA,IACpB,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,QAAA,EAAU,IAAA,EAAM,SAAS,CAAC,CAAA;AAChC;;;;"}
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var core = require('@salt-ds/core');
|
|
4
4
|
var SidePanelContext = require('./internal/SidePanelContext.js');
|
|
5
|
+
require('react');
|
|
5
6
|
require('tabbable');
|
|
6
7
|
|
|
7
8
|
function useSidePanel() {
|
|
8
9
|
const { openState, setOpen, setReference, panelId } = SidePanelContext.useSidePanelContext();
|
|
9
|
-
const getTriggerProps =
|
|
10
|
+
const getTriggerProps = core.useEventCallback(
|
|
10
11
|
(userProps) => {
|
|
11
12
|
const userOnClick = userProps == null ? void 0 : userProps.onClick;
|
|
13
|
+
const userRef = userProps == null ? void 0 : userProps.ref;
|
|
12
14
|
return {
|
|
13
15
|
"aria-expanded": openState,
|
|
14
16
|
"aria-controls": openState ? panelId : void 0,
|
|
15
17
|
...userProps,
|
|
16
|
-
onClick: (
|
|
17
|
-
userOnClick == null ? void 0 : userOnClick(
|
|
18
|
+
onClick: (event) => {
|
|
19
|
+
userOnClick == null ? void 0 : userOnClick(event);
|
|
18
20
|
setOpen(!openState);
|
|
19
21
|
},
|
|
20
22
|
ref: (node) => {
|
|
21
23
|
setReference(node);
|
|
22
|
-
const userRef = userProps == null ? void 0 : userProps.ref;
|
|
23
24
|
if (typeof userRef === "function") {
|
|
24
25
|
userRef(node);
|
|
25
26
|
} else if (userRef && typeof userRef === "object" && "current" in userRef) {
|
|
@@ -27,8 +28,7 @@ function useSidePanel() {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
};
|
|
30
|
-
}
|
|
31
|
-
[openState, panelId, setReference, setOpen]
|
|
31
|
+
}
|
|
32
32
|
);
|
|
33
33
|
return {
|
|
34
34
|
openState,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSidePanel.js","sources":["../src/side-panel/useSidePanel.ts"],"sourcesContent":["import {\n
|
|
1
|
+
{"version":3,"file":"useSidePanel.js","sources":["../src/side-panel/useSidePanel.ts"],"sourcesContent":["import { useEventCallback } from \"@salt-ds/core\";\nimport type {\n ComponentPropsWithoutRef,\n MouseEvent,\n MutableRefObject,\n RefCallback,\n} from \"react\";\nimport { useSidePanelContext } from \"./internal\";\n\nexport interface SidePanelTriggerExtraProps {\n /**\n * Optional ref to forward alongside the focus-return registration.\n */\n ref?:\n | RefCallback<HTMLElement | null>\n | MutableRefObject<HTMLElement | null>\n | null;\n /**\n * Click handler. Runs before the built-in toggle so consumers can\n * preventDefault to skip the toggle.\n */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n}\n\nexport type SidePanelTriggerPropsResult = ComponentPropsWithoutRef<\"button\"> & {\n \"aria-expanded\": boolean;\n \"aria-controls\": string | undefined;\n ref: RefCallback<HTMLElement | null>;\n};\n\nexport interface SidePanelValue {\n /**\n * Whether the side panel is currently open.\n */\n openState: boolean;\n /**\n * Sets the open state of the panel.\n */\n setOpen: (open: boolean) => void;\n /**\n * Props getter for a trigger element outside of `SidePanelTrigger`.\n * Returns `aria-expanded`, `aria-controls`, a `ref` (for focus-return),\n * and an `onClick` that toggles the panel.\n *\n * Spread the result onto a Button to get full trigger behavior:\n * ```tsx\n * <Button {...getTriggerProps()}>Toggle panel</Button>\n * ```\n *\n * You can pass additional props which are merged in. If you provide your\n * own `onClick`, it runs before the built-in toggle. If you provide your\n * own `ref`, it is forwarded alongside the internal focus-return ref.\n *\n * For multi-trigger scenarios (e.g. table rows), use `setTriggerRef` and\n * manage ARIA attributes yourself instead.\n */\n getTriggerProps: (\n userProps?: SidePanelTriggerExtraProps,\n ) => SidePanelTriggerPropsResult;\n /**\n * Registers the element that should receive focus when the panel closes.\n * Use this in multi-trigger scenarios (e.g. table rows) where each trigger\n * needs explicit control over which element is the reference. Pass `null`\n * to clear the previously-registered trigger.\n */\n setTriggerRef: (element: HTMLElement | null) => void;\n /**\n * The panel's DOM id. Use this for `aria-controls` in multi-trigger\n * scenarios where you manage ARIA attributes yourself.\n */\n panelId: string | undefined;\n}\n\nexport function useSidePanel(): SidePanelValue {\n const { openState, setOpen, setReference, panelId } = useSidePanelContext();\n\n // useEventCallback gives a stable identity so consumers can memoise\n // around getTriggerProps without it churning on open/close.\n const getTriggerProps = useEventCallback(\n (userProps?: SidePanelTriggerExtraProps): SidePanelTriggerPropsResult => {\n const userOnClick = userProps?.onClick;\n const userRef = userProps?.ref;\n\n return {\n \"aria-expanded\": openState,\n \"aria-controls\": openState ? panelId : undefined,\n ...userProps,\n onClick: (event: MouseEvent<HTMLButtonElement>) => {\n userOnClick?.(event);\n setOpen(!openState);\n },\n ref: (node: HTMLElement | null) => {\n setReference(node);\n if (typeof userRef === \"function\") {\n userRef(node);\n } else if (\n userRef &&\n typeof userRef === \"object\" &&\n \"current\" in userRef\n ) {\n userRef.current = node;\n }\n },\n };\n },\n );\n\n return {\n openState,\n setOpen,\n getTriggerProps,\n setTriggerRef: setReference,\n panelId,\n };\n}\n"],"names":["useSidePanelContext","useEventCallback"],"mappings":";;;;;;;AAyEO,SAAS,YAAA,GAA+B;AAC7C,EAAA,MAAM,EAAE,SAAA,EAAW,OAAA,EAAS,YAAA,EAAc,OAAA,KAAYA,oCAAA,EAAoB;AAI1E,EAAA,MAAM,eAAA,GAAkBC,qBAAA;AAAA,IACtB,CAAC,SAAA,KAAwE;AACvE,MAAA,MAAM,cAAc,SAAA,IAAA,IAAA,GAAA,MAAA,GAAA,SAAA,CAAW,OAAA;AAC/B,MAAA,MAAM,UAAU,SAAA,IAAA,IAAA,GAAA,MAAA,GAAA,SAAA,CAAW,GAAA;AAE3B,MAAA,OAAO;AAAA,QACL,eAAA,EAAiB,SAAA;AAAA,QACjB,eAAA,EAAiB,YAAY,OAAA,GAAU,MAAA;AAAA,QACvC,GAAG,SAAA;AAAA,QACH,OAAA,EAAS,CAAC,KAAA,KAAyC;AACjD,UAAA,WAAA,IAAA,IAAA,GAAA,MAAA,GAAA,WAAA,CAAc,KAAA,CAAA;AACd,UAAA,OAAA,CAAQ,CAAC,SAAS,CAAA;AAAA,QACpB,CAAA;AAAA,QACA,GAAA,EAAK,CAAC,IAAA,KAA6B;AACjC,UAAA,YAAA,CAAa,IAAI,CAAA;AACjB,UAAA,IAAI,OAAO,YAAY,UAAA,EAAY;AACjC,YAAA,OAAA,CAAQ,IAAI,CAAA;AAAA,UACd,WACE,OAAA,IACA,OAAO,OAAA,KAAY,QAAA,IACnB,aAAa,OAAA,EACb;AACA,YAAA,OAAA,CAAQ,OAAA,GAAU,IAAA;AAAA,UACpB;AAAA,QACF;AAAA,OACF;AAAA,IACF;AAAA,GACF;AAEA,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,OAAA;AAAA,IACA,eAAA;AAAA,IACA,aAAA,EAAe,YAAA;AAAA,IACf;AAAA,GACF;AACF;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var css_248z = ".saltSidePanel {\n
|
|
1
|
+
var css_248z = ".saltSidePanel {\n --saltSidePanel-width: 300px;\n --sidePanel-border: var(--salt-size-fixed-100) var(--salt-borderStyle-solid) var(--sidePanel-borderColor, var(--salt-container-primary-borderColor));\n --saltSidePanel-padding: var(--salt-spacing-300);\n /* Outer wrapper. Idle: a normal flex column whose width comes from\n --saltSidePanel-width (supports any CSS length, including %).\n During enter/exit animation: animates `width` between 0 and the\n panel width and clips the absolutely-positioned inner via\n `contain: paint`. The inner uses a JS-resolved px width during\n animation (--saltSidePanel-animation-width) so percentage widths\n don't compound against the animating outer. */\n position: relative;\n display: flex;\n flex-direction: column;\n flex: none;\n width: var(--saltSidePanel-width);\n height: 100%;\n min-height: 0;\n align-self: stretch;\n}\n\n.saltSidePanel-primary {\n --sidePanel-background: var(--salt-container-primary-background);\n --sidePanel-borderColor: var(--salt-container-primary-borderColor);\n}\n\n.saltSidePanel-secondary {\n --sidePanel-background: var(--salt-container-secondary-background);\n --sidePanel-borderColor: var(--salt-container-secondary-borderColor);\n}\n\n.saltSidePanel-tertiary {\n --sidePanel-background: var(--salt-container-tertiary-background);\n --sidePanel-borderColor: var(--salt-container-tertiary-borderColor);\n}\n\n.saltSidePanel-none {\n --sidePanel-background: none;\n --saltSidePanel-padding: var(--salt-spacing-200);\n}\n\n.saltSidePanel-inner {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n padding: var(--saltSidePanel-padding);\n width: 100%;\n background-color: var(--sidePanel-background, var(--salt-container-primary-background));\n}\n\n.saltSidePanel-left .saltSidePanel-inner {\n border-right: var(--sidePanel-border);\n}\n\n.saltSidePanel-right .saltSidePanel-inner {\n border-left: var(--sidePanel-border);\n}\n\n.saltSidePanel-none .saltSidePanel-inner {\n border: none;\n}\n\n/* Animation: outer becomes the gap + clip; inner is absolutely positioned\n and anchored to the gap's GROWING edge so the leading edge tracks the\n gap's expansion — content visibly pushes the sibling rather than being\n revealed in place. Left grows rightward → anchor inner right; right\n grows leftward → anchor inner left.\n\n During animation the outer's width is interpolating, so the inner can't\n rely on --saltSidePanel-width (which may be a percentage that would\n resolve against the animating outer). Instead the JS layer measures the\n resolved width once and exposes it as --saltSidePanel-animation-width\n (a px value) for the inner to use. */\n.saltSidePanel-enterAnimation,\n.saltSidePanel-exitAnimation {\n contain: paint;\n}\n\n.saltSidePanel-enterAnimation .saltSidePanel-inner,\n.saltSidePanel-exitAnimation .saltSidePanel-inner {\n position: absolute;\n top: 0;\n bottom: 0;\n flex: none;\n width: var(--saltSidePanel-animation-width, var(--saltSidePanel-width));\n}\n\n.saltSidePanel-left.saltSidePanel-enterAnimation .saltSidePanel-inner,\n.saltSidePanel-left.saltSidePanel-exitAnimation .saltSidePanel-inner {\n right: 0;\n}\n\n.saltSidePanel-right.saltSidePanel-enterAnimation .saltSidePanel-inner,\n.saltSidePanel-right.saltSidePanel-exitAnimation .saltSidePanel-inner {\n left: 0;\n}\n\n.saltSidePanel-exitAnimation .saltSidePanel-inner {\n pointer-events: none;\n}\n\n.saltSidePanel-enterAnimation {\n animation: saltSidePanel-open var(--salt-duration-perceptible) var(--salt-animation-timing-function);\n}\n\n.saltSidePanel-exitAnimation {\n animation: saltSidePanel-close var(--salt-duration-perceptible) var(--salt-animation-timing-function) both;\n}\n\n@keyframes saltSidePanel-open {\n from {\n width: 0;\n }\n}\n\n@keyframes saltSidePanel-close {\n to {\n width: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .saltSidePanel-enterAnimation,\n .saltSidePanel-exitAnimation {\n animation: none;\n }\n}\n";
|
|
2
2
|
|
|
3
3
|
export { css_248z as default };
|
|
4
4
|
//# sourceMappingURL=SidePanel.css.js.map
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { jsx } from 'react/jsx-runtime';
|
|
2
|
-
import {
|
|
3
|
-
import { makePrefixer, useId, useFloatingUI, useForkRef, useIsomorphicLayoutEffect } from '@salt-ds/core';
|
|
2
|
+
import { makePrefixer, useId, useEventCallback, useForkRef, useIsomorphicLayoutEffect, usePrevious } from '@salt-ds/core';
|
|
4
3
|
import { useComponentCssInjection } from '@salt-ds/styles';
|
|
5
4
|
import { useWindow } from '@salt-ds/window';
|
|
6
5
|
import { clsx } from 'clsx';
|
|
7
6
|
import { forwardRef, useMemo, useState, useRef, useEffect } from 'react';
|
|
7
|
+
import { tabbable } from 'tabbable';
|
|
8
8
|
import { useSidePanelContext, SidePanelContext } from './internal/SidePanelContext.js';
|
|
9
|
-
import 'tabbable';
|
|
10
9
|
import css_248z from './SidePanel.css.js';
|
|
11
10
|
|
|
12
11
|
const withBaseName = makePrefixer("saltSidePanel");
|
|
@@ -25,7 +24,7 @@ const SidePanel = forwardRef(
|
|
|
25
24
|
...rest
|
|
26
25
|
} = props;
|
|
27
26
|
const sidePanelContext = useSidePanelContext();
|
|
28
|
-
const { openState,
|
|
27
|
+
const { openState, setFloating, setPanelId, titleId } = sidePanelContext;
|
|
29
28
|
const positionedContext = useMemo(
|
|
30
29
|
() => ({ ...sidePanelContext, position }),
|
|
31
30
|
[sidePanelContext, position]
|
|
@@ -34,43 +33,94 @@ const SidePanel = forwardRef(
|
|
|
34
33
|
const [showComponent, setShowComponent] = useState(openState);
|
|
35
34
|
const [animating, setAnimating] = useState(false);
|
|
36
35
|
const shouldAnimateOpen = useRef(!openState);
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
if (!openState) return false;
|
|
40
|
-
const reference = floatingRootContext.elements.reference;
|
|
41
|
-
if (!(reference instanceof Element)) return true;
|
|
42
|
-
const activeElement = (_a = reference.ownerDocument) == null ? void 0 : _a.activeElement;
|
|
43
|
-
return !activeElement || !reference.contains(activeElement);
|
|
44
|
-
});
|
|
36
|
+
const initialMountRef = useRef(true);
|
|
37
|
+
const panelRef = useRef(null);
|
|
45
38
|
const targetWindow = useWindow();
|
|
46
39
|
useComponentCssInjection({
|
|
47
40
|
testId: "salt-side-panel",
|
|
48
41
|
css: css_248z,
|
|
49
42
|
window: targetWindow
|
|
50
43
|
});
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
});
|
|
54
|
-
const handleRef = useForkRef(setFloating, ref);
|
|
55
|
-
const handleAnimationEnd = (event) => {
|
|
56
|
-
onAnimationEnd == null ? void 0 : onAnimationEnd(event);
|
|
57
|
-
if (event.currentTarget !== event.target || disableAnimation) return;
|
|
58
|
-
setAnimating(false);
|
|
44
|
+
const initialFocusDoneRef = useRef(false);
|
|
45
|
+
useEffect(() => {
|
|
59
46
|
if (!openState) {
|
|
60
|
-
|
|
47
|
+
initialFocusDoneRef.current = false;
|
|
48
|
+
}
|
|
49
|
+
}, [openState]);
|
|
50
|
+
const handleInitialFocus = useEventCallback((el) => {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
if (!el || !openState || initialFocusDoneRef.current) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (initialMountRef.current) {
|
|
56
|
+
const reference2 = sidePanelContext.floatingRootContext.elements.reference;
|
|
57
|
+
const activeElement = reference2 instanceof Element ? (_a = reference2.ownerDocument) == null ? void 0 : _a.activeElement : null;
|
|
58
|
+
const focusCameFromTrigger = reference2 instanceof Element && activeElement instanceof Node && reference2.contains(activeElement);
|
|
59
|
+
if (!focusCameFromTrigger) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
initialMountRef.current = false;
|
|
63
|
+
}
|
|
64
|
+
initialFocusDoneRef.current = true;
|
|
65
|
+
const raf = ((_b = el.ownerDocument.defaultView) == null ? void 0 : _b.requestAnimationFrame) ?? (targetWindow == null ? void 0 : targetWindow.requestAnimationFrame) ?? requestAnimationFrame;
|
|
66
|
+
raf(() => {
|
|
67
|
+
if (!el.isConnected) return;
|
|
68
|
+
const focusTarget = resolveInitialFocusTarget(el, initialFocus);
|
|
69
|
+
focusTarget == null ? void 0 : focusTarget.focus();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const setPanelEl = useEventCallback((el) => {
|
|
73
|
+
panelRef.current = el;
|
|
74
|
+
setFloating(el);
|
|
75
|
+
handleInitialFocus(el);
|
|
76
|
+
});
|
|
77
|
+
const handleRef = useForkRef(setPanelEl, ref);
|
|
78
|
+
useIsomorphicLayoutEffect(() => {
|
|
79
|
+
if (!animating || disableAnimation) return;
|
|
80
|
+
const panel = panelRef.current;
|
|
81
|
+
if (!panel) return;
|
|
82
|
+
const previousAnimation = panel.style.animation;
|
|
83
|
+
panel.style.animation = "none";
|
|
84
|
+
const widthPx = panel.getBoundingClientRect().width;
|
|
85
|
+
panel.style.animation = previousAnimation;
|
|
86
|
+
panel.style.setProperty(
|
|
87
|
+
"--saltSidePanel-animation-width",
|
|
88
|
+
`${widthPx}px`
|
|
89
|
+
);
|
|
90
|
+
return () => {
|
|
91
|
+
panel.style.removeProperty("--saltSidePanel-animation-width");
|
|
92
|
+
};
|
|
93
|
+
}, [animating, disableAnimation]);
|
|
94
|
+
const handleAnimationEnd = useEventCallback(
|
|
95
|
+
(event) => {
|
|
96
|
+
onAnimationEnd == null ? void 0 : onAnimationEnd(event);
|
|
97
|
+
if (event.currentTarget !== event.target || disableAnimation) return;
|
|
98
|
+
setAnimating(false);
|
|
99
|
+
if (!openState) {
|
|
100
|
+
setShowComponent(false);
|
|
101
|
+
}
|
|
61
102
|
}
|
|
62
|
-
|
|
103
|
+
);
|
|
63
104
|
useEffect(() => {
|
|
64
105
|
setPanelId(id);
|
|
65
106
|
return () => {
|
|
66
107
|
setPanelId(void 0);
|
|
67
108
|
};
|
|
68
109
|
}, [id, setPanelId]);
|
|
110
|
+
const reference = sidePanelContext.floatingRootContext.elements.reference;
|
|
111
|
+
const previousOpenState = usePrevious(openState, [openState], false);
|
|
69
112
|
useEffect(() => {
|
|
70
|
-
if (!openState)
|
|
71
|
-
|
|
113
|
+
if (!previousOpenState || openState) return;
|
|
114
|
+
const panel = panelRef.current;
|
|
115
|
+
if (!(reference instanceof HTMLElement)) return;
|
|
116
|
+
const doc = reference.ownerDocument;
|
|
117
|
+
const active = doc == null ? void 0 : doc.activeElement;
|
|
118
|
+
const focusInsidePanel = panel && active instanceof Node && panel.contains(active);
|
|
119
|
+
const focusOnBody = active === (doc == null ? void 0 : doc.body) || active == null;
|
|
120
|
+
if (focusInsidePanel || focusOnBody) {
|
|
121
|
+
reference.focus();
|
|
72
122
|
}
|
|
73
|
-
}, [openState]);
|
|
123
|
+
}, [openState, previousOpenState, reference]);
|
|
74
124
|
useIsomorphicLayoutEffect(() => {
|
|
75
125
|
var _a, _b;
|
|
76
126
|
if (disableAnimation) {
|
|
@@ -104,8 +154,7 @@ const SidePanel = forwardRef(
|
|
|
104
154
|
}
|
|
105
155
|
}, [openState, targetWindow, disableAnimation]);
|
|
106
156
|
if (!showComponent) return null;
|
|
107
|
-
|
|
108
|
-
const panelDiv = /* @__PURE__ */ jsx(
|
|
157
|
+
return /* @__PURE__ */ jsx(
|
|
109
158
|
"div",
|
|
110
159
|
{
|
|
111
160
|
role: "region",
|
|
@@ -128,22 +177,19 @@ const SidePanel = forwardRef(
|
|
|
128
177
|
children: /* @__PURE__ */ jsx(SidePanelContext.Provider, { value: positionedContext, children: /* @__PURE__ */ jsx("div", { className: withBaseName("inner"), children }) })
|
|
129
178
|
}
|
|
130
179
|
);
|
|
131
|
-
if (openState || animating) {
|
|
132
|
-
return /* @__PURE__ */ jsx(
|
|
133
|
-
FloatingFocusManager,
|
|
134
|
-
{
|
|
135
|
-
context,
|
|
136
|
-
modal: false,
|
|
137
|
-
initialFocus: resolvedInitialFocus,
|
|
138
|
-
closeOnFocusOut: false,
|
|
139
|
-
guards: false,
|
|
140
|
-
children: panelDiv
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
return panelDiv;
|
|
145
180
|
}
|
|
146
181
|
);
|
|
182
|
+
function resolveInitialFocusTarget(panel, initialFocus) {
|
|
183
|
+
if (initialFocus && typeof initialFocus === "object") {
|
|
184
|
+
return initialFocus.current ?? null;
|
|
185
|
+
}
|
|
186
|
+
const managed = Array.from(
|
|
187
|
+
panel.querySelectorAll("[data-salt-original-tabindex]")
|
|
188
|
+
);
|
|
189
|
+
const candidates = managed.length ? managed : tabbable(panel, { displayCheck: "none" });
|
|
190
|
+
const index = typeof initialFocus === "number" ? initialFocus : 0;
|
|
191
|
+
return candidates[index] ?? candidates[0] ?? panel;
|
|
192
|
+
}
|
|
147
193
|
|
|
148
194
|
export { SidePanel };
|
|
149
195
|
//# sourceMappingURL=SidePanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SidePanel.js","sources":["../src/side-panel/SidePanel.tsx"],"sourcesContent":["import { FloatingFocusManager } from \"@floating-ui/react\";\nimport {\n makePrefixer,\n useFloatingUI,\n useForkRef,\n useId,\n useIsomorphicLayoutEffect,\n} from \"@salt-ds/core\";\nimport { useComponentCssInjection } from \"@salt-ds/styles\";\nimport { useWindow } from \"@salt-ds/window\";\nimport { clsx } from \"clsx\";\nimport {\n type AnimationEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n forwardRef,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { SidePanelContext, useSidePanelContext } from \"./internal\";\nimport sidePanelCss from \"./SidePanel.css\";\n\nconst withBaseName = makePrefixer(\"saltSidePanel\");\n\nexport interface SidePanelProps extends ComponentPropsWithRef<\"div\"> {\n /**\n * Disable the panel's own open/close animation.\n * Set to `true` when the parent controls sizing and animation (e.g. inside a splitter).\n * @default false\n */\n disableAnimation?: boolean;\n /**\n * Edge the panel is anchored to; controls animation direction and divider side.\n * @default \"right\"\n */\n position?: \"right\" | \"left\";\n /**\n * Which element receives focus when the panel opens.\n * Pass a number for the tabbable element index (0 = first), or a ref to a specific element.\n * Defaults to the side panel close button.\n */\n initialFocus?: ComponentProps<typeof FloatingFocusManager>[\"initialFocus\"];\n /**\n * The background color palette. Options are 'primary', 'secondary', 'tertiary' and 'none'.\n * @default \"primary\"\n */\n variant?: \"primary\" | \"secondary\" | \"tertiary\" | \"none\";\n}\n\nexport const SidePanel = forwardRef<HTMLDivElement, SidePanelProps>(\n function SidePanel(props, ref) {\n const {\n disableAnimation = false,\n position = \"right\",\n initialFocus,\n variant = \"primary\",\n children,\n id: idProp,\n className,\n \"aria-labelledby\": ariaLabelledBy,\n onAnimationEnd,\n ...rest\n } = props;\n\n const sidePanelContext = useSidePanelContext();\n const { openState, floatingRootContext, setFloating, setPanelId, titleId } =\n sidePanelContext;\n const positionedContext = useMemo(\n () => ({ ...sidePanelContext, position }),\n [sidePanelContext, position],\n );\n\n const id = useId(idProp);\n\n const [showComponent, setShowComponent] = useState(openState);\n const [animating, setAnimating] = useState(false);\n const shouldAnimateOpen = useRef(!openState);\n // On first mount while open, skip moving focus when focus did not come from the trigger.\n const [skipInitialFocus, setSkipInitialFocus] = useState(() => {\n if (!openState) return false;\n const reference = floatingRootContext.elements.reference;\n if (!(reference instanceof Element)) return true;\n const activeElement = reference.ownerDocument?.activeElement;\n return !activeElement || !reference.contains(activeElement);\n });\n const targetWindow = useWindow();\n\n useComponentCssInjection({\n testId: \"salt-side-panel\",\n css: sidePanelCss,\n window: targetWindow,\n });\n\n const { context } = useFloatingUI({\n rootContext: floatingRootContext,\n });\n\n const handleRef = useForkRef<HTMLDivElement>(setFloating, ref);\n\n const handleAnimationEnd = (event: AnimationEvent<HTMLDivElement>) => {\n onAnimationEnd?.(event);\n\n if (event.currentTarget !== event.target || disableAnimation) return;\n setAnimating(false);\n if (!openState) {\n setShowComponent(false);\n }\n };\n\n useEffect(() => {\n setPanelId(id);\n return () => {\n setPanelId(undefined);\n };\n }, [id, setPanelId]);\n\n useEffect(() => {\n if (!openState) {\n setSkipInitialFocus(false);\n }\n }, [openState]);\n\n useIsomorphicLayoutEffect(() => {\n if (disableAnimation) {\n setShowComponent(openState);\n setAnimating(false);\n if (!openState) shouldAnimateOpen.current = true;\n return;\n }\n\n if (!openState) {\n shouldAnimateOpen.current = true;\n }\n\n // Don't animate if the panel has never been closed (defaultOpen scenario).\n if (openState && !shouldAnimateOpen.current) {\n setShowComponent(true);\n setAnimating(false);\n return;\n }\n\n const prefersReducedMotion = targetWindow?.matchMedia?.(\n \"(prefers-reduced-motion: reduce)\",\n )?.matches;\n\n if (openState) {\n setShowComponent(true);\n }\n\n if (prefersReducedMotion) {\n setAnimating(false);\n if (!openState) {\n setShowComponent(false);\n }\n } else {\n setAnimating(true);\n }\n }, [openState, targetWindow, disableAnimation]);\n\n if (!showComponent) return null;\n\n const resolvedInitialFocus = skipInitialFocus ? -1 : (initialFocus ?? 0);\n\n const panelDiv = (\n <div\n role=\"region\"\n aria-labelledby={clsx(ariaLabelledBy, titleId) || undefined}\n ref={handleRef}\n className={clsx(\n withBaseName(),\n {\n [withBaseName(position)]: position,\n [withBaseName(variant)]: variant,\n [withBaseName(\"enterAnimation\")]:\n !disableAnimation && openState && animating,\n [withBaseName(\"exitAnimation\")]:\n !disableAnimation && !openState && animating,\n },\n className,\n )}\n onAnimationEnd={handleAnimationEnd}\n tabIndex={-1}\n {...rest}\n id={id}\n >\n <SidePanelContext.Provider value={positionedContext}>\n <div className={withBaseName(\"inner\")}>{children}</div>\n </SidePanelContext.Provider>\n </div>\n );\n\n if (openState || animating) {\n return (\n <FloatingFocusManager\n context={context}\n modal={false}\n initialFocus={resolvedInitialFocus}\n closeOnFocusOut={false}\n guards={false}\n >\n {panelDiv}\n </FloatingFocusManager>\n );\n }\n\n return panelDiv;\n },\n);\n"],"names":["SidePanel","sidePanelCss"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SidePanel.js","sources":["../src/side-panel/SidePanel.tsx"],"sourcesContent":["import {\n makePrefixer,\n useEventCallback,\n useForkRef,\n useId,\n useIsomorphicLayoutEffect,\n usePrevious,\n} from \"@salt-ds/core\";\nimport { useComponentCssInjection } from \"@salt-ds/styles\";\nimport { useWindow } from \"@salt-ds/window\";\nimport { clsx } from \"clsx\";\nimport {\n type AnimationEvent,\n type ComponentPropsWithRef,\n forwardRef,\n type RefObject,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { tabbable } from \"tabbable\";\nimport { SidePanelContext, useSidePanelContext } from \"./internal\";\nimport sidePanelCss from \"./SidePanel.css\";\n\nconst withBaseName = makePrefixer(\"saltSidePanel\");\n\nexport interface SidePanelProps extends ComponentPropsWithRef<\"div\"> {\n /**\n * Disable the panel's own open/close animation.\n * Set to `true` when the parent controls sizing and animation (e.g. inside a splitter).\n * @default false\n */\n disableAnimation?: boolean;\n /**\n * Edge the panel is anchored to; controls animation direction and divider side.\n * @default \"right\"\n */\n position?: \"right\" | \"left\";\n /**\n * Which element receives focus when the panel opens.\n * Pass a number for the tabbable element index (0 = first), or a ref to a specific element.\n * Defaults to the first tabbable element inside the panel (close button if present).\n */\n initialFocus?: number | RefObject<HTMLElement | null>;\n /**\n * The background color palette. Options are 'primary', 'secondary', 'tertiary' and 'none'.\n * @default \"primary\"\n */\n variant?: \"primary\" | \"secondary\" | \"tertiary\" | \"none\";\n}\n\nexport const SidePanel = forwardRef<HTMLDivElement, SidePanelProps>(\n function SidePanel(props, ref) {\n const {\n disableAnimation = false,\n position = \"right\",\n initialFocus,\n variant = \"primary\",\n children,\n id: idProp,\n className,\n \"aria-labelledby\": ariaLabelledBy,\n onAnimationEnd,\n ...rest\n } = props;\n\n const sidePanelContext = useSidePanelContext();\n const { openState, setFloating, setPanelId, titleId } = sidePanelContext;\n const positionedContext = useMemo(\n () => ({ ...sidePanelContext, position }),\n [sidePanelContext, position],\n );\n\n const id = useId(idProp);\n\n const [showComponent, setShowComponent] = useState(openState);\n const [animating, setAnimating] = useState(false);\n const shouldAnimateOpen = useRef(!openState);\n // Stays true until a ref-callback invocation observes focus already\n // inside the trigger (a user-driven open). Flipping this from a mount\n // effect would break under React 18 strict-mode double-mounting.\n const initialMountRef = useRef(true);\n const panelRef = useRef<HTMLDivElement | null>(null);\n const targetWindow = useWindow();\n\n useComponentCssInjection({\n testId: \"salt-side-panel\",\n css: sidePanelCss,\n window: targetWindow,\n });\n\n // Guards against re-focusing the panel multiple times per open session.\n const initialFocusDoneRef = useRef(false);\n useEffect(() => {\n if (!openState) {\n initialFocusDoneRef.current = false;\n }\n }, [openState]);\n\n // useEventCallback keeps a stable identity while always reading the\n // latest closure, so React doesn't tear down and re-invoke the ref\n // callback per render.\n const handleInitialFocus = useEventCallback((el: HTMLDivElement | null) => {\n if (!el || !openState || initialFocusDoneRef.current) {\n return;\n }\n\n // On first mount, only auto-focus if focus is already in the trigger\n // (the common click path). For defaultOpen without user interaction\n // we leave focus alone.\n if (initialMountRef.current) {\n const reference =\n sidePanelContext.floatingRootContext.elements.reference;\n const activeElement =\n reference instanceof Element\n ? reference.ownerDocument?.activeElement\n : null;\n const focusCameFromTrigger =\n reference instanceof Element &&\n activeElement instanceof Node &&\n reference.contains(activeElement);\n if (!focusCameFromTrigger) {\n return;\n }\n initialMountRef.current = false;\n }\n\n initialFocusDoneRef.current = true;\n // Defer one frame so useSidePanelTabOrder has marked descendants\n // with data-salt-original-tabindex and any child layout effects\n // (e.g. SidePanelContent's scrollable-body tabIndex toggle) have\n // settled. Scoped to the panel's owner window for iframe/shadow-root\n // hosts.\n const raf =\n el.ownerDocument.defaultView?.requestAnimationFrame ??\n targetWindow?.requestAnimationFrame ??\n requestAnimationFrame;\n raf(() => {\n if (!el.isConnected) return;\n const focusTarget = resolveInitialFocusTarget(el, initialFocus);\n focusTarget?.focus();\n });\n });\n\n const setPanelEl = useEventCallback((el: HTMLDivElement | null) => {\n panelRef.current = el;\n setFloating(el);\n handleInitialFocus(el);\n });\n\n const handleRef = useForkRef<HTMLDivElement>(setPanelEl, ref);\n\n // Snapshot the panel's natural width (in px) into a CSS variable so the\n // inner can keep its full size while the outer animates between 0 and\n // its target width. This makes percentage values for\n // --saltSidePanel-width work, since the inner no longer resolves a\n // percentage against the outer's animating width.\n useIsomorphicLayoutEffect(() => {\n if (!animating || disableAnimation) return;\n const panel = panelRef.current;\n if (!panel) return;\n\n // Read the natural width with the animation suspended so we see the\n // resting size rather than the in-flight interpolated value.\n // getBoundingClientRect() flushes style/layout, so the inline\n // `animation: none` takes effect for this read.\n const previousAnimation = panel.style.animation;\n panel.style.animation = \"none\";\n const widthPx = panel.getBoundingClientRect().width;\n panel.style.animation = previousAnimation;\n\n panel.style.setProperty(\n \"--saltSidePanel-animation-width\",\n `${widthPx}px`,\n );\n\n return () => {\n panel.style.removeProperty(\"--saltSidePanel-animation-width\");\n };\n }, [animating, disableAnimation]);\n\n const handleAnimationEnd = useEventCallback(\n (event: AnimationEvent<HTMLDivElement>) => {\n onAnimationEnd?.(event);\n\n if (event.currentTarget !== event.target || disableAnimation) return;\n setAnimating(false);\n if (!openState) {\n setShowComponent(false);\n }\n },\n );\n\n useEffect(() => {\n setPanelId(id);\n return () => {\n setPanelId(undefined);\n };\n }, [id, setPanelId]);\n\n // Return focus to the trigger on close (mirrors floating-ui's\n // returnFocus). Initial previousOpenState of `false` ensures we never\n // restore on mount — only on a real true→false transition.\n const reference = sidePanelContext.floatingRootContext.elements.reference;\n const previousOpenState = usePrevious(openState, [openState], false);\n useEffect(() => {\n if (!previousOpenState || openState) return;\n const panel = panelRef.current;\n if (!(reference instanceof HTMLElement)) return;\n const doc = reference.ownerDocument;\n const active = doc?.activeElement;\n const focusInsidePanel =\n panel && active instanceof Node && panel.contains(active);\n const focusOnBody = active === doc?.body || active == null;\n if (focusInsidePanel || focusOnBody) {\n reference.focus();\n }\n }, [openState, previousOpenState, reference]);\n\n useIsomorphicLayoutEffect(() => {\n if (disableAnimation) {\n setShowComponent(openState);\n setAnimating(false);\n if (!openState) shouldAnimateOpen.current = true;\n return;\n }\n\n if (!openState) {\n shouldAnimateOpen.current = true;\n }\n\n // Skip enter animation when the panel was open from the start.\n if (openState && !shouldAnimateOpen.current) {\n setShowComponent(true);\n setAnimating(false);\n return;\n }\n\n const prefersReducedMotion = targetWindow?.matchMedia?.(\n \"(prefers-reduced-motion: reduce)\",\n )?.matches;\n\n if (openState) {\n setShowComponent(true);\n }\n\n if (prefersReducedMotion) {\n setAnimating(false);\n if (!openState) {\n setShowComponent(false);\n }\n } else {\n setAnimating(true);\n }\n }, [openState, targetWindow, disableAnimation]);\n\n if (!showComponent) return null;\n\n return (\n <div\n role=\"region\"\n aria-labelledby={clsx(ariaLabelledBy, titleId) || undefined}\n ref={handleRef}\n className={clsx(\n withBaseName(),\n {\n [withBaseName(position)]: position,\n [withBaseName(variant)]: variant,\n [withBaseName(\"enterAnimation\")]:\n !disableAnimation && openState && animating,\n [withBaseName(\"exitAnimation\")]:\n !disableAnimation && !openState && animating,\n },\n className,\n )}\n onAnimationEnd={handleAnimationEnd}\n tabIndex={-1}\n {...rest}\n id={id}\n >\n <SidePanelContext.Provider value={positionedContext}>\n <div className={withBaseName(\"inner\")}>{children}</div>\n </SidePanelContext.Provider>\n </div>\n );\n },\n);\n\nfunction resolveInitialFocusTarget(\n panel: HTMLElement,\n initialFocus: SidePanelProps[\"initialFocus\"],\n): HTMLElement | null {\n if (initialFocus && typeof initialFocus === \"object\") {\n return initialFocus.current ?? null;\n }\n\n // Prefer the panel's \"managed\" sequence (elements detached from the\n // natural tab order by useSidePanelTabOrder), falling back to a fresh\n // tabbable() scan when detachment hasn't run yet.\n const managed = Array.from(\n panel.querySelectorAll<HTMLElement>(\"[data-salt-original-tabindex]\"),\n );\n\n const candidates = managed.length\n ? managed\n : (tabbable(panel, { displayCheck: \"none\" }) as HTMLElement[]);\n\n const index = typeof initialFocus === \"number\" ? initialFocus : 0;\n return candidates[index] ?? candidates[0] ?? panel;\n}\n"],"names":["SidePanel","sidePanelCss","reference"],"mappings":"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"}
|