@juspay/svelte-ui-components 4.27.5 → 4.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -36
- package/dist/Animations/ModalAnimation.svelte +15 -1
- package/dist/Animations/OverlayAnimation.svelte +7 -1
- package/dist/AreaChart/AreaChart.svelte +366 -85
- package/dist/AspectRatio/AspectRatio.svelte +51 -0
- package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
- package/dist/AspectRatio/properties.d.ts +27 -0
- package/dist/AspectRatio/properties.js +1 -0
- package/dist/Banner/Banner.svelte +10 -4
- package/dist/Banner/properties.d.ts +8 -0
- package/dist/BarChart/BarChart.svelte +156 -49
- package/dist/Book/Book.svelte +14 -1
- package/dist/BrandLoader/BrandLoader.svelte +76 -12
- package/dist/Button/Button.svelte +56 -11
- package/dist/Calendar/Calendar.svelte +88 -61
- package/dist/Card/Card.svelte +1 -1
- package/dist/Carousel/Carousel.svelte +6 -3
- package/dist/ChatComposer/ChatComposer.svelte +54 -4
- package/dist/ChatComposer/dictationState.d.ts +15 -0
- package/dist/ChatComposer/dictationState.js +9 -0
- package/dist/ChatComposer/properties.d.ts +28 -1
- package/dist/ChatMessage/ChatMessage.svelte +27 -1
- package/dist/CheckListItem/CheckListItem.svelte +3 -3
- package/dist/Checkbox/Checkbox.svelte +84 -3
- package/dist/Checkbox/properties.d.ts +45 -13
- package/dist/ChipInput/ChipInput.svelte +70 -3
- package/dist/ChipInput/properties.d.ts +16 -0
- package/dist/Choicebox/Choicebox.svelte +156 -1
- package/dist/Choicebox/group.d.ts +63 -0
- package/dist/Choicebox/group.js +127 -0
- package/dist/Choicebox/properties.d.ts +46 -0
- package/dist/ColorPicker/ColorPicker.svelte +169 -20
- package/dist/ColorPicker/properties.d.ts +16 -0
- package/dist/Combobox/Combobox.svelte +87 -25
- package/dist/Combobox/properties.d.ts +23 -0
- package/dist/CommandMenu/CommandMenu.svelte +27 -11
- package/dist/ContextMenu/ContextMenu.svelte +31 -37
- package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
- package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
- package/dist/Draggable/Draggable.svelte +12 -7
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
- package/dist/EmptyState/EmptyState.svelte +1 -1
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
- package/dist/FileInput/FileInput.svelte +50 -1
- package/dist/FileInput/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +40 -12
- package/dist/FunnelChart/properties.d.ts +7 -1
- package/dist/Gallery/Gallery.svelte +20 -13
- package/dist/Gallery/properties.d.ts +8 -0
- package/dist/GridItem/GridItem.svelte +31 -1
- package/dist/HITL/HITL.svelte +35 -9
- package/dist/Icon/Icon.svelte +19 -3
- package/dist/Input/Input.svelte +24 -10
- package/dist/Input/properties.d.ts +37 -1
- package/dist/InputButton/InputButton.svelte +86 -11
- package/dist/InputButton/properties.d.ts +10 -1
- package/dist/Label/Label.svelte +54 -0
- package/dist/Label/Label.svelte.d.ts +3 -0
- package/dist/Label/properties.d.ts +26 -0
- package/dist/Label/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +385 -94
- package/dist/ListItem/ListItem.svelte +121 -15
- package/dist/Loader/Loader.svelte +22 -2
- package/dist/LoadingDots/LoadingDots.svelte +21 -0
- package/dist/MediaUpload/MediaUpload.svelte +13 -3
- package/dist/Menu/Menu.svelte +56 -28
- package/dist/Modal/Modal.svelte +77 -42
- package/dist/Pagination/Pagination.svelte +9 -2
- package/dist/Phone/Phone.svelte +4 -0
- package/dist/PieChart/PieChart.svelte +282 -38
- package/dist/PieChart/properties.d.ts +9 -0
- package/dist/Pill/Pill.svelte +14 -5
- package/dist/Progress/Progress.svelte +20 -0
- package/dist/Radio/Radio.svelte +58 -1
- package/dist/Radio/properties.d.ts +23 -0
- package/dist/RatingGroup/RatingGroup.svelte +283 -0
- package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
- package/dist/RatingGroup/properties.d.ts +73 -0
- package/dist/RatingGroup/properties.js +1 -0
- package/dist/RelativeTime/RelativeTime.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +272 -50
- package/dist/SankeyChart/properties.d.ts +9 -0
- package/dist/Scroller/Scroller.svelte +37 -2
- package/dist/Select/Select.svelte +166 -46
- package/dist/Select/properties.d.ts +7 -0
- package/dist/Separator/Separator.svelte +48 -0
- package/dist/Separator/Separator.svelte.d.ts +3 -0
- package/dist/Separator/properties.d.ts +22 -0
- package/dist/Separator/properties.js +1 -0
- package/dist/Sheet/Sheet.svelte +60 -21
- package/dist/Shimmer/Shimmer.svelte +15 -0
- package/dist/Slider/Slider.svelte +87 -6
- package/dist/Slider/properties.d.ts +26 -0
- package/dist/Snippet/Snippet.svelte +4 -0
- package/dist/Snippet/copyState.svelte.d.ts +13 -1
- package/dist/Snippet/copyState.svelte.js +27 -1
- package/dist/Snippet/properties.d.ts +7 -0
- package/dist/SoundKit/SoundKit.js +9 -8
- package/dist/SplitInput/SplitInput.svelte +68 -2
- package/dist/SplitInput/properties.d.ts +23 -0
- package/dist/StatCard/StatCard.svelte +6 -6
- package/dist/Status/Status.svelte +2 -2
- package/dist/Stepper/Step.svelte +19 -6
- package/dist/Stepper/Stepper.svelte +108 -35
- package/dist/Table/BuiltinCell.svelte +2 -2
- package/dist/Table/Table.svelte +373 -55
- package/dist/Table/properties.d.ts +134 -3
- package/dist/Tabs/Tabs.svelte +263 -65
- package/dist/Tabs/properties.d.ts +23 -0
- package/dist/TaskList/TaskList.svelte +14 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
- package/dist/Toast/Toast.svelte +27 -2
- package/dist/Toggle/Toggle.svelte +79 -7
- package/dist/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/Toggle/properties.d.ts +30 -7
- package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
- package/dist/ToolCallLog/properties.d.ts +10 -0
- package/dist/Toolbar/Toolbar.svelte +1 -1
- package/dist/Tooltip/Tooltip.svelte +20 -2
- package/dist/Tooltip/tooltip-action.js +39 -4
- package/dist/TypewriterText/TypewriterText.svelte +34 -2
- package/dist/TypewriterText/properties.d.ts +18 -5
- package/dist/_chart/ChartContainer.svelte +12 -0
- package/dist/_chart/ChartTooltip.svelte +18 -4
- package/dist/_chart/Legend.svelte +21 -0
- package/dist/_chart/Legend.svelte.d.ts +15 -0
- package/dist/_chart/geometry.d.ts +54 -1
- package/dist/_chart/geometry.js +122 -14
- package/dist/_chart/interactions.d.ts +5 -1
- package/dist/_chart/interactions.js +9 -5
- package/dist/_chart/scales.js +16 -0
- package/dist/_chart/temporal.d.ts +45 -0
- package/dist/_chart/temporal.js +116 -0
- package/dist/_field/description.d.ts +50 -0
- package/dist/_field/description.js +51 -0
- package/dist/_interaction/dismissal.d.ts +67 -0
- package/dist/_interaction/dismissal.js +137 -0
- package/dist/_interaction/focus.d.ts +73 -0
- package/dist/_interaction/focus.js +143 -0
- package/dist/assets/star.svg +5 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +21 -1
- package/dist/reduced-motion.svelte.d.ts +37 -0
- package/dist/reduced-motion.svelte.js +44 -0
- package/dist/styles/theme-dark.css +778 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +62 -1
- package/dist/utils.js +80 -21
- package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
- package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
- package/dist-codemod/codemod/migrate-audit.js +395 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
- package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
- package/dist-codemod/migrate/analyze.js +804 -0
- package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
- package/dist-wc/index.js +13992 -4323
- package/package.json +13 -6
- package/dist/Modal/focus-trap.d.ts +0 -42
- package/dist/Modal/focus-trap.js +0 -57
- /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
- /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
- /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documented, tested calendar-tick helpers for the temporal limitation in
|
|
3
|
+
* Line/AreaChart's x axis -- see docs/CHART_INPUT_POLICY.md#calendar--temporal-axes
|
|
4
|
+
* for the full policy this satisfies.
|
|
5
|
+
*
|
|
6
|
+
* Line/AreaChart's x axis is a plain numeric linear scale (`createLinearScale`
|
|
7
|
+
* in `./scales`). Epoch-millisecond x values preserve relative distance --
|
|
8
|
+
* `t2 - t1` still means the same thing in pixels as it does in time -- but
|
|
9
|
+
* `computeLinearTicks`'s "nice round number" ticks are not calendar-aware:
|
|
10
|
+
* they land on values like 1_700_000_000_000 or a rounded step in
|
|
11
|
+
* milliseconds, not on UTC midnight/week/month boundaries. `xTickFormat`
|
|
12
|
+
* only changes how a tick's existing numeric position is labelled; it cannot
|
|
13
|
+
* move that position onto a calendar boundary, and neither chart accepts an
|
|
14
|
+
* explicit tick-position override today (`Axis.svelte`, shared by every chart
|
|
15
|
+
* type in this package, always derives its own tick positions from the
|
|
16
|
+
* scale) -- wiring one in is deliberately out of scope here, since it would
|
|
17
|
+
* change a contract every other chart consumes, not just these two.
|
|
18
|
+
*
|
|
19
|
+
* `computeUtcTicks`/`formatUtcTick` are the disclosed alternative: pure,
|
|
20
|
+
* fully-tested functions a caller can use to compute correct UTC calendar
|
|
21
|
+
* tick positions and labels for their own presentation (a custom axis
|
|
22
|
+
* overlay, a legend of period boundaries, or pre-bucketing data into
|
|
23
|
+
* calendar-aligned x values before it ever reaches the chart). They do not
|
|
24
|
+
* replace the chart's linear scale with array indices, and they impose
|
|
25
|
+
* nothing on a caller who does not import this module -- both charts behave
|
|
26
|
+
* exactly as before.
|
|
27
|
+
*/
|
|
28
|
+
const MS_PER_DAY = 86_400_000;
|
|
29
|
+
const MONTH_NAMES = [
|
|
30
|
+
'Jan',
|
|
31
|
+
'Feb',
|
|
32
|
+
'Mar',
|
|
33
|
+
'Apr',
|
|
34
|
+
'May',
|
|
35
|
+
'Jun',
|
|
36
|
+
'Jul',
|
|
37
|
+
'Aug',
|
|
38
|
+
'Sep',
|
|
39
|
+
'Oct',
|
|
40
|
+
'Nov',
|
|
41
|
+
'Dec'
|
|
42
|
+
];
|
|
43
|
+
function startOfUtcDay(ms) {
|
|
44
|
+
const d = new Date(ms);
|
|
45
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
46
|
+
}
|
|
47
|
+
function startOfUtcMonth(ms) {
|
|
48
|
+
const d = new Date(ms);
|
|
49
|
+
return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Computes UTC calendar-aligned tick positions (epoch ms) covering
|
|
53
|
+
* `[domain[0], domain[1]]`. `unit` picks the calendar grain; ticks are
|
|
54
|
+
* thinned so the count stays at or under `maxTicks` (mirrors Line/AreaChart's
|
|
55
|
+
* own tick-count capping).
|
|
56
|
+
*
|
|
57
|
+
* Returns `[]` for a non-finite or inverted domain -- callers pass this
|
|
58
|
+
* straight into `xTicks`, and an empty array there just falls back to no
|
|
59
|
+
* override (see LineChart/AreaChart docs).
|
|
60
|
+
*/
|
|
61
|
+
export function computeUtcTicks(domain, unit, maxTicks = 8) {
|
|
62
|
+
const [minMs, maxMs] = domain;
|
|
63
|
+
if (!Number.isFinite(minMs) || !Number.isFinite(maxMs) || maxMs < minMs) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
if (unit === 'month') {
|
|
67
|
+
const approxMonths = Math.max(1, Math.round((maxMs - minMs) / (MS_PER_DAY * 30)));
|
|
68
|
+
const stepMonths = Math.max(1, Math.ceil((approxMonths + 1) / maxTicks));
|
|
69
|
+
const ticks = [];
|
|
70
|
+
let cursor = startOfUtcMonth(minMs);
|
|
71
|
+
// Bounded by the month count rather than a time delta -- months vary in
|
|
72
|
+
// length, so stepping by calendar month (not a fixed ms step) is what
|
|
73
|
+
// makes these boundaries actually land on the 1st in UTC.
|
|
74
|
+
let guard = 0;
|
|
75
|
+
while (cursor <= maxMs && guard < 10_000) {
|
|
76
|
+
ticks.push(cursor);
|
|
77
|
+
const d = new Date(cursor);
|
|
78
|
+
cursor = Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + stepMonths, 1);
|
|
79
|
+
guard++;
|
|
80
|
+
}
|
|
81
|
+
return ticks;
|
|
82
|
+
}
|
|
83
|
+
const stepDays = unit === 'week' ? 7 : 1;
|
|
84
|
+
const stepMs = stepDays * MS_PER_DAY;
|
|
85
|
+
const spanSteps = Math.max(1, (maxMs - minMs) / stepMs);
|
|
86
|
+
const thinning = Math.max(1, Math.ceil(spanSteps / maxTicks));
|
|
87
|
+
const ticks = [];
|
|
88
|
+
let cursor = startOfUtcDay(minMs);
|
|
89
|
+
let i = 0;
|
|
90
|
+
let guard = 0;
|
|
91
|
+
while (cursor <= maxMs && guard < 10_000) {
|
|
92
|
+
if (i % thinning === 0) {
|
|
93
|
+
ticks.push(cursor);
|
|
94
|
+
}
|
|
95
|
+
cursor += stepMs;
|
|
96
|
+
i++;
|
|
97
|
+
guard++;
|
|
98
|
+
}
|
|
99
|
+
return ticks;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Formats an epoch-ms tick value produced by `computeUtcTicks` as a UTC
|
|
103
|
+
* calendar label ("Mar 4" for day/week, "Mar 2026" for month). Pass as
|
|
104
|
+
* `xTickFormat` alongside `xTicks={computeUtcTicks(...)}`.
|
|
105
|
+
*/
|
|
106
|
+
export function formatUtcTick(value, unit = 'day') {
|
|
107
|
+
if (!Number.isFinite(value)) {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
const d = new Date(value);
|
|
111
|
+
const month = MONTH_NAMES[d.getUTCMonth()];
|
|
112
|
+
if (unit === 'month') {
|
|
113
|
+
return `${month} ${d.getUTCFullYear()}`;
|
|
114
|
+
}
|
|
115
|
+
return `${month} ${d.getUTCDate()}`;
|
|
116
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The link between a form control and the text that explains it.
|
|
3
|
+
*
|
|
4
|
+
* `Input` got this right and was the only one: it derives an id per message,
|
|
5
|
+
* composes `aria-describedby` from the messages that are actually on screen, and
|
|
6
|
+
* sets `aria-invalid` only while an error is showing. Across Select, Combobox,
|
|
7
|
+
* ChipInput, SplitInput, Slider, Radio, Checkbox, Toggle, Choicebox, ColorPicker
|
|
8
|
+
* and FileInput there was not one `aria-describedby` and not one `aria-invalid`;
|
|
9
|
+
* DateRangePicker had eight `aria-invalid` and no `aria-describedby` at all, so
|
|
10
|
+
* it could announce that a date was wrong while linking nothing that said why.
|
|
11
|
+
*
|
|
12
|
+
* Two rules are easy to get wrong and are the reason this is shared rather than
|
|
13
|
+
* copied:
|
|
14
|
+
*
|
|
15
|
+
* - **Reference only ids that exist.** `aria-describedby="x-error"` pointing at
|
|
16
|
+
* an element that is not rendered passes any attribute-presence assertion and
|
|
17
|
+
* silently resolves to nothing in a real screen reader. So the id is included
|
|
18
|
+
* only while its message is being rendered, and the caller must render each
|
|
19
|
+
* message under the same condition.
|
|
20
|
+
* - **`aria-invalid` means invalid now**, not "this control can be invalid". A
|
|
21
|
+
* control that carries it permanently tells a user every field is broken.
|
|
22
|
+
*/
|
|
23
|
+
export type FieldMessages = {
|
|
24
|
+
/** Text shown when the control is in error. Empty or null means no error. */
|
|
25
|
+
readonly error?: string | null;
|
|
26
|
+
/** Persistent helper text describing the control. */
|
|
27
|
+
readonly info?: string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Forces the error state on for consumers driving validity from a server or
|
|
30
|
+
* their own rules, independently of any message.
|
|
31
|
+
*/
|
|
32
|
+
readonly invalid?: boolean;
|
|
33
|
+
};
|
|
34
|
+
export type FieldDescription = {
|
|
35
|
+
/** Id for the error element. Render it only when `showsError` is true. */
|
|
36
|
+
readonly errorId: string;
|
|
37
|
+
/** Id for the helper element. Render it only when `showsInfo` is true. */
|
|
38
|
+
readonly infoId: string;
|
|
39
|
+
readonly showsError: boolean;
|
|
40
|
+
readonly showsInfo: boolean;
|
|
41
|
+
/** For `aria-describedby`; null when there is nothing to reference. */
|
|
42
|
+
readonly describedBy: string | null;
|
|
43
|
+
/** For `aria-invalid`; null rather than "false" so the attribute is absent. */
|
|
44
|
+
readonly ariaInvalid: 'true' | null;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* @param id The control's own id. Message ids are derived from it so two
|
|
48
|
+
* controls on one page cannot collide.
|
|
49
|
+
*/
|
|
50
|
+
export declare function describeField(id: string, messages: FieldMessages): FieldDescription;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The link between a form control and the text that explains it.
|
|
3
|
+
*
|
|
4
|
+
* `Input` got this right and was the only one: it derives an id per message,
|
|
5
|
+
* composes `aria-describedby` from the messages that are actually on screen, and
|
|
6
|
+
* sets `aria-invalid` only while an error is showing. Across Select, Combobox,
|
|
7
|
+
* ChipInput, SplitInput, Slider, Radio, Checkbox, Toggle, Choicebox, ColorPicker
|
|
8
|
+
* and FileInput there was not one `aria-describedby` and not one `aria-invalid`;
|
|
9
|
+
* DateRangePicker had eight `aria-invalid` and no `aria-describedby` at all, so
|
|
10
|
+
* it could announce that a date was wrong while linking nothing that said why.
|
|
11
|
+
*
|
|
12
|
+
* Two rules are easy to get wrong and are the reason this is shared rather than
|
|
13
|
+
* copied:
|
|
14
|
+
*
|
|
15
|
+
* - **Reference only ids that exist.** `aria-describedby="x-error"` pointing at
|
|
16
|
+
* an element that is not rendered passes any attribute-presence assertion and
|
|
17
|
+
* silently resolves to nothing in a real screen reader. So the id is included
|
|
18
|
+
* only while its message is being rendered, and the caller must render each
|
|
19
|
+
* message under the same condition.
|
|
20
|
+
* - **`aria-invalid` means invalid now**, not "this control can be invalid". A
|
|
21
|
+
* control that carries it permanently tells a user every field is broken.
|
|
22
|
+
*/
|
|
23
|
+
const hasText = (value) => typeof value === 'string' && value !== '';
|
|
24
|
+
/**
|
|
25
|
+
* @param id The control's own id. Message ids are derived from it so two
|
|
26
|
+
* controls on one page cannot collide.
|
|
27
|
+
*/
|
|
28
|
+
export function describeField(id, messages) {
|
|
29
|
+
const errorId = `${id}-error`;
|
|
30
|
+
const infoId = `${id}-info`;
|
|
31
|
+
// An explicit `invalid` with no message still marks the control invalid --
|
|
32
|
+
// that is what forceError means -- but it cannot be described by an element
|
|
33
|
+
// that will not be rendered.
|
|
34
|
+
const showsError = hasText(messages.error ?? null);
|
|
35
|
+
const showsInfo = hasText(messages.info ?? null);
|
|
36
|
+
const referenced = [];
|
|
37
|
+
if (showsError) {
|
|
38
|
+
referenced.push(errorId);
|
|
39
|
+
}
|
|
40
|
+
if (showsInfo) {
|
|
41
|
+
referenced.push(infoId);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
errorId,
|
|
45
|
+
infoId,
|
|
46
|
+
showsError,
|
|
47
|
+
showsInfo,
|
|
48
|
+
describedBy: referenced.length > 0 ? referenced.join(' ') : null,
|
|
49
|
+
ariaInvalid: showsError || messages.invalid === true ? 'true' : null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who owns a dismissal, when several overlays are open at once.
|
|
3
|
+
*
|
|
4
|
+
* Before this module each overlay listened wherever suited it: Modal on the
|
|
5
|
+
* window, Sheet only on its own overlay element, Menu and ContextMenu on the
|
|
6
|
+
* document, CommandMenu on the window again. Nothing coordinated them, so one
|
|
7
|
+
* Escape reached every listener and a menu opened inside a modal closed both --
|
|
8
|
+
* the menu the user meant, and the modal behind it they did not.
|
|
9
|
+
*
|
|
10
|
+
* Ownership here is an explicit registration order, not something inferred from
|
|
11
|
+
* DOM nesting. Nesting is the wrong signal: a portaled dropdown is a sibling of
|
|
12
|
+
* the modal it visually sits inside, and a menu anchored in the light DOM is not
|
|
13
|
+
* a descendant of the shadow root that opened it. What actually decides which
|
|
14
|
+
* overlay a dismissal belongs to is which one opened last, and only the overlay
|
|
15
|
+
* itself knows when that happened -- so it says so, and says so again when it
|
|
16
|
+
* closes.
|
|
17
|
+
*
|
|
18
|
+
* Registration returns its own release function rather than exposing the stack,
|
|
19
|
+
* so a layer can only ever remove itself, and removal from the middle is safe:
|
|
20
|
+
* an overlay that closes underneath another leaves the one above it topmost.
|
|
21
|
+
*/
|
|
22
|
+
export type DismissibleLayer = {
|
|
23
|
+
/**
|
|
24
|
+
* What counts as "inside" this layer, read at event time rather than captured
|
|
25
|
+
* at registration -- the element frequently does not exist yet when a layer
|
|
26
|
+
* registers, and can be replaced while it stays open.
|
|
27
|
+
*/
|
|
28
|
+
readonly element: () => Node | null;
|
|
29
|
+
/** Escape was pressed while this layer was topmost. */
|
|
30
|
+
readonly onEscape?: () => void;
|
|
31
|
+
/** A pointer went down outside this layer while it was topmost. */
|
|
32
|
+
readonly onOutside?: (event: Event) => void;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Registers `layer` as the topmost dismissible surface and returns the function
|
|
36
|
+
* that removes it again. Calling that function more than once is harmless.
|
|
37
|
+
*/
|
|
38
|
+
export declare function registerDismissible(layer: DismissibleLayer): () => void;
|
|
39
|
+
/** How many layers are currently registered. Exists for tests and diagnostics. */
|
|
40
|
+
export declare function dismissibleLayerCount(): number;
|
|
41
|
+
/**
|
|
42
|
+
* Does this event's journey pass through `container`?
|
|
43
|
+
*
|
|
44
|
+
* `container.contains(event.target)` is the obvious test and it is wrong the
|
|
45
|
+
* moment a shadow boundary is involved. Once an event crosses one, `target` is
|
|
46
|
+
* retargeted to the HOST element -- an ANCESTOR of everything in the shadow
|
|
47
|
+
* root -- so `contains()` returns false for a click on the container itself.
|
|
48
|
+
* An outside-click handler written that way treats every click as outside and
|
|
49
|
+
* closes the panel on the same click that opened it.
|
|
50
|
+
*
|
|
51
|
+
* `composedPath()` is unaffected: it lists every node the event actually
|
|
52
|
+
* travelled through, inside shadow roots included. The `contains` fallback is
|
|
53
|
+
* for a synthetic event with no composed path, which is the only case where the
|
|
54
|
+
* retargeting problem cannot arise.
|
|
55
|
+
*/
|
|
56
|
+
export declare function eventHitsInside(container: Node | null, event: Event): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The element the user actually acted on, not the one the DOM admits to.
|
|
59
|
+
*
|
|
60
|
+
* A listener on `document` sees `event.target` retargeted to the shadow HOST,
|
|
61
|
+
* so `target.closest(selector)` walks the light tree and never sees the node
|
|
62
|
+
* that was clicked. `composedPath()[0]` is the real innermost target, and
|
|
63
|
+
* walking the path is the shadow-safe form of `closest()` -- the path already
|
|
64
|
+
* spans every root the event crossed, so an ancestor search over it finds
|
|
65
|
+
* matches inside the shadow tree and outside it in one pass.
|
|
66
|
+
*/
|
|
67
|
+
export declare function closestInEventPath(event: Event, selector: string): Element | null;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who owns a dismissal, when several overlays are open at once.
|
|
3
|
+
*
|
|
4
|
+
* Before this module each overlay listened wherever suited it: Modal on the
|
|
5
|
+
* window, Sheet only on its own overlay element, Menu and ContextMenu on the
|
|
6
|
+
* document, CommandMenu on the window again. Nothing coordinated them, so one
|
|
7
|
+
* Escape reached every listener and a menu opened inside a modal closed both --
|
|
8
|
+
* the menu the user meant, and the modal behind it they did not.
|
|
9
|
+
*
|
|
10
|
+
* Ownership here is an explicit registration order, not something inferred from
|
|
11
|
+
* DOM nesting. Nesting is the wrong signal: a portaled dropdown is a sibling of
|
|
12
|
+
* the modal it visually sits inside, and a menu anchored in the light DOM is not
|
|
13
|
+
* a descendant of the shadow root that opened it. What actually decides which
|
|
14
|
+
* overlay a dismissal belongs to is which one opened last, and only the overlay
|
|
15
|
+
* itself knows when that happened -- so it says so, and says so again when it
|
|
16
|
+
* closes.
|
|
17
|
+
*
|
|
18
|
+
* Registration returns its own release function rather than exposing the stack,
|
|
19
|
+
* so a layer can only ever remove itself, and removal from the middle is safe:
|
|
20
|
+
* an overlay that closes underneath another leaves the one above it topmost.
|
|
21
|
+
*/
|
|
22
|
+
const stack = [];
|
|
23
|
+
function topmost() {
|
|
24
|
+
return stack.length === 0 ? null : stack[stack.length - 1];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether the event started inside `node`. Uses the composed path so a press
|
|
28
|
+
* inside a `sui-*` element's shadow root still counts as inside: `event.target`
|
|
29
|
+
* is retargeted to the shadow host by the time a document listener sees it,
|
|
30
|
+
* which would report every press inside a custom element as an outside click.
|
|
31
|
+
*/
|
|
32
|
+
function startedInside(event, node) {
|
|
33
|
+
return eventHitsInside(node, event);
|
|
34
|
+
}
|
|
35
|
+
function handleKeydown(event) {
|
|
36
|
+
if (event.key !== 'Escape') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
topmost()?.onEscape?.();
|
|
40
|
+
}
|
|
41
|
+
function handlePointerdown(event) {
|
|
42
|
+
const layer = topmost();
|
|
43
|
+
if (layer === null || startedInside(event, layer.element())) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
layer.onOutside?.(event);
|
|
47
|
+
}
|
|
48
|
+
// Capture phase, so the owning layer is decided before any component's own
|
|
49
|
+
// handlers run and before a click can be swallowed by something in between.
|
|
50
|
+
const LISTENER_OPTIONS = { capture: true };
|
|
51
|
+
function setListeners(attach) {
|
|
52
|
+
if (typeof document === 'undefined') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const apply = attach
|
|
56
|
+
? document.addEventListener.bind(document)
|
|
57
|
+
: document.removeEventListener.bind(document);
|
|
58
|
+
apply('keydown', handleKeydown, LISTENER_OPTIONS);
|
|
59
|
+
apply('pointerdown', handlePointerdown, LISTENER_OPTIONS);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Registers `layer` as the topmost dismissible surface and returns the function
|
|
63
|
+
* that removes it again. Calling that function more than once is harmless.
|
|
64
|
+
*/
|
|
65
|
+
export function registerDismissible(layer) {
|
|
66
|
+
stack.push(layer);
|
|
67
|
+
if (stack.length === 1) {
|
|
68
|
+
setListeners(true);
|
|
69
|
+
}
|
|
70
|
+
let released = false;
|
|
71
|
+
return () => {
|
|
72
|
+
if (released) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
released = true;
|
|
76
|
+
const index = stack.lastIndexOf(layer);
|
|
77
|
+
if (index !== -1) {
|
|
78
|
+
stack.splice(index, 1);
|
|
79
|
+
}
|
|
80
|
+
if (stack.length === 0) {
|
|
81
|
+
setListeners(false);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** How many layers are currently registered. Exists for tests and diagnostics. */
|
|
86
|
+
export function dismissibleLayerCount() {
|
|
87
|
+
return stack.length;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Does this event's journey pass through `container`?
|
|
91
|
+
*
|
|
92
|
+
* `container.contains(event.target)` is the obvious test and it is wrong the
|
|
93
|
+
* moment a shadow boundary is involved. Once an event crosses one, `target` is
|
|
94
|
+
* retargeted to the HOST element -- an ANCESTOR of everything in the shadow
|
|
95
|
+
* root -- so `contains()` returns false for a click on the container itself.
|
|
96
|
+
* An outside-click handler written that way treats every click as outside and
|
|
97
|
+
* closes the panel on the same click that opened it.
|
|
98
|
+
*
|
|
99
|
+
* `composedPath()` is unaffected: it lists every node the event actually
|
|
100
|
+
* travelled through, inside shadow roots included. The `contains` fallback is
|
|
101
|
+
* for a synthetic event with no composed path, which is the only case where the
|
|
102
|
+
* retargeting problem cannot arise.
|
|
103
|
+
*/
|
|
104
|
+
export function eventHitsInside(container, event) {
|
|
105
|
+
if (container === null) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
|
|
109
|
+
if (path.length > 0) {
|
|
110
|
+
return path.includes(container);
|
|
111
|
+
}
|
|
112
|
+
const target = event.target;
|
|
113
|
+
return target instanceof Node && container.contains(target);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The element the user actually acted on, not the one the DOM admits to.
|
|
117
|
+
*
|
|
118
|
+
* A listener on `document` sees `event.target` retargeted to the shadow HOST,
|
|
119
|
+
* so `target.closest(selector)` walks the light tree and never sees the node
|
|
120
|
+
* that was clicked. `composedPath()[0]` is the real innermost target, and
|
|
121
|
+
* walking the path is the shadow-safe form of `closest()` -- the path already
|
|
122
|
+
* spans every root the event crossed, so an ancestor search over it finds
|
|
123
|
+
* matches inside the shadow tree and outside it in one pass.
|
|
124
|
+
*/
|
|
125
|
+
export function closestInEventPath(event, selector) {
|
|
126
|
+
const path = typeof event.composedPath === 'function' ? event.composedPath() : [];
|
|
127
|
+
if (path.length === 0) {
|
|
128
|
+
const target = event.target;
|
|
129
|
+
return target instanceof Element ? target.closest(selector) : null;
|
|
130
|
+
}
|
|
131
|
+
for (const node of path) {
|
|
132
|
+
if (node instanceof Element && node.matches(selector)) {
|
|
133
|
+
return node;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared focus-trap primitives for any `aria-modal="true"` panel -- Modal,
|
|
3
|
+
* Sheet and DateRangePicker each grew their own copy of the same selector
|
|
4
|
+
* plus the same first/last edge-wrap decision. Three copies of one decision
|
|
5
|
+
* is a maintenance hazard (a fix to one silently misses the other two), so
|
|
6
|
+
* this is the single home for it. Originally written for Modal alone
|
|
7
|
+
* (#529: "no Tab-cycling") as `Modal/focus-trap.ts`; moved here once Sheet
|
|
8
|
+
* and DateRangePicker had each reimplemented it independently.
|
|
9
|
+
*
|
|
10
|
+
* Pure DOM-reading functions, no Svelte runtime dependency, so they stay
|
|
11
|
+
* unit testable without mounting a component -- see focus.test.ts, which
|
|
12
|
+
* exercises all of this against plain object stubs.
|
|
13
|
+
*
|
|
14
|
+
* Modal.svelte, Sheet.svelte and DateRangePicker.svelte own the event wiring
|
|
15
|
+
* (which key was pressed, calling `.focus()`); this module owns the two
|
|
16
|
+
* decisions that wiring needs answered: "what are the edges of the tab order
|
|
17
|
+
* in here" and "given where focus is and which way Tab was pressed, should
|
|
18
|
+
* it wrap, and to where".
|
|
19
|
+
*
|
|
20
|
+
* Lives under an underscore-prefixed directory, mirroring `src/lib/_chart/`:
|
|
21
|
+
* this is an implementation detail three components happen to share, not a
|
|
22
|
+
* themed, documented component with a prop/event surface of its own. It has
|
|
23
|
+
* no visual output, no `classes`/`testId` escape hatch, nothing a consumer
|
|
24
|
+
* configures -- there is nothing here that belongs in `src/lib/index.ts` or
|
|
25
|
+
* the web-component build. Promoting it to the public API would freeze one
|
|
26
|
+
* internal decision (exactly which selector, exactly which edge-wrap
|
|
27
|
+
* algorithm) into a contract this library would then have to keep working
|
|
28
|
+
* forever, for consumers who were never meant to reach it directly.
|
|
29
|
+
*/
|
|
30
|
+
export type FocusTrapBoundary = {
|
|
31
|
+
readonly first: HTMLElement | null;
|
|
32
|
+
readonly last: HTMLElement | null;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The first and last focusable descendants of `container`, in DOM order,
|
|
36
|
+
* filtered to elements that can actually receive focus (see
|
|
37
|
+
* `canReceiveFocus`). Both are null when the container is null or holds
|
|
38
|
+
* nothing focusable.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getFocusTrapBoundary(container: ParentNode | null): FocusTrapBoundary;
|
|
41
|
+
/**
|
|
42
|
+
* Given a Tab/Shift+Tab keydown inside a focus-trapped container, returns the
|
|
43
|
+
* element focus should move to, or null when the default browser behavior
|
|
44
|
+
* (move to the next/previous focusable element) should be left alone.
|
|
45
|
+
*
|
|
46
|
+
* Wrapping only happens exactly at an edge: focus sits on the last focusable
|
|
47
|
+
* element and Tab (not Shift+Tab) was pressed, or focus sits on the first and
|
|
48
|
+
* Shift+Tab was pressed. Anywhere else in the middle of the tab order, the
|
|
49
|
+
* browser's own handling is correct and this returns null.
|
|
50
|
+
*/
|
|
51
|
+
export declare function focusTrapTabTarget(params: {
|
|
52
|
+
readonly container: ParentNode | null;
|
|
53
|
+
readonly activeElement: Element | null;
|
|
54
|
+
readonly shiftKey: boolean;
|
|
55
|
+
}): HTMLElement | null;
|
|
56
|
+
/**
|
|
57
|
+
* Moves focus into `container` -- its first focusable descendant, or the
|
|
58
|
+
* container itself as a fallback for a panel with no focusable content (it
|
|
59
|
+
* must carry tabindex="-1" for that fallback to actually take focus). A no-op
|
|
60
|
+
* when the container is null.
|
|
61
|
+
*/
|
|
62
|
+
export declare function focusEntryPoint(container: HTMLElement | null): void;
|
|
63
|
+
/**
|
|
64
|
+
* The element that really has focus, whether `node` sits in the light DOM or
|
|
65
|
+
* inside a custom element's open shadow root.
|
|
66
|
+
*
|
|
67
|
+
* `document.activeElement` stops at a shadow boundary and reports the shadow HOST,
|
|
68
|
+
* never the element focused inside it, so a component that reads it directly gets
|
|
69
|
+
* the wrong answer through the `sui-*` builds while looking correct in the Svelte
|
|
70
|
+
* one. `getRootNode()` returns the shadow root when the node is shadow-hosted and
|
|
71
|
+
* the owner document otherwise, and both expose `activeElement`.
|
|
72
|
+
*/
|
|
73
|
+
export declare function getActiveElement(node: Node | null): Element | null;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared focus-trap primitives for any `aria-modal="true"` panel -- Modal,
|
|
3
|
+
* Sheet and DateRangePicker each grew their own copy of the same selector
|
|
4
|
+
* plus the same first/last edge-wrap decision. Three copies of one decision
|
|
5
|
+
* is a maintenance hazard (a fix to one silently misses the other two), so
|
|
6
|
+
* this is the single home for it. Originally written for Modal alone
|
|
7
|
+
* (#529: "no Tab-cycling") as `Modal/focus-trap.ts`; moved here once Sheet
|
|
8
|
+
* and DateRangePicker had each reimplemented it independently.
|
|
9
|
+
*
|
|
10
|
+
* Pure DOM-reading functions, no Svelte runtime dependency, so they stay
|
|
11
|
+
* unit testable without mounting a component -- see focus.test.ts, which
|
|
12
|
+
* exercises all of this against plain object stubs.
|
|
13
|
+
*
|
|
14
|
+
* Modal.svelte, Sheet.svelte and DateRangePicker.svelte own the event wiring
|
|
15
|
+
* (which key was pressed, calling `.focus()`); this module owns the two
|
|
16
|
+
* decisions that wiring needs answered: "what are the edges of the tab order
|
|
17
|
+
* in here" and "given where focus is and which way Tab was pressed, should
|
|
18
|
+
* it wrap, and to where".
|
|
19
|
+
*
|
|
20
|
+
* Lives under an underscore-prefixed directory, mirroring `src/lib/_chart/`:
|
|
21
|
+
* this is an implementation detail three components happen to share, not a
|
|
22
|
+
* themed, documented component with a prop/event surface of its own. It has
|
|
23
|
+
* no visual output, no `classes`/`testId` escape hatch, nothing a consumer
|
|
24
|
+
* configures -- there is nothing here that belongs in `src/lib/index.ts` or
|
|
25
|
+
* the web-component build. Promoting it to the public API would freeze one
|
|
26
|
+
* internal decision (exactly which selector, exactly which edge-wrap
|
|
27
|
+
* algorithm) into a contract this library would then have to keep working
|
|
28
|
+
* forever, for consumers who were never meant to reach it directly.
|
|
29
|
+
*/
|
|
30
|
+
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
31
|
+
/**
|
|
32
|
+
* True when `el` can actually take focus. `FOCUSABLE_SELECTOR` alone matches
|
|
33
|
+
* candidates that pass its tag/attribute shape but still cannot receive
|
|
34
|
+
* focus in a real browser: hidden via the `hidden` attribute, inside an
|
|
35
|
+
* `inert` subtree, or explicitly marked `aria-disabled="true"`. Without this,
|
|
36
|
+
* Tab wrapping can land focus on one of these -- worse than no trap at all,
|
|
37
|
+
* since focus then effectively vanishes instead of just not being managed.
|
|
38
|
+
*
|
|
39
|
+
* Duck-typed rather than an `instanceof HTMLElement` check: this module is
|
|
40
|
+
* also exercised against plain `{ focus() {} }` stubs that carry none of
|
|
41
|
+
* these methods (see focus.test.ts, and Modal's original test suite that
|
|
42
|
+
* predates this filter) -- a stub with nothing to check on has nothing to
|
|
43
|
+
* filter out, so it passes through unchanged rather than throwing.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately does not consult layout (`offsetParent`, `getClientRects`):
|
|
46
|
+
* jsdom, which the test suite runs under, never computes layout, so a
|
|
47
|
+
* layout-based check would either be a permanent no-op there or -- worse --
|
|
48
|
+
* report every element as non-focusable and empty out every boundary under
|
|
49
|
+
* test. The three checks below are all attribute/tree-shape reads, which
|
|
50
|
+
* jsdom does support, so behavior under test matches behavior in a browser.
|
|
51
|
+
*/
|
|
52
|
+
function canReceiveFocus(el) {
|
|
53
|
+
if (typeof el.hasAttribute !== 'function' ||
|
|
54
|
+
typeof el.getAttribute !== 'function' ||
|
|
55
|
+
typeof el.closest !== 'function') {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (el.hasAttribute('hidden')) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (el.getAttribute('aria-disabled') === 'true') {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return el.closest('[inert]') === null;
|
|
65
|
+
}
|
|
66
|
+
function collectFocusableCandidates(container) {
|
|
67
|
+
const matches = container.querySelectorAll(FOCUSABLE_SELECTOR);
|
|
68
|
+
const candidates = [];
|
|
69
|
+
for (let index = 0; index < matches.length; index += 1) {
|
|
70
|
+
const candidate = matches.item(index);
|
|
71
|
+
if (canReceiveFocus(candidate)) {
|
|
72
|
+
candidates.push(candidate);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return candidates;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The first and last focusable descendants of `container`, in DOM order,
|
|
79
|
+
* filtered to elements that can actually receive focus (see
|
|
80
|
+
* `canReceiveFocus`). Both are null when the container is null or holds
|
|
81
|
+
* nothing focusable.
|
|
82
|
+
*/
|
|
83
|
+
export function getFocusTrapBoundary(container) {
|
|
84
|
+
if (container === null) {
|
|
85
|
+
return { first: null, last: null };
|
|
86
|
+
}
|
|
87
|
+
const candidates = collectFocusableCandidates(container);
|
|
88
|
+
return {
|
|
89
|
+
first: candidates.length > 0 ? candidates[0] : null,
|
|
90
|
+
last: candidates.length > 0 ? candidates[candidates.length - 1] : null
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Given a Tab/Shift+Tab keydown inside a focus-trapped container, returns the
|
|
95
|
+
* element focus should move to, or null when the default browser behavior
|
|
96
|
+
* (move to the next/previous focusable element) should be left alone.
|
|
97
|
+
*
|
|
98
|
+
* Wrapping only happens exactly at an edge: focus sits on the last focusable
|
|
99
|
+
* element and Tab (not Shift+Tab) was pressed, or focus sits on the first and
|
|
100
|
+
* Shift+Tab was pressed. Anywhere else in the middle of the tab order, the
|
|
101
|
+
* browser's own handling is correct and this returns null.
|
|
102
|
+
*/
|
|
103
|
+
export function focusTrapTabTarget(params) {
|
|
104
|
+
const { first, last } = getFocusTrapBoundary(params.container);
|
|
105
|
+
if (first === null || last === null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const edge = params.shiftKey ? first : last;
|
|
109
|
+
if (params.activeElement !== edge) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return params.shiftKey ? last : first;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Moves focus into `container` -- its first focusable descendant, or the
|
|
116
|
+
* container itself as a fallback for a panel with no focusable content (it
|
|
117
|
+
* must carry tabindex="-1" for that fallback to actually take focus). A no-op
|
|
118
|
+
* when the container is null.
|
|
119
|
+
*/
|
|
120
|
+
export function focusEntryPoint(container) {
|
|
121
|
+
if (container === null) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const { first } = getFocusTrapBoundary(container);
|
|
125
|
+
(first ?? container).focus();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The element that really has focus, whether `node` sits in the light DOM or
|
|
129
|
+
* inside a custom element's open shadow root.
|
|
130
|
+
*
|
|
131
|
+
* `document.activeElement` stops at a shadow boundary and reports the shadow HOST,
|
|
132
|
+
* never the element focused inside it, so a component that reads it directly gets
|
|
133
|
+
* the wrong answer through the `sui-*` builds while looking correct in the Svelte
|
|
134
|
+
* one. `getRootNode()` returns the shadow root when the node is shadow-hosted and
|
|
135
|
+
* the owner document otherwise, and both expose `activeElement`.
|
|
136
|
+
*/
|
|
137
|
+
export function getActiveElement(node) {
|
|
138
|
+
if (node === null) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const root = node.getRootNode();
|
|
142
|
+
return root instanceof Document || root instanceof ShadowRoot ? root.activeElement : null;
|
|
143
|
+
}
|