@juspay/svelte-ui-components 4.9.2 → 4.10.1
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/dist/Checkbox/Checkbox.svelte +18 -1
- package/dist/CommandMenu/CommandMenu.svelte +69 -2
- package/dist/ContextMenu/ContextMenu.svelte +62 -2
- package/dist/Slider/Slider.svelte +2 -0
- package/dist/Slider/properties.d.ts +7 -0
- package/dist/SplitButton/SplitButton.svelte +21 -1
- package/dist/SplitButton/properties.d.ts +7 -0
- package/dist/Stepper/Step.svelte +8 -2
- package/dist/Table/BuiltinCell.svelte +10 -3
- package/dist/Table/Table.svelte +3 -1
- package/dist-wc/index.js +406 -323
- package/package.json +1 -1
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
attributes
|
|
20
20
|
}: CheckboxProperties = $props();
|
|
21
21
|
|
|
22
|
+
// Visible text wins over `ariaLabel`, which is what `properties.ts` has always
|
|
23
|
+
// promised and what WCAG 2.5.3 requires: a control showing "Accept terms" must
|
|
24
|
+
// carry that text in its accessible name, so letting an explicit label replace
|
|
25
|
+
// it outright would swap the name a speech-input user says for one they cannot
|
|
26
|
+
// see. `ariaLabel` names the box only when there is no visible text to use —
|
|
27
|
+
// the icon-only and label-in-a-table-header cases it exists for.
|
|
28
|
+
//
|
|
29
|
+
// A blank `ariaLabel` is treated as absent rather than as an empty name, since
|
|
30
|
+
// `aria-label=""` leaves the box unnamed, which is the bug this fixes.
|
|
31
|
+
const accessibleName: string | null = $derived(
|
|
32
|
+
text.length > 0
|
|
33
|
+
? text
|
|
34
|
+
: typeof ariaLabel === 'string' && ariaLabel.trim() !== ''
|
|
35
|
+
? ariaLabel
|
|
36
|
+
: null
|
|
37
|
+
);
|
|
38
|
+
|
|
22
39
|
function handleClick(): void {
|
|
23
40
|
if (disabled) {
|
|
24
41
|
return;
|
|
@@ -82,7 +99,7 @@
|
|
|
82
99
|
aria-checked={indeterminate ? 'mixed' : checked}
|
|
83
100
|
aria-disabled={disabled}
|
|
84
101
|
aria-controls={ariaControls ?? null}
|
|
85
|
-
aria-label={
|
|
102
|
+
aria-label={accessibleName}
|
|
86
103
|
onkeydown={handleKeyDown}
|
|
87
104
|
data-pw={typeof testId === 'string' ? `${testId}-box` : null}
|
|
88
105
|
testID={typeof testId === 'string' ? `${testId}-box` : null}
|
|
@@ -19,10 +19,19 @@
|
|
|
19
19
|
classes
|
|
20
20
|
}: CommandMenuProperties = $props();
|
|
21
21
|
|
|
22
|
+
// Stable per-instance ids keep ARIA references distinct across menus.
|
|
23
|
+
const uid = $props.id();
|
|
24
|
+
const listboxId = `command-menu-listbox-${uid}`;
|
|
25
|
+
|
|
22
26
|
let query = $state('');
|
|
23
27
|
let activeIndex = $state(0);
|
|
24
28
|
let inputElement: HTMLInputElement | null = $state(null);
|
|
25
29
|
let listElement: HTMLDivElement | null = $state(null);
|
|
30
|
+
let dialogElement: HTMLDivElement | null = $state(null);
|
|
31
|
+
// Captured when the dialog opens, consumed once it unmounts — mirrors
|
|
32
|
+
// Gallery.svelte's lightboxAction so focus returns to whatever opened the
|
|
33
|
+
// command menu instead of falling back to <body>.
|
|
34
|
+
let openerElement: HTMLElement | null = null;
|
|
26
35
|
|
|
27
36
|
let filteredItems = $derived.by(() => {
|
|
28
37
|
if (query.trim() === '') {
|
|
@@ -68,6 +77,13 @@
|
|
|
68
77
|
return map;
|
|
69
78
|
});
|
|
70
79
|
|
|
80
|
+
// aria-activedescendant must point at the id of the highlighted option, which
|
|
81
|
+
// is keyed by its flat index (see the option's own id below) — null once the
|
|
82
|
+
// list is empty, since there is then no option to describe.
|
|
83
|
+
let activeDescendantId = $derived(
|
|
84
|
+
flatItems.length > 0 ? `command-menu-option-${uid}-${activeIndex}` : null
|
|
85
|
+
);
|
|
86
|
+
|
|
71
87
|
function close() {
|
|
72
88
|
open = false;
|
|
73
89
|
query = '';
|
|
@@ -132,9 +148,47 @@
|
|
|
132
148
|
close();
|
|
133
149
|
break;
|
|
134
150
|
}
|
|
151
|
+
case 'Tab': {
|
|
152
|
+
// aria-modal="true" promises focus stays inside, so Tab cycles rather
|
|
153
|
+
// than escaping. Cycle over whatever is actually tabbable instead of
|
|
154
|
+
// pinning to the input: today the input is the only tab stop (options
|
|
155
|
+
// are tabindex="-1" and are reached with arrows), so the two are
|
|
156
|
+
// equivalent — but pinning would silently break the moment the dialog
|
|
157
|
+
// gains a second tab stop, such as a close button.
|
|
158
|
+
const tabbable = tabbableElements();
|
|
159
|
+
if (tabbable.length === 0) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
const active = document.activeElement;
|
|
164
|
+
const current = active instanceof HTMLElement ? tabbable.indexOf(active) : -1;
|
|
165
|
+
const step = event.shiftKey ? -1 : 1;
|
|
166
|
+
const next = (current + step + tabbable.length) % tabbable.length;
|
|
167
|
+
tabbable.at(next)?.focus();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
135
170
|
}
|
|
136
171
|
}
|
|
137
172
|
|
|
173
|
+
// Everything inside the dialog that Tab would normally reach. `tabindex="-1"`
|
|
174
|
+
// is excluded because those are arrow-key targets, not tab stops.
|
|
175
|
+
function tabbableElements(): HTMLElement[] {
|
|
176
|
+
if (dialogElement === null) {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
const candidates = dialogElement.querySelectorAll<HTMLElement>(
|
|
180
|
+
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]'
|
|
181
|
+
);
|
|
182
|
+
return [...candidates].filter(
|
|
183
|
+
(el) =>
|
|
184
|
+
// The options are real <button>s held out of the tab order with
|
|
185
|
+
// tabindex="-1"; they are arrow-key targets. Excluding -1 only in the
|
|
186
|
+
// `[tabindex]` selector would still match them as buttons.
|
|
187
|
+
el.getAttribute('tabindex') !== '-1' &&
|
|
188
|
+
(el.offsetParent !== null || el === document.activeElement)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
138
192
|
async function scrollActiveIntoView() {
|
|
139
193
|
await tick();
|
|
140
194
|
if (listElement !== null) {
|
|
@@ -167,6 +221,9 @@
|
|
|
167
221
|
// onDestroy below must NOT unlock as well: with a shared reference count a second
|
|
168
222
|
// decrement would release a lock another open surface still needs.
|
|
169
223
|
function scrollLockAction(_node: HTMLElement) {
|
|
224
|
+
if (openerElement === null && document.activeElement instanceof HTMLElement) {
|
|
225
|
+
openerElement = document.activeElement;
|
|
226
|
+
}
|
|
170
227
|
lockBodyScroll();
|
|
171
228
|
tick().then(() => {
|
|
172
229
|
if (inputElement !== null) {
|
|
@@ -176,6 +233,10 @@
|
|
|
176
233
|
return {
|
|
177
234
|
destroy() {
|
|
178
235
|
unlockBodyScroll();
|
|
236
|
+
if (openerElement !== null) {
|
|
237
|
+
openerElement.focus();
|
|
238
|
+
openerElement = null;
|
|
239
|
+
}
|
|
179
240
|
}
|
|
180
241
|
};
|
|
181
242
|
}
|
|
@@ -204,7 +265,7 @@
|
|
|
204
265
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
205
266
|
testID={typeof testId === 'string' ? testId : null}
|
|
206
267
|
>
|
|
207
|
-
<div class="command-menu-dialog">
|
|
268
|
+
<div class="command-menu-dialog" bind:this={dialogElement}>
|
|
208
269
|
<div class="command-menu-input-wrapper">
|
|
209
270
|
{#if typeof searchIcon === 'function'}
|
|
210
271
|
{@render searchIcon()}
|
|
@@ -223,6 +284,11 @@
|
|
|
223
284
|
{placeholder}
|
|
224
285
|
autocomplete="off"
|
|
225
286
|
spellcheck="false"
|
|
287
|
+
role="combobox"
|
|
288
|
+
aria-expanded={flatItems.length > 0}
|
|
289
|
+
aria-controls={listboxId}
|
|
290
|
+
aria-autocomplete="list"
|
|
291
|
+
aria-activedescendant={activeDescendantId}
|
|
226
292
|
data-pw={typeof testId === 'string' ? `${testId}-input` : null}
|
|
227
293
|
testID={typeof testId === 'string' ? `${testId}-input` : null}
|
|
228
294
|
/>
|
|
@@ -230,7 +296,7 @@
|
|
|
230
296
|
|
|
231
297
|
<div class="command-menu-separator"></div>
|
|
232
298
|
|
|
233
|
-
<div class="command-menu-list" bind:this={listElement} role="listbox">
|
|
299
|
+
<div class="command-menu-list" bind:this={listElement} id={listboxId} role="listbox">
|
|
234
300
|
{#if flatItems.length === 0}
|
|
235
301
|
<div class="command-menu-empty">
|
|
236
302
|
{emptyText}
|
|
@@ -244,6 +310,7 @@
|
|
|
244
310
|
{@const index = flatIndexLookup.get(item.value) ?? -1}
|
|
245
311
|
<button
|
|
246
312
|
type="button"
|
|
313
|
+
id={`command-menu-option-${uid}-${index}`}
|
|
247
314
|
class="command-menu-item"
|
|
248
315
|
class:active={index === activeIndex}
|
|
249
316
|
class:disabled={item.disabled}
|
|
@@ -19,27 +19,66 @@
|
|
|
19
19
|
let focusedIndex: number = $state(-1);
|
|
20
20
|
let posX: number = $state(0);
|
|
21
21
|
let posY: number = $state(0);
|
|
22
|
+
// Captured when the menu opens, consumed once on close — the element that had
|
|
23
|
+
// focus (or triggered the native contextmenu event) is where focus belongs once
|
|
24
|
+
// the menu goes away, matching Menu.svelte's own restore-on-close contract.
|
|
25
|
+
let openerElement: HTMLElement | null = $state(null);
|
|
22
26
|
|
|
23
27
|
let selectableItems: ContextMenuItem[] = $derived(
|
|
24
28
|
items.filter((item) => !item.separator && !item.disabled)
|
|
25
29
|
);
|
|
26
30
|
|
|
31
|
+
// The frame that moves focus into the freshly-opened menu, kept so closing can
|
|
32
|
+
// cancel it. Escape pressed between the right-click and that frame used to
|
|
33
|
+
// leave the callback queued: it ran after the menu was gone, moved focus onto
|
|
34
|
+
// an item that no longer existed, and left the page with nothing focused at
|
|
35
|
+
// all — worse than never restoring, because the user's place is simply lost.
|
|
36
|
+
let openFrame: number | null = null;
|
|
37
|
+
|
|
38
|
+
function cancelOpenFrame() {
|
|
39
|
+
if (openFrame !== null) {
|
|
40
|
+
cancelAnimationFrame(openFrame);
|
|
41
|
+
openFrame = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
function openMenu(x: number, y: number) {
|
|
28
46
|
posX = x;
|
|
29
47
|
posY = y;
|
|
30
48
|
open = true;
|
|
31
49
|
focusedIndex = 0;
|
|
32
50
|
onopen?.();
|
|
33
|
-
|
|
51
|
+
cancelOpenFrame();
|
|
52
|
+
openFrame = requestAnimationFrame(() => {
|
|
53
|
+
openFrame = null;
|
|
34
54
|
adjustPosition();
|
|
35
55
|
focusItem(0);
|
|
36
56
|
});
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
function close() {
|
|
60
|
+
cancelOpenFrame();
|
|
40
61
|
open = false;
|
|
41
62
|
focusedIndex = -1;
|
|
42
63
|
onclose?.();
|
|
64
|
+
|
|
65
|
+
const opener = openerElement;
|
|
66
|
+
openerElement = null;
|
|
67
|
+
if (opener === null) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Only take focus back if nothing else has claimed it. Closing by clicking a
|
|
72
|
+
// focusable element outside means the browser has already moved focus there,
|
|
73
|
+
// and pulling it back to the opener would fight the user and lose their
|
|
74
|
+
// place. Focus still inside the dying menu, or fallen to <body>, is loose and
|
|
75
|
+
// does belong to the opener.
|
|
76
|
+
const active = document.activeElement;
|
|
77
|
+
const focusIsLoose =
|
|
78
|
+
active === null || active === document.body || (menuEl?.contains(active) ?? false);
|
|
79
|
+
if (focusIsLoose) {
|
|
80
|
+
opener.focus();
|
|
81
|
+
}
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
function selectMenuItem(item: ContextMenuItem) {
|
|
@@ -90,6 +129,12 @@
|
|
|
90
129
|
|
|
91
130
|
function handleContextMenu(event: MouseEvent) {
|
|
92
131
|
event.preventDefault();
|
|
132
|
+
// A right-click usually leaves <body> as the active element, and "restore
|
|
133
|
+
// focus to <body>" is not a restoration — it is the same as doing nothing,
|
|
134
|
+
// but it also discards whatever the browser would have done. Capture only a
|
|
135
|
+
// real element.
|
|
136
|
+
const active = document.activeElement;
|
|
137
|
+
openerElement = active instanceof HTMLElement && active !== document.body ? active : null;
|
|
93
138
|
openMenu(event.clientX, event.clientY);
|
|
94
139
|
}
|
|
95
140
|
|
|
@@ -147,13 +192,28 @@
|
|
|
147
192
|
}
|
|
148
193
|
}
|
|
149
194
|
|
|
195
|
+
// Escape is handled on the document as well as on the menu, because for one
|
|
196
|
+
// frame after the right-click the menu is open but focus has not moved into it
|
|
197
|
+
// yet. A menu-only handler does nothing during that window: the keystroke is
|
|
198
|
+
// swallowed, the menu stays open, and the pending frame then pulls focus into
|
|
199
|
+
// it — so a quick Escape appeared to do the opposite of what it asked for. An
|
|
200
|
+
// open menu should close on Escape wherever focus happens to be.
|
|
201
|
+
function handleDocumentKeydown(event: KeyboardEvent) {
|
|
202
|
+
if (open && event.key === 'Escape') {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
close();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
150
208
|
onMount(() => {
|
|
151
209
|
document.addEventListener('click', handleClickOutside);
|
|
210
|
+
document.addEventListener('keydown', handleDocumentKeydown);
|
|
152
211
|
});
|
|
153
212
|
|
|
154
213
|
onDestroy(() => {
|
|
155
214
|
if (typeof window !== 'undefined') {
|
|
156
215
|
document.removeEventListener('click', handleClickOutside);
|
|
216
|
+
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
157
217
|
}
|
|
158
218
|
});
|
|
159
219
|
</script>
|
|
@@ -162,7 +222,7 @@
|
|
|
162
222
|
class="context-menu-container {classes ?? ''}"
|
|
163
223
|
bind:this={containerEl}
|
|
164
224
|
oncontextmenu={handleContextMenu}
|
|
165
|
-
role
|
|
225
|
+
{...open ? { role: 'application' } : {}}
|
|
166
226
|
data-pw={testId}
|
|
167
227
|
testID={testId}
|
|
168
228
|
>
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
showValue = false,
|
|
11
11
|
labelFormatter,
|
|
12
12
|
ariaLabel,
|
|
13
|
+
ariaLabelledby,
|
|
13
14
|
testId,
|
|
14
15
|
onchange,
|
|
15
16
|
oninput,
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
{value}
|
|
45
46
|
{disabled}
|
|
46
47
|
aria-label={typeof ariaLabel === 'string' ? ariaLabel : null}
|
|
48
|
+
aria-labelledby={typeof ariaLabel === 'string' ? null : (ariaLabelledby ?? null)}
|
|
47
49
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
48
50
|
testID={typeof testId === 'string' ? testId : null}
|
|
49
51
|
oninput={handleInput}
|
|
@@ -14,6 +14,13 @@ export type OptionalSliderProperties = {
|
|
|
14
14
|
* so a control whose purpose is not carried by adjacent visible text needs this.
|
|
15
15
|
*/
|
|
16
16
|
ariaLabel?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Ids of the elements that name the range input (aria-labelledby). Use this
|
|
19
|
+
* instead of `ariaLabel` when the name is already on screen, so the two cannot
|
|
20
|
+
* drift apart. `ariaLabel` wins if both are given, matching how the platform
|
|
21
|
+
* resolves them.
|
|
22
|
+
*/
|
|
23
|
+
ariaLabelledby?: string;
|
|
17
24
|
testId?: string;
|
|
18
25
|
classes?: string;
|
|
19
26
|
};
|
|
@@ -12,12 +12,27 @@
|
|
|
12
12
|
testId,
|
|
13
13
|
dropdownIcon,
|
|
14
14
|
classes,
|
|
15
|
+
triggerAriaLabel,
|
|
15
16
|
onclick,
|
|
16
17
|
onselect
|
|
17
18
|
}: SplitButtonProperties = $props();
|
|
18
19
|
|
|
19
20
|
let menuOpen = $state(false);
|
|
20
21
|
|
|
22
|
+
// The dropdown trigger renders a chevron and nothing else, so without a name it is a
|
|
23
|
+
// focusable control that announces as an unnamed button.
|
|
24
|
+
// A blank `triggerAriaLabel` falls back rather than being honoured: `??` only
|
|
25
|
+
// rejects null and undefined, so an empty string would have been passed
|
|
26
|
+
// through as `aria-label=""` and left the trigger unnamed — the very thing
|
|
27
|
+
// this generated fallback exists to prevent.
|
|
28
|
+
let resolvedTriggerAriaLabel = $derived(
|
|
29
|
+
typeof triggerAriaLabel === 'string' && triggerAriaLabel.trim() !== ''
|
|
30
|
+
? triggerAriaLabel
|
|
31
|
+
: text.length > 0
|
|
32
|
+
? `More ${text} options`
|
|
33
|
+
: 'More options'
|
|
34
|
+
);
|
|
35
|
+
|
|
21
36
|
function handlePrimaryClick(event: MouseEvent): void {
|
|
22
37
|
if (disabled) {
|
|
23
38
|
return;
|
|
@@ -40,7 +55,12 @@
|
|
|
40
55
|
<Button {text} onclick={handlePrimaryClick} {disabled} />
|
|
41
56
|
</div>
|
|
42
57
|
<div class="split-button-trigger">
|
|
43
|
-
<Menu
|
|
58
|
+
<Menu
|
|
59
|
+
{items}
|
|
60
|
+
bind:open={menuOpen}
|
|
61
|
+
triggerAriaLabel={resolvedTriggerAriaLabel}
|
|
62
|
+
onselect={handleMenuSelect}
|
|
63
|
+
>
|
|
44
64
|
{#snippet trigger()}
|
|
45
65
|
<span class="split-button-arrow">
|
|
46
66
|
{#if typeof dropdownIcon === 'function'}
|
|
@@ -10,6 +10,13 @@ export type OptionalSplitButtonProperties = {
|
|
|
10
10
|
testId?: string;
|
|
11
11
|
dropdownIcon?: Snippet;
|
|
12
12
|
classes?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Accessible name for the dropdown trigger — the second control, which shows only a
|
|
15
|
+
* chevron and so has no text to be named by. Defaults to `More <text> options`, which
|
|
16
|
+
* distinguishes it from the primary button beside it. Named after Menu's own
|
|
17
|
+
* `triggerAriaLabel`, which is what it forwards to.
|
|
18
|
+
*/
|
|
19
|
+
triggerAriaLabel?: string;
|
|
13
20
|
};
|
|
14
21
|
export type SplitButtonEventProperties = {
|
|
15
22
|
onclick?: (event: MouseEvent) => void;
|
package/dist/Stepper/Step.svelte
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
onkeydown
|
|
17
17
|
}: StepProperties = $props();
|
|
18
18
|
|
|
19
|
+
// Per-instance, because `step-label-${stepIndex}` collides across two Steppers
|
|
20
|
+
// on one page and `aria-labelledby` then resolves to whichever matching id
|
|
21
|
+
// comes first in the document — naming a step after a different Stepper's.
|
|
22
|
+
const uid = $props.id();
|
|
23
|
+
const labelId = `step-label-${uid}`;
|
|
24
|
+
|
|
19
25
|
const handleStepClick = (): void => {
|
|
20
26
|
onclick?.({ selectedIndex: stepIndex });
|
|
21
27
|
};
|
|
@@ -59,7 +65,7 @@
|
|
|
59
65
|
role={suppressRoleAndTabindex ? null : 'button'}
|
|
60
66
|
tabindex={suppressRoleAndTabindex ? null : 0}
|
|
61
67
|
aria-label={ariaLabel ?? null}
|
|
62
|
-
aria-labelledby={ariaLabel ? null :
|
|
68
|
+
aria-labelledby={ariaLabel ? null : labelId}
|
|
63
69
|
aria-current={status === 'active' ? 'step' : null}
|
|
64
70
|
>
|
|
65
71
|
{#if typeof icon === 'string' && icon.length > 0}
|
|
@@ -99,7 +105,7 @@
|
|
|
99
105
|
</div>
|
|
100
106
|
{/if}
|
|
101
107
|
|
|
102
|
-
<div class="step-text" id=
|
|
108
|
+
<div class="step-text" id={labelId}>
|
|
103
109
|
{label}
|
|
104
110
|
</div>
|
|
105
111
|
|
|
@@ -447,11 +447,16 @@
|
|
|
447
447
|
}))}
|
|
448
448
|
testId={generatedTestId('menu', rowIndex)}
|
|
449
449
|
{usePortal}
|
|
450
|
+
interactiveTrigger
|
|
450
451
|
onselect={(menuItem) => column.onMenuAction?.(rowIndex, menuItem.value, originalIndex)}
|
|
451
452
|
>
|
|
452
|
-
{#snippet trigger()}
|
|
453
|
+
{#snippet trigger(triggerProps)}
|
|
453
454
|
<span class="builtin-icon-button">
|
|
454
|
-
<Button
|
|
455
|
+
<Button
|
|
456
|
+
{...triggerProps}
|
|
457
|
+
ariaLabel="More actions"
|
|
458
|
+
testId={generatedTestId('menuTrigger', rowIndex)}
|
|
459
|
+
>
|
|
455
460
|
{#snippet icon()}
|
|
456
461
|
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
457
462
|
<span class="builtin-menu-dots">{@html dotsSvg}</span>
|
|
@@ -483,11 +488,13 @@
|
|
|
483
488
|
}))}
|
|
484
489
|
testId={generatedTestId('popup', rowIndex)}
|
|
485
490
|
{usePortal}
|
|
491
|
+
interactiveTrigger
|
|
486
492
|
onselect={(menuItem) => column.onMenuAction?.(rowIndex, menuItem.value, originalIndex)}
|
|
487
493
|
>
|
|
488
|
-
{#snippet trigger()}
|
|
494
|
+
{#snippet trigger(triggerProps)}
|
|
489
495
|
<span class="builtin-icon-button">
|
|
490
496
|
<Button
|
|
497
|
+
{...triggerProps}
|
|
491
498
|
ariaLabel={popupData.ariaLabel ?? 'More actions'}
|
|
492
499
|
testId={generatedTestId('popupTrigger', rowIndex)}
|
|
493
500
|
>
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -758,19 +758,21 @@
|
|
|
758
758
|
}))}
|
|
759
759
|
selectedValue={filter.selectedValue ?? null}
|
|
760
760
|
role="listbox"
|
|
761
|
+
interactiveTrigger
|
|
761
762
|
testId={headerColumn.testId && `${headerColumn.testId}-filter`}
|
|
762
763
|
onselect={(menuItem) =>
|
|
763
764
|
filter.onFilterChange?.(
|
|
764
765
|
menuItem.value === filter.selectedValue ? null : menuItem.value
|
|
765
766
|
)}
|
|
766
767
|
>
|
|
767
|
-
{#snippet trigger()}
|
|
768
|
+
{#snippet trigger(triggerProps)}
|
|
768
769
|
<span
|
|
769
770
|
class="table-header-filter-trigger"
|
|
770
771
|
class:table-header-filter-active={typeof filter.selectedValue ===
|
|
771
772
|
'string'}
|
|
772
773
|
>
|
|
773
774
|
<Button
|
|
775
|
+
{...triggerProps}
|
|
774
776
|
ariaLabel="Filter by {header}"
|
|
775
777
|
testId={headerColumn.testId &&
|
|
776
778
|
`${headerColumn.testId}-filter-trigger`}
|