@jini-ai/ui 0.4.3 → 0.4.5
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 +11 -0
- package/README.md +20 -0
- package/dist/core/theme/default.d.ts +4 -0
- package/dist/core/theme/default.js +11 -0
- package/dist/core/theme/stylesheet.d.ts +8 -0
- package/dist/core/theme/stylesheet.js +14 -0
- package/dist/core/theme/types.d.ts +39 -0
- package/dist/core/theme/types.js +2 -0
- package/dist/core/theme/validation.d.ts +15 -0
- package/dist/core/theme/validation.js +55 -0
- package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
- package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
- package/dist/features/admin-widgets/components/Select/Select.js +4 -3
- package/dist/features/appearance/constants.js +1 -1
- package/dist/features/connectors/rules.d.ts +1 -6
- package/dist/features/connectors/types.d.ts +1 -6
- package/dist/features/connectors/types.js +1 -6
- package/dist/features/execution/http-adapter.d.ts +111 -0
- package/dist/features/execution/http-adapter.js +160 -0
- package/dist/features/execution/index.d.ts +4 -0
- package/dist/features/execution/index.js +2 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
- package/dist/features/html-editor/canvas-style.d.ts +1 -1
- package/dist/features/html-editor/canvas-style.js +2 -2
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
- package/dist/features/i18n/dictionary-translator.d.ts +5 -2
- package/dist/features/i18n/dictionary-translator.js +1 -1
- package/dist/features/i18n/index.d.ts +1 -1
- package/dist/features/i18n/index.js +1 -1
- package/dist/features/i18n/template-i18n.d.ts +11 -0
- package/dist/features/i18n/template-i18n.js +12 -0
- package/dist/features/interactive-ui/interactive-ui.css +1 -1
- package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
- package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
- package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
- package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
- package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
- package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
- package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
- package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
- package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
- package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
- package/dist/features/lexical-rich-text-editor/types.js +3 -13
- package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
- package/dist/features/mcp-ui/early-message-buffer.js +1 -2
- package/dist/features/mcp-ui/escape.d.ts +4 -0
- package/dist/features/mcp-ui/escape.js +2 -9
- package/dist/features/mcp-ui/index.d.ts +1 -0
- package/dist/features/mcp-ui/index.js +1 -0
- package/dist/features/mcp-ui/protocol.d.ts +2 -5
- package/dist/features/mcp-ui/protocol.js +2 -5
- package/dist/features/mcp-ui/resource.d.ts +47 -7
- package/dist/features/mcp-ui/resource.js +5 -8
- package/dist/features/mcp-ui/secret-card.d.ts +115 -0
- package/dist/features/mcp-ui/secret-card.js +137 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
- package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
- package/dist/features/mcp-ui/surfaces/fields.js +3 -1
- package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
- package/dist/features/mcp-ui/surfaces/form.js +65 -3
- package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
- package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
- package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
- package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
- package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
- package/dist/features/panel-kit/helpers/permissions.js +2 -0
- package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
- package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
- package/dist/features/panel-kit/index.d.ts +7 -1
- package/dist/features/panel-kit/index.js +5 -1
- package/dist/features/resource-dashboard/types.d.ts +8 -41
- package/dist/features/resource-dashboard/types.js +3 -26
- package/dist/features/tab-strip/constants.d.ts +3 -4
- package/dist/features/tab-strip/constants.js +3 -4
- package/dist/features/tab-strip/dependencies.d.ts +1 -6
- package/dist/features/tab-strip/dependencies.js +1 -6
- package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
- package/dist/features/tab-strip/react/components/TabBar.js +12 -1
- package/dist/features/tab-strip/rules.d.ts +4 -17
- package/dist/features/tab-strip/rules.js +4 -17
- package/dist/features/tab-strip/types.d.ts +2 -13
- package/dist/icon-name.d.ts +1 -1
- package/dist/icon-name.js +1 -0
- package/dist/index.d.ts +42 -417
- package/dist/index.js +42 -430
- package/dist/react/components/CustomSelect.d.ts +6 -2
- package/dist/react/components/CustomSelect.js +20 -91
- package/dist/react/components/Icon.d.ts +6 -0
- package/dist/react/components/Icon.js +12 -0
- package/dist/react/components/select-popover.d.ts +106 -0
- package/dist/react/components/select-popover.js +210 -0
- package/dist/renderers/sandbox-scripts.d.ts +29 -0
- package/dist/renderers/sandbox-scripts.js +127 -0
- package/dist/renderers/sandboxed-document.js +3 -86
- package/dist/renderers/srcdoc/build.d.ts +10 -17
- package/dist/renderers/srcdoc/build.js +31 -234
- package/dist/styles/variables.css +30 -0
- package/dist/theme/apply.d.ts +1 -1
- package/dist/theme/apply.js +2 -2
- package/dist/theme/color-scheme.d.ts +1 -1
- package/dist/theme/default.d.ts +1 -3
- package/dist/theme/default.js +2 -10
- package/dist/theme/index.d.ts +3 -3
- package/dist/theme/index.js +2 -2
- package/dist/theme/stylesheet.d.ts +1 -7
- package/dist/theme/stylesheet.js +2 -13
- package/dist/theme/types.d.ts +1 -38
- package/dist/theme/types.js +2 -1
- package/dist/theme/validation.d.ts +1 -14
- package/dist/theme/validation.js +2 -54
- package/dist/utils/endpoint-policy.d.ts +21 -39
- package/dist/utils/endpoint-policy.js +19 -107
- package/dist/utils/file-system-errors.d.ts +1 -2
- package/dist/utils/file-system-errors.js +1 -2
- package/dist/utils/ime-composing.d.ts +0 -3
- package/dist/utils/ime-composing.js +0 -3
- package/dist/utils/platform.d.ts +1 -4
- package/dist/utils/platform.js +1 -4
- package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
- package/dist/utils/smooth-scroll-to-top.js +3 -13
- package/dist/utils/timezone.js +1 -5
- package/dist/utils/uuid.d.ts +0 -3
- package/dist/utils/uuid.js +0 -3
- package/dist/utils/visual-stability.d.ts +1 -3
- package/dist/utils/visual-stability.js +1 -3
- package/package.json +74 -15
|
@@ -8,8 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// pattern (see TooltipLayer.tsx). Every seam is exported: pure helpers, the
|
|
9
9
|
// `useCustomSelect` hook that owns all state/refs/effects/handlers, the
|
|
10
10
|
// `CustomSelectOptionButton` leaf, and the dumb `CustomSelect` render.
|
|
11
|
-
import {
|
|
11
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
12
12
|
import { createPortal } from 'react-dom';
|
|
13
|
+
import { useSelectPopover, isSelectEventInside, openSelectOnKey, applySelectNavigationKey, resolveSelectPortalContainer } from './select-popover.js';
|
|
13
14
|
import { Icon } from './Icon.js';
|
|
14
15
|
/** Space between the trigger and the menu. */
|
|
15
16
|
export const CUSTOM_SELECT_MENU_GAP = 4;
|
|
@@ -88,7 +89,7 @@ export function reconcileCustomSelectActiveValue(input) {
|
|
|
88
89
|
/** Pure: is a pointerdown target inside the trigger or the (possibly absent)
|
|
89
90
|
* menu? Used to decide whether an outside click should close the menu. */
|
|
90
91
|
export function isCustomSelectEventInside(target, button, menu) {
|
|
91
|
-
return
|
|
92
|
+
return isSelectEventInside({ target, trigger: button, panel: menu });
|
|
92
93
|
}
|
|
93
94
|
/**
|
|
94
95
|
* All of CustomSelect's behavior — ids, refs, open/active/position state, the
|
|
@@ -98,24 +99,14 @@ export function isCustomSelectEventInside(target, button, menu) {
|
|
|
98
99
|
* logic is testable in isolation from the DOM; `CustomSelect` is the dumb
|
|
99
100
|
* consumer.
|
|
100
101
|
*/
|
|
101
|
-
export function useCustomSelect({ value, options, onChange, portal, placeholder, onOpenChange, }) {
|
|
102
|
+
export function useCustomSelect({ value, options, onChange, portal, placeholder, onOpenChange, }, _optional = {}) {
|
|
102
103
|
const reactId = useId();
|
|
103
104
|
const idBase = reactId.replace(/:/g, '');
|
|
104
|
-
const buttonRef = useRef(null);
|
|
105
|
-
const menuRef = useRef(null);
|
|
106
105
|
const wasOpenRef = useRef(false);
|
|
107
106
|
const activeSourceValueRef = useRef(value);
|
|
108
|
-
const [open, setOpen] = useState(false);
|
|
109
|
-
// Stored in a ref (not a `useEffect` dependency) so an unmemoized caller
|
|
110
|
-
// passing a fresh function identity every render never re-fires this for
|
|
111
|
-
// any reason OTHER than `open` actually changing.
|
|
112
|
-
const onOpenChangeRef = useRef(onOpenChange);
|
|
113
|
-
onOpenChangeRef.current = onOpenChange;
|
|
114
107
|
const [activeValue, setActiveValue] = useState(value);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
onOpenChangeRef.current?.(open);
|
|
118
|
-
}, [open]);
|
|
108
|
+
const popover = useSelectPopover({ measurePosition: ({ trigger }) => computeCustomSelectMenuPosition(trigger.getBoundingClientRect(), { width: window.innerWidth, height: window.innerHeight }) }, { portal, onOpenChange });
|
|
109
|
+
const { open, position, triggerRef: buttonRef, panelRef: menuRef, updatePosition } = popover;
|
|
119
110
|
const flatOptions = useMemo(() => flattenCustomSelectOptions(options), [options]);
|
|
120
111
|
const selected = flatOptions.find((option) => option.value === value);
|
|
121
112
|
const selectedLabel = selected?.label ?? placeholder ?? value;
|
|
@@ -126,24 +117,6 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
|
|
|
126
117
|
enabledOptionsRef.current = enabledOptions;
|
|
127
118
|
const optionIdByValue = useMemo(() => new Map(flatOptions.map((option, index) => [option.value, `${idBase}-option-${index}`])), [flatOptions, idBase]);
|
|
128
119
|
const activeOptionId = open && activeValue ? optionIdByValue.get(activeValue) : undefined;
|
|
129
|
-
const updatePosition = useCallback(() => {
|
|
130
|
-
if (!buttonRef.current)
|
|
131
|
-
return;
|
|
132
|
-
const rect = buttonRef.current.getBoundingClientRect();
|
|
133
|
-
setPosition(computeCustomSelectMenuPosition(rect, {
|
|
134
|
-
width: window.innerWidth,
|
|
135
|
-
height: window.innerHeight,
|
|
136
|
-
}));
|
|
137
|
-
}, []);
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (!portal)
|
|
140
|
-
return;
|
|
141
|
-
if (!open) {
|
|
142
|
-
setPosition(null);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
updatePosition();
|
|
146
|
-
}, [open, portal, updatePosition]);
|
|
147
120
|
useEffect(() => {
|
|
148
121
|
const result = reconcileCustomSelectActiveValue({
|
|
149
122
|
open,
|
|
@@ -158,71 +131,31 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
|
|
|
158
131
|
if (result.nextActiveValue !== undefined)
|
|
159
132
|
setActiveValue(result.nextActiveValue);
|
|
160
133
|
}, [open, value]);
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (!open)
|
|
163
|
-
return;
|
|
164
|
-
const onPointerDown = (event) => {
|
|
165
|
-
const target = event.target;
|
|
166
|
-
if (isCustomSelectEventInside(target, buttonRef.current, menuRef.current))
|
|
167
|
-
return;
|
|
168
|
-
setOpen(false);
|
|
169
|
-
};
|
|
170
|
-
const onScrollOrResize = () => {
|
|
171
|
-
if (portal)
|
|
172
|
-
updatePosition();
|
|
173
|
-
};
|
|
174
|
-
document.addEventListener('mousedown', onPointerDown);
|
|
175
|
-
window.addEventListener('resize', onScrollOrResize);
|
|
176
|
-
window.addEventListener('scroll', onScrollOrResize, true);
|
|
177
|
-
return () => {
|
|
178
|
-
document.removeEventListener('mousedown', onPointerDown);
|
|
179
|
-
window.removeEventListener('resize', onScrollOrResize);
|
|
180
|
-
window.removeEventListener('scroll', onScrollOrResize, true);
|
|
181
|
-
};
|
|
182
|
-
}, [open, portal, updatePosition]);
|
|
183
134
|
const choose = (nextValue) => {
|
|
184
135
|
const next = flatOptions.find((option) => option.value === nextValue);
|
|
185
136
|
if (!next || next.disabled)
|
|
186
137
|
return;
|
|
187
138
|
onChange(next.value);
|
|
188
|
-
|
|
189
|
-
buttonRef.current?.focus();
|
|
139
|
+
popover.closePanel({ refocusTrigger: true });
|
|
190
140
|
};
|
|
191
141
|
const moveActive = (direction) => {
|
|
192
142
|
const next = nextCustomSelectActiveValue(enabledOptions, activeValue, direction);
|
|
193
143
|
if (next !== null)
|
|
194
144
|
setActiveValue(next);
|
|
195
145
|
};
|
|
196
|
-
const toggleOpen = () =>
|
|
197
|
-
const closeAndRefocusTrigger = () => {
|
|
198
|
-
setOpen(false);
|
|
199
|
-
buttonRef.current?.focus();
|
|
200
|
-
};
|
|
146
|
+
const toggleOpen = () => open ? popover.closePanel({ refocusTrigger: false }) : popover.openPanel();
|
|
201
147
|
const onButtonKeyDown = (event) => {
|
|
202
|
-
if (
|
|
203
|
-
event.preventDefault();
|
|
204
|
-
if (!open) {
|
|
205
|
-
setOpen(true);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
moveActive(event.key === 'ArrowDown' ? 1 : -1);
|
|
148
|
+
if (popover.handleDismissKey(event))
|
|
209
149
|
return;
|
|
210
|
-
}
|
|
211
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
212
|
-
event.preventDefault();
|
|
213
|
-
if (open) {
|
|
214
|
-
choose(activeValue || value);
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
setOpen(true);
|
|
218
|
-
}
|
|
150
|
+
if (openSelectOnKey({ event, open, openPanel: popover.openPanel }))
|
|
219
151
|
return;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
152
|
+
if (!open)
|
|
153
|
+
return;
|
|
154
|
+
applySelectNavigationKey({ key: event.key, actions: {
|
|
155
|
+
preventDefault: () => event.preventDefault(), move: moveActive,
|
|
156
|
+
edge: (edge) => setActiveValue((edge === 'first' ? enabledOptions[0] : enabledOptions.at(-1))?.value ?? ''),
|
|
157
|
+
choose: () => choose(activeValue || value),
|
|
158
|
+
} }, { spaceChooses: true });
|
|
226
159
|
};
|
|
227
160
|
// Escape closes even when focus has moved into custom `menuHeader` content
|
|
228
161
|
// (e.g. `SearchableModelSelect`'s search input) — `onButtonKeyDown` alone
|
|
@@ -232,11 +165,7 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
|
|
|
232
165
|
// take real focus). Attached to the menu container, not the document, so it
|
|
233
166
|
// never fires for an unrelated Escape press elsewhere on the page.
|
|
234
167
|
const onMenuKeyDown = (event) => {
|
|
235
|
-
|
|
236
|
-
return;
|
|
237
|
-
event.preventDefault();
|
|
238
|
-
event.stopPropagation();
|
|
239
|
-
closeAndRefocusTrigger();
|
|
168
|
+
popover.handleDismissKey(event);
|
|
240
169
|
};
|
|
241
170
|
return {
|
|
242
171
|
idBase,
|
|
@@ -262,7 +191,7 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
|
|
|
262
191
|
* {@link useCustomSelect}; this component only wires the hook's state to the
|
|
263
192
|
* trigger button and the listbox menu.
|
|
264
193
|
*/
|
|
265
|
-
export function CustomSelect({ value, options, onChange, ariaLabel, labelledBy, className, triggerClassName, menuClassName, disabled = false, placeholder, portal = true, title, onFocus, menuHeader, onOpenChange, useCustomSelect: useCustomSelectHook = useCustomSelect, }) {
|
|
194
|
+
export function CustomSelect({ value, options, onChange, ariaLabel, labelledBy, className, triggerClassName, menuClassName, disabled = false, placeholder, portal = true, portalContainer, title, onFocus, menuHeader, onOpenChange, useCustomSelect: useCustomSelectHook = useCustomSelect, }, _optional = {}) {
|
|
266
195
|
const { idBase, buttonRef, menuRef, open, activeValue, position, selectedLabel, optionIdByValue, activeOptionId, choose, setActiveValue, toggleOpen, onButtonKeyDown, onMenuKeyDown, } = useCustomSelectHook({ value, options, onChange, portal, placeholder, onOpenChange });
|
|
267
196
|
/**
|
|
268
197
|
* The theme of the place this menu was opened FROM.
|
|
@@ -301,7 +230,7 @@ export function CustomSelect({ value, options, onChange, ariaLabel, labelledBy,
|
|
|
301
230
|
}
|
|
302
231
|
return (_jsx(CustomSelectOptionButton, { option: item, selected: item.value === value, active: item.value === activeValue, id: optionIdByValue.get(item.value), onChoose: choose, onActive: setActiveValue }, item.value));
|
|
303
232
|
})] }));
|
|
304
|
-
return (_jsxs("div", { className: ['jini-select', className].filter(Boolean).join(' '), children: [_jsxs("button", { ref: buttonRef, type: "button", className: ['jini-select-trigger', triggerClassName].filter(Boolean).join(' '), role: "combobox", value: value, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${idBase}-menu`, "aria-activedescendant": activeOptionId, "aria-describedby": labelledBy, "aria-label": `${ariaLabel}: ${selectedLabel}`, disabled: disabled, title: title, onClick: toggleOpen, onKeyDown: onButtonKeyDown, onFocus: onFocus, children: [_jsx("span", { id: `${idBase}-value`, className: "jini-select-value", children: selectedLabel }), _jsx(Icon, { name: "chevron-down", size: 14 })] }), open ? (portal ? (position ? createPortal(menu,
|
|
233
|
+
return (_jsxs("div", { className: ['jini-select', className].filter(Boolean).join(' '), children: [_jsxs("button", { ref: buttonRef, type: "button", className: ['jini-select-trigger', triggerClassName].filter(Boolean).join(' '), role: "combobox", value: value, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${idBase}-menu`, "aria-activedescendant": activeOptionId, "aria-describedby": labelledBy, "aria-label": `${ariaLabel}: ${selectedLabel}`, disabled: disabled, title: title, onClick: toggleOpen, onKeyDown: onButtonKeyDown, onFocus: onFocus, children: [_jsx("span", { id: `${idBase}-value`, className: "jini-select-value", children: selectedLabel }), _jsx(Icon, { name: "chevron-down", size: 14 })] }), open ? (portal ? (position ? createPortal(menu, resolveSelectPortalContainer({ trigger: buttonRef.current }, { container: portalContainer })) : null) : menu) : null] }));
|
|
305
234
|
}
|
|
306
235
|
/** One row in the listbox: a selectable option button that reports hover
|
|
307
236
|
* (activation) and click (choice) back to {@link CustomSelect}. */
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { ReactElement, SVGProps } from 'react';
|
|
2
2
|
import type { IconName } from '../../icon-name.js';
|
|
3
3
|
export type { IconName } from '../../icon-name.js';
|
|
4
|
+
/** Exact shared geometry for outlines that feature artwork decorates with different inner marks.
|
|
5
|
+
* Keep the marks with their feature; a shared outline does not make two whole icons equivalent. */
|
|
6
|
+
export declare const ICON_PATH_DATA: {
|
|
7
|
+
readonly 'document-outline-compact': "M6.5 3.5h7.6l3.9 3.9v13.1H6.5z";
|
|
8
|
+
readonly 'folder-outline-compact': "M3.5 7.5V6A1.5 1.5 0 0 1 5 4.5h4l2 2h8A1.5 1.5 0 0 1 20.5 8v10A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z";
|
|
9
|
+
};
|
|
4
10
|
interface Props extends Omit<SVGProps<SVGSVGElement>, 'name'> {
|
|
5
11
|
name: IconName;
|
|
6
12
|
size?: number | string;
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Exact shared geometry for outlines that feature artwork decorates with different inner marks.
|
|
3
|
+
* Keep the marks with their feature; a shared outline does not make two whole icons equivalent. */
|
|
4
|
+
export const ICON_PATH_DATA = {
|
|
5
|
+
'document-outline-compact': 'M6.5 3.5h7.6l3.9 3.9v13.1H6.5z',
|
|
6
|
+
'folder-outline-compact': 'M3.5 7.5V6A1.5 1.5 0 0 1 5 4.5h4l2 2h8A1.5 1.5 0 0 1 20.5 8v10A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z',
|
|
7
|
+
};
|
|
2
8
|
// Exported as a test seam only — call it directly to assert key-set
|
|
3
9
|
// completeness and per-entry validity without rendering. NOT part of the
|
|
4
10
|
// package's public API: do not re-export it from `packages/ui/src/index.ts`
|
|
5
11
|
// or any entry point.
|
|
6
12
|
export const ICON_RENDERERS = {
|
|
13
|
+
'package-check': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M20.5 8.2v5.1L12 17.9l-8.5-4.6V8.2" }), _jsx("path", { d: "M3.5 8.2 12 3.6l8.5 4.6L12 12.8z" }), _jsx("path", { d: "M15.6 19.4l1.9 1.9 3.4-3.9" })] })),
|
|
14
|
+
'storefront': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M3.5 8.5h17l-1.2-4H4.7z" }), _jsx("path", { d: "M5 8.5v11h14v-11" }), _jsx("path", { d: "M9.75 19.5v-5.5h4.5v5.5" })] })),
|
|
15
|
+
'package-outline': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M3.5 8.2 12 3.6l8.5 4.6v7.6L12 20.4l-8.5-4.6z" }), _jsx("path", { d: "M3.5 8.2 12 12.8l8.5-4.6M12 12.8v7.6" })] })),
|
|
16
|
+
'trash-compact': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M4.5 7h15M9.5 7V4.6h5V7" }), _jsx("path", { d: "M6.6 7l.9 12.4h9l.9-12.4" }), _jsx("path", { d: "M10.4 10.5v6M13.6 10.5v6" })] })),
|
|
17
|
+
'chevron-right-wide': (common) => (_jsx("svg", { ...common, children: _jsx("path", { d: "M9 5.5 15.5 12 9 18.5" }) })),
|
|
18
|
+
'check-long': (common) => (_jsx("svg", { ...common, children: _jsx("path", { d: "m5 13 4.5 4.5L19 6.5" }) })),
|
|
7
19
|
'alert-triangle': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }), _jsx("path", { d: "M12 9v4" }), _jsx("path", { d: "M12 17h.01" })] })),
|
|
8
20
|
'arrow-left': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M19 12H5" }), _jsx("path", { d: "m12 19-7-7 7-7" })] })),
|
|
9
21
|
'arrow-up': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M12 19V5" }), _jsx("path", { d: "m5 12 7-7 7 7" })] })),
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { KeyboardEvent, RefObject } from 'react';
|
|
2
|
+
/** DOM-order focus candidates within root, excluding the floating panel's own controls. */
|
|
3
|
+
export declare function focusableInDomOrder({ exclude }: {
|
|
4
|
+
exclude: HTMLElement | null;
|
|
5
|
+
}, { root }?: {
|
|
6
|
+
root?: ParentNode | undefined;
|
|
7
|
+
}): HTMLElement[];
|
|
8
|
+
/** Find the trigger's Tab neighbour, wrapping within its modal dialog to avoid inert page controls. */
|
|
9
|
+
export declare function resolveTabTarget({ panel, trigger, shiftKey }: {
|
|
10
|
+
panel: HTMLElement | null;
|
|
11
|
+
trigger: HTMLElement | null;
|
|
12
|
+
shiftKey: boolean;
|
|
13
|
+
}, _optional?: Record<string, never>): HTMLElement | null;
|
|
14
|
+
/** Resolve a portal inside the trigger's native dialog so its menu stays above inert page content. */
|
|
15
|
+
export declare function resolveSelectPortalContainer({ trigger }: {
|
|
16
|
+
trigger: HTMLElement | null;
|
|
17
|
+
}, { container }?: {
|
|
18
|
+
container?: Element | DocumentFragment | null | undefined;
|
|
19
|
+
}): Element | DocumentFragment;
|
|
20
|
+
/** Pure containment decision; outside clicks must neither select an option nor steal focus. */
|
|
21
|
+
export declare function isSelectEventInside({ target, trigger, panel }: {
|
|
22
|
+
target: Node;
|
|
23
|
+
trigger: HTMLElement | null;
|
|
24
|
+
panel: HTMLElement | null;
|
|
25
|
+
}, _optional?: Record<string, never>): boolean;
|
|
26
|
+
/** Apply the variant's geometry port; widget selects additionally dismiss a clipped/obscured trigger. */
|
|
27
|
+
export declare function repositionSelectPopover<P>({ trigger, measurePosition, setPosition }: {
|
|
28
|
+
trigger: HTMLElement;
|
|
29
|
+
measurePosition: (input: {
|
|
30
|
+
trigger: HTMLElement;
|
|
31
|
+
}) => P;
|
|
32
|
+
setPosition: (position: P) => void;
|
|
33
|
+
}, { onOutOfView }?: {
|
|
34
|
+
onOutOfView?: (() => void) | undefined;
|
|
35
|
+
}): void;
|
|
36
|
+
/** Measure before mounting/focusing a portal, then keep it anchored until close; listeners are disposed on close/unmount.
|
|
37
|
+
* Geometry is O(1); focus/visibility effects use the live DOM. The port retains each variant's existing placement bounds.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useSelectPopoverPosition<P>(required: {
|
|
40
|
+
open: boolean;
|
|
41
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
42
|
+
panelRef: RefObject<HTMLDivElement | null>;
|
|
43
|
+
measurePosition: (input: {
|
|
44
|
+
trigger: HTMLElement;
|
|
45
|
+
}) => P;
|
|
46
|
+
}, optional?: {
|
|
47
|
+
portal?: boolean | undefined;
|
|
48
|
+
focusInitial?: ((input: {
|
|
49
|
+
panel: HTMLDivElement | null;
|
|
50
|
+
}) => void) | undefined;
|
|
51
|
+
onOutOfView?: (() => void) | undefined;
|
|
52
|
+
}): {
|
|
53
|
+
position: P | null;
|
|
54
|
+
setPosition: import("react").Dispatch<import("react").SetStateAction<P | null>>;
|
|
55
|
+
updatePosition: () => void;
|
|
56
|
+
};
|
|
57
|
+
/** Open/close, outside dismissal and Escape/Tab focus handling shared by the public select variants.
|
|
58
|
+
* Tab scans the focusable DOM in O(n); other lifecycle transitions are O(1).
|
|
59
|
+
* @example useSelectPopover({ measurePosition }, { focusInitial: ({ panel }) => panel?.focus() })
|
|
60
|
+
*/
|
|
61
|
+
export declare function useSelectPopover<P>(required: {
|
|
62
|
+
measurePosition: (input: {
|
|
63
|
+
trigger: HTMLElement;
|
|
64
|
+
}) => P;
|
|
65
|
+
}, optional?: {
|
|
66
|
+
portal?: boolean | undefined;
|
|
67
|
+
disabled?: boolean | undefined;
|
|
68
|
+
dismissWhenHidden?: boolean | undefined;
|
|
69
|
+
focusInitial?: ((input: {
|
|
70
|
+
panel: HTMLDivElement | null;
|
|
71
|
+
}) => void) | undefined;
|
|
72
|
+
onOpenChange?: ((open: boolean) => void) | undefined;
|
|
73
|
+
}): {
|
|
74
|
+
openPanel: () => void;
|
|
75
|
+
closePanel: ({ refocusTrigger }: {
|
|
76
|
+
refocusTrigger: boolean;
|
|
77
|
+
}) => void;
|
|
78
|
+
handleDismissKey: (event: KeyboardEvent<HTMLElement>) => boolean;
|
|
79
|
+
position: P | null;
|
|
80
|
+
setPosition: import("react").Dispatch<import("react").SetStateAction<P | null>>;
|
|
81
|
+
updatePosition: () => void;
|
|
82
|
+
open: boolean;
|
|
83
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
84
|
+
panelRef: RefObject<HTMLDivElement | null>;
|
|
85
|
+
};
|
|
86
|
+
/** Open-key decision shared by variants; returns false for unrelated keys or an already-open/disabled trigger. */
|
|
87
|
+
export declare function openSelectOnKey({ event, open, openPanel }: {
|
|
88
|
+
event: KeyboardEvent<HTMLElement>;
|
|
89
|
+
open: boolean;
|
|
90
|
+
openPanel: () => void;
|
|
91
|
+
}, { disabled }?: {
|
|
92
|
+
disabled?: boolean | undefined;
|
|
93
|
+
}): boolean;
|
|
94
|
+
/** Shared keyboard dispatch; adapters map navigation onto their enabled-value or filtered-index contract. */
|
|
95
|
+
export declare function applySelectNavigationKey({ key, actions }: {
|
|
96
|
+
key: string;
|
|
97
|
+
actions: {
|
|
98
|
+
preventDefault: () => void;
|
|
99
|
+
move: (delta: 1 | -1) => void;
|
|
100
|
+
edge: (edge: 'first' | 'last') => void;
|
|
101
|
+
choose: () => void;
|
|
102
|
+
};
|
|
103
|
+
}, { spaceChooses }?: {
|
|
104
|
+
spaceChooses?: boolean;
|
|
105
|
+
}): boolean;
|
|
106
|
+
//# sourceMappingURL=select-popover.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/** Shared DOM lifecycle for the two custom select variants. Option/search policy stays in each adapter. */
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
// The panel is portaled to the end of document.body, its dialog, or a host container. Native Tab
|
|
4
|
+
// would follow that DOM position toward browser chrome; walk the trigger real DOM-order
|
|
5
|
+
// neighbours instead, staying inside a modal dialog when one owns the trigger.
|
|
6
|
+
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
7
|
+
/** DOM-order focus candidates within root, excluding the floating panel's own controls. */
|
|
8
|
+
export function focusableInDomOrder({ exclude }, { root = document } = {}) {
|
|
9
|
+
return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => !exclude || !exclude.contains(el));
|
|
10
|
+
}
|
|
11
|
+
/** Find the trigger's Tab neighbour, wrapping within its modal dialog to avoid inert page controls. */
|
|
12
|
+
export function resolveTabTarget({ panel, trigger, shiftKey }, _optional = {}) {
|
|
13
|
+
const dialog = trigger?.closest("dialog[open]");
|
|
14
|
+
// An open non-modal dialog permits focus outside it; only showModal() creates inert neighbours.
|
|
15
|
+
const modalDialog = dialog?.matches(":modal") ? dialog : null;
|
|
16
|
+
const nodes = focusableInDomOrder({ exclude: panel }, { root: modalDialog ?? document });
|
|
17
|
+
const triggerIndex = trigger ? nodes.indexOf(trigger) : -1;
|
|
18
|
+
if (triggerIndex < 0)
|
|
19
|
+
return null;
|
|
20
|
+
const nextIndex = triggerIndex + (shiftKey ? -1 : 1);
|
|
21
|
+
return nodes[modalDialog ? (nextIndex + nodes.length) % nodes.length : nextIndex] ?? null;
|
|
22
|
+
}
|
|
23
|
+
/** Resolve a portal inside the trigger's native dialog so its menu stays above inert page content. */
|
|
24
|
+
export function resolveSelectPortalContainer({ trigger }, { container } = {}) {
|
|
25
|
+
return container ?? trigger?.closest('dialog') ?? document.body;
|
|
26
|
+
}
|
|
27
|
+
/** Pure containment decision; outside clicks must neither select an option nor steal focus. */
|
|
28
|
+
export function isSelectEventInside({ target, trigger, panel }, _optional = {}) {
|
|
29
|
+
return Boolean(trigger?.contains(target) || panel?.contains(target));
|
|
30
|
+
}
|
|
31
|
+
/** Apply the variant's geometry port; widget selects additionally dismiss a clipped/obscured trigger. */
|
|
32
|
+
export function repositionSelectPopover({ trigger, measurePosition, setPosition }, { onOutOfView } = {}) {
|
|
33
|
+
if (onOutOfView) {
|
|
34
|
+
const rect = trigger.getBoundingClientRect();
|
|
35
|
+
const outOfViewport = rect.bottom <= 0 || rect.top >= window.innerHeight || rect.right <= 0 || rect.left >= window.innerWidth;
|
|
36
|
+
if (outOfViewport) {
|
|
37
|
+
onOutOfView();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (typeof document.elementFromPoint === 'function') {
|
|
41
|
+
const topmostAtCenter = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
42
|
+
const obscured = !topmostAtCenter || !(trigger.contains(topmostAtCenter) || topmostAtCenter.contains(trigger));
|
|
43
|
+
if (obscured) {
|
|
44
|
+
onOutOfView();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setPosition(measurePosition({ trigger }));
|
|
50
|
+
}
|
|
51
|
+
/** Measure before mounting/focusing a portal, then keep it anchored until close; listeners are disposed on close/unmount.
|
|
52
|
+
* Geometry is O(1); focus/visibility effects use the live DOM. The port retains each variant's existing placement bounds.
|
|
53
|
+
*/
|
|
54
|
+
export function useSelectPopoverPosition(required, optional = {}) {
|
|
55
|
+
const { open, triggerRef, panelRef } = required, portal = optional.portal !== false;
|
|
56
|
+
const latest = useRef({ required, optional });
|
|
57
|
+
latest.current = { required, optional };
|
|
58
|
+
const [position, setPosition] = useState(null);
|
|
59
|
+
const updatePosition = useCallback(() => {
|
|
60
|
+
const trigger = triggerRef.current;
|
|
61
|
+
if (trigger)
|
|
62
|
+
setPosition(latest.current.required.measurePosition({ trigger }));
|
|
63
|
+
}, [triggerRef]);
|
|
64
|
+
// Two phases, deliberately in one effect rather than two: (1) `position` starts `null` on every
|
|
65
|
+
// open (`closePanel` resets it), so the panel/`createPortal` call below isn't rendered at all yet
|
|
66
|
+
// — `panelRef`/`searchInputRef` are still unattached, so focusing them here would silently no-op.
|
|
67
|
+
// Computing `position` schedules a state update; because this is a `useLayoutEffect`, React
|
|
68
|
+
// re-renders and re-runs layout effects synchronously before the browser paints, rather than
|
|
69
|
+
// waiting a frame. (2) On that second pass `position` is non-null, the portal now exists in the
|
|
70
|
+
// DOM, and the refs are populated — only now is it correct to move focus into it. Found live via
|
|
71
|
+
// this component's own tests: every keyboard-driven test failed until this was split out, because
|
|
72
|
+
// `panelRef.current`/`searchInputRef.current` were both still `null` when focus was attempted in
|
|
73
|
+
// the same pass that first computed the position.
|
|
74
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: two-phase focus effect — see comment above; changing search/focus callbacks are read through latest and intentionally do not redrive it.
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
if (!open) {
|
|
77
|
+
setPosition(null);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (portal && !position) {
|
|
81
|
+
updatePosition();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
latest.current.optional.focusInitial?.({ panel: panelRef.current });
|
|
85
|
+
}, [open, position, portal, updatePosition, panelRef]);
|
|
86
|
+
// Keeps the panel anchored to the trigger if the surrounding page/dialog scrolls or resizes while
|
|
87
|
+
// it's open — `.widget-picker-body`'s own `overflow-y: auto` made this a real, live scenario
|
|
88
|
+
// rather than a hypothetical one. `scroll` needs the capture phase: an inner element's own scroll
|
|
89
|
+
// does not bubble to `window` in the bubble phase. The decision logic itself is
|
|
90
|
+
// `repositionSelectPopover`, above; this effect is only the DOM listener wiring.
|
|
91
|
+
//
|
|
92
|
+
// Found live in a real browser, not by this component's own test suite: scrolling a host's
|
|
93
|
+
// scrolling ancestor far enough that the trigger scrolls out of its own visible clip used to leave
|
|
94
|
+
// this effect happily recomputing a mathematically-correct `position` for it — the panel just
|
|
95
|
+
// ended up following the trigger to an off-screen (or behind-the-header, obscured) spot, floating
|
|
96
|
+
// there indefinitely, nominally still "open" with no visible way to tell.
|
|
97
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll/resize listener wiring — see comment above; onOutOfView is read through latest; only lifecycle/ref changes re-arm it.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!open || !portal)
|
|
100
|
+
return;
|
|
101
|
+
function reposition() {
|
|
102
|
+
const trigger = triggerRef.current;
|
|
103
|
+
if (!trigger)
|
|
104
|
+
return;
|
|
105
|
+
repositionSelectPopover({ trigger, measurePosition: latest.current.required.measurePosition, setPosition }, { onOutOfView: latest.current.optional.onOutOfView });
|
|
106
|
+
}
|
|
107
|
+
window.addEventListener('scroll', reposition, true);
|
|
108
|
+
window.addEventListener('resize', reposition);
|
|
109
|
+
return () => {
|
|
110
|
+
window.removeEventListener('scroll', reposition, true);
|
|
111
|
+
window.removeEventListener('resize', reposition);
|
|
112
|
+
};
|
|
113
|
+
}, [open, portal, triggerRef]);
|
|
114
|
+
return { position, setPosition, updatePosition };
|
|
115
|
+
}
|
|
116
|
+
/** Open/close, outside dismissal and Escape/Tab focus handling shared by the public select variants.
|
|
117
|
+
* Tab scans the focusable DOM in O(n); other lifecycle transitions are O(1).
|
|
118
|
+
* @example useSelectPopover({ measurePosition }, { focusInitial: ({ panel }) => panel?.focus() })
|
|
119
|
+
*/
|
|
120
|
+
export function useSelectPopover(required, optional = {}) {
|
|
121
|
+
const [open, setOpen] = useState(false);
|
|
122
|
+
const triggerRef = useRef(null), panelRef = useRef(null);
|
|
123
|
+
// Stored in a ref (not a `useEffect` dependency) so an unmemoized caller
|
|
124
|
+
// passing a fresh function identity every render never re-fires this for
|
|
125
|
+
// any reason OTHER than `open` actually changing.
|
|
126
|
+
const latest = useRef(optional);
|
|
127
|
+
latest.current = optional;
|
|
128
|
+
useEffect(() => { latest.current.onOpenChange?.(open); }, [open]);
|
|
129
|
+
const placement = useSelectPopoverPosition({ open, triggerRef, panelRef, measurePosition: required.measurePosition }, {
|
|
130
|
+
portal: optional.portal, focusInitial: optional.focusInitial,
|
|
131
|
+
onOutOfView: optional.dismissWhenHidden ? () => closePanel({ refocusTrigger: false }) : undefined,
|
|
132
|
+
});
|
|
133
|
+
function openPanel() { if (!latest.current.disabled)
|
|
134
|
+
setOpen(true); }
|
|
135
|
+
function closePanel({ refocusTrigger }) {
|
|
136
|
+
setOpen(false);
|
|
137
|
+
placement.setPosition(null); // forces the effect through its "measure, then focus" two-phase sequence again on the next open, rather than focusing at a stale, pre-close position
|
|
138
|
+
if (refocusTrigger)
|
|
139
|
+
triggerRef.current?.focus();
|
|
140
|
+
}
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!open)
|
|
143
|
+
return;
|
|
144
|
+
function onOutside(event) {
|
|
145
|
+
if (isSelectEventInside({ target: event.target, trigger: triggerRef.current, panel: panelRef.current }))
|
|
146
|
+
return;
|
|
147
|
+
closePanel({ refocusTrigger: false });
|
|
148
|
+
}
|
|
149
|
+
document.addEventListener('mousedown', onOutside);
|
|
150
|
+
return () => document.removeEventListener('mousedown', onOutside);
|
|
151
|
+
}, [open]);
|
|
152
|
+
function handleDismissKey(event) {
|
|
153
|
+
if (!open)
|
|
154
|
+
return false;
|
|
155
|
+
if (event.key === 'Escape') {
|
|
156
|
+
// Must not bubble: a host dialog (`WidgetPickerDialog`) listens for Escape on `document`
|
|
157
|
+
// to cancel the whole modal. Without stopping propagation here, closing just this dropdown
|
|
158
|
+
// would also close the dialog underneath it — the regression `Select.unit.test.tsx` pins.
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
closePanel({ refocusTrigger: true });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (event.key === 'Tab') {
|
|
165
|
+
// See `resolveTabTarget`'s own comment: walk the trigger's real DOM-order neighbours rather
|
|
166
|
+
// than let native Tab handling run, since this event is bubbling from a panel portaled to
|
|
167
|
+
// the end of `document.body`, its dialog, or a host container, not sitting next to the
|
|
168
|
+
// trigger in the DOM. Dialog-local traversal avoids focusing inert page neighbours.
|
|
169
|
+
if (optional.portal !== false) {
|
|
170
|
+
const target = resolveTabTarget({ panel: panelRef.current, trigger: triggerRef.current, shiftKey: event.shiftKey });
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
closePanel({ refocusTrigger: false });
|
|
173
|
+
target?.focus();
|
|
174
|
+
}
|
|
175
|
+
else
|
|
176
|
+
closePanel({ refocusTrigger: false });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return { open, triggerRef, panelRef, ...placement, openPanel, closePanel, handleDismissKey };
|
|
182
|
+
}
|
|
183
|
+
/** Open-key decision shared by variants; returns false for unrelated keys or an already-open/disabled trigger. */
|
|
184
|
+
export function openSelectOnKey({ event, open, openPanel }, { disabled } = {}) {
|
|
185
|
+
if (disabled || open || !['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key))
|
|
186
|
+
return false;
|
|
187
|
+
event.preventDefault();
|
|
188
|
+
openPanel();
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
/** Shared keyboard dispatch; adapters map navigation onto their enabled-value or filtered-index contract. */
|
|
192
|
+
export function applySelectNavigationKey({ key, actions }, { spaceChooses = false } = {}) {
|
|
193
|
+
if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
194
|
+
actions.preventDefault();
|
|
195
|
+
actions.move(key === 'ArrowDown' ? 1 : -1);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
if (key === 'Home' || key === 'End') {
|
|
199
|
+
actions.preventDefault();
|
|
200
|
+
actions.edge(key === 'Home' ? 'first' : 'last');
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (key === 'Enter' || (spaceChooses && key === ' ')) {
|
|
204
|
+
actions.preventDefault();
|
|
205
|
+
actions.choose();
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=select-popover.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared sandbox script generation. The builders have established byte-level
|
|
3
|
+
* contracts: srcdoc uses modern link-handler syntax and a different focus
|
|
4
|
+
* marker/listener spelling. Keep those serialization differences here; each
|
|
5
|
+
* builder still owns its injection positions, defaults, CSP and bridges.
|
|
6
|
+
*/
|
|
7
|
+
type ScriptFormat = 'sandbox' | 'srcdoc';
|
|
8
|
+
/**
|
|
9
|
+
* Script that shims `localStorage`/`sessionStorage` with an in-memory store
|
|
10
|
+
* when the real Storage API throws, and intercepts `<a href>` clicks so
|
|
11
|
+
* hash-only links scroll within the document (instead of attempting a
|
|
12
|
+
* sandboxed top-level navigation) and `target="_blank"` links open via
|
|
13
|
+
* `window.open` behind an `http:`/`https:`/`mailto:` scheme allow-list
|
|
14
|
+
* (instead of being silently blocked by the iframe sandbox).
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildStorageScript({ format }: {
|
|
17
|
+
format: ScriptFormat;
|
|
18
|
+
}, _optional?: Record<string, never>): string;
|
|
19
|
+
/**
|
|
20
|
+
* Script that suppresses `focus()` calls (on `window` or any element) that
|
|
21
|
+
* aren't the direct result of a real pointer/keyboard event within the
|
|
22
|
+
* last second, so embedded content can't steal keyboard focus away from
|
|
23
|
+
* the host page around it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildFocusScript({ format }: {
|
|
26
|
+
format: ScriptFormat;
|
|
27
|
+
}, _optional?: Record<string, never>): string;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=sandbox-scripts.d.ts.map
|