@juspay/svelte-ui-components 4.52.1 → 4.53.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/dist/AttachmentChipRow/AttachmentChipRow.svelte +3 -2
- package/dist/AttachmentChipRow/properties.d.ts +2 -0
- package/dist/Book/Book.svelte +2 -0
- package/dist/Book/properties.d.ts +2 -0
- package/dist/Chat/Chat.svelte +2 -0
- package/dist/Chat/properties.d.ts +2 -0
- package/dist/Chat/types.d.ts +2 -0
- package/dist/ChatMessage/ChatMessage.svelte +18 -3
- package/dist/ChatMessage/properties.d.ts +6 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +31 -7
- package/dist/ChatMessageList/properties.d.ts +2 -0
- package/dist/Choicebox/Choicebox.svelte +33 -23
- package/dist/Draggable/Draggable.svelte +4 -2
- package/dist/MediaPlayer/MediaPlayer.svelte +28 -13
- package/dist/MediaPlayer/properties.d.ts +2 -0
- package/dist/Modal/Modal.svelte +30 -43
- package/dist/Modal/properties.d.ts +5 -4
- package/dist/Pagination/Pagination.svelte +2 -0
- package/dist/Pagination/properties.d.ts +2 -0
- package/dist/Pill/Pill.svelte +61 -4
- package/dist/Pill/properties.d.ts +8 -16
- package/dist/Radio/Radio.svelte +6 -2
- package/dist/RatingGroup/RatingGroup.svelte +38 -26
- package/dist/Select/Select.svelte +8 -0
- package/dist/StatCard/StatCard.svelte +68 -17
- package/dist/StatCard/properties.d.ts +1 -1
- package/dist/Stepper/Step.svelte +2 -2
- package/dist/Table/Table.svelte +32 -2
- package/dist/Table/properties.d.ts +11 -5
- package/dist/Tabs/Tabs.svelte +187 -12
- package/dist/utils.js +20 -2
- package/dist-codemod/codemod/cli.js +4 -4
- package/dist-wc/index.js +2964 -2589
- package/package.json +1 -1
- package/scripts/postinstall.mjs +13 -15
package/dist/Pill/Pill.svelte
CHANGED
|
@@ -53,15 +53,25 @@
|
|
|
53
53
|
let interactive = $derived(typeof onclick === 'function');
|
|
54
54
|
|
|
55
55
|
// A <button> may not contain another button, and a dismissible pill is inherently two
|
|
56
|
-
// controls.
|
|
57
|
-
//
|
|
56
|
+
// controls. A neutral div holds independent primary and dismiss buttons when
|
|
57
|
+
// both actions are present, preserving native activation for each action.
|
|
58
58
|
const isButtonRoot = $derived(as === 'button' && !dismissible);
|
|
59
59
|
const rootTag = $derived(isButtonRoot ? 'button' : 'div');
|
|
60
|
+
const hasSeparateAction = $derived(interactive && dismissible);
|
|
61
|
+
const rootAttrs = $derived(
|
|
62
|
+
hasSeparateAction
|
|
63
|
+
? Object.fromEntries(
|
|
64
|
+
Object.entries(attrs ?? {}).filter(
|
|
65
|
+
([key]) => !['aria-label', 'aria-labelledby', 'aria-describedby'].includes(key)
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
: attrs
|
|
69
|
+
);
|
|
60
70
|
|
|
61
71
|
// The synthetic shim exists only because a <div> has no button semantics. A real button
|
|
62
72
|
// already has them, and adding role/tabindex/keydown on top would duplicate the platform's
|
|
63
73
|
// own activation — Enter would fire the handler twice.
|
|
64
|
-
const needsInteractiveShim = $derived(interactive && !isButtonRoot);
|
|
74
|
+
const needsInteractiveShim = $derived(interactive && !isButtonRoot && !hasSeparateAction);
|
|
65
75
|
|
|
66
76
|
// aria-expanded / aria-pressed / aria-disabled describe a control's state, so they are
|
|
67
77
|
// meaningless on a plain label and are emitted only once the pill actually is one.
|
|
@@ -102,9 +112,10 @@
|
|
|
102
112
|
|
|
103
113
|
<svelte:element
|
|
104
114
|
this={rootTag}
|
|
105
|
-
{...
|
|
115
|
+
{...rootAttrs ?? {}}
|
|
106
116
|
class="pill {pillToneClass(tone)} {classes ?? ''}"
|
|
107
117
|
class:disabled
|
|
118
|
+
class:separate-action={hasSeparateAction}
|
|
108
119
|
type={isButtonRoot ? 'button' : null}
|
|
109
120
|
onclick={interactive ? handleClick : null}
|
|
110
121
|
onkeydown={needsInteractiveShim ? handleKeydown : null}
|
|
@@ -117,6 +128,22 @@
|
|
|
117
128
|
title={title ?? null}
|
|
118
129
|
testID={typeof testId === 'string' ? testId : null}
|
|
119
130
|
>
|
|
131
|
+
{#if hasSeparateAction}
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="pill-action"
|
|
135
|
+
{disabled}
|
|
136
|
+
aria-label={typeof attrs?.['aria-label'] === 'string' ? attrs['aria-label'] : text}
|
|
137
|
+
aria-labelledby={typeof attrs?.['aria-labelledby'] === 'string'
|
|
138
|
+
? attrs['aria-labelledby']
|
|
139
|
+
: null}
|
|
140
|
+
aria-describedby={typeof attrs?.['aria-describedby'] === 'string'
|
|
141
|
+
? attrs['aria-describedby']
|
|
142
|
+
: null}
|
|
143
|
+
aria-expanded={typeof ariaExpanded === 'boolean' ? ariaExpanded : null}
|
|
144
|
+
aria-pressed={typeof ariaPressed === 'boolean' ? ariaPressed : null}
|
|
145
|
+
></button>
|
|
146
|
+
{/if}
|
|
120
147
|
{#if typeof leadingIcon === 'function'}
|
|
121
148
|
<span class="pill-leading-icon">{@render leadingIcon()}</span>
|
|
122
149
|
{/if}
|
|
@@ -143,6 +170,35 @@
|
|
|
143
170
|
</svelte:element>
|
|
144
171
|
|
|
145
172
|
<style>
|
|
173
|
+
.separate-action {
|
|
174
|
+
position: relative;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.pill-action {
|
|
178
|
+
position: absolute;
|
|
179
|
+
inset: 0;
|
|
180
|
+
border: 0;
|
|
181
|
+
padding: 0;
|
|
182
|
+
border-radius: inherit;
|
|
183
|
+
background: transparent;
|
|
184
|
+
cursor: inherit;
|
|
185
|
+
outline: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.separate-action:has(> .pill-action:focus-visible) {
|
|
189
|
+
outline: var(--pill-focus-outline, 2px solid currentColor);
|
|
190
|
+
outline-offset: var(--pill-focus-outline-offset, 2px);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.separate-action > .pill-text,
|
|
194
|
+
.separate-action > .pill-leading-icon {
|
|
195
|
+
pointer-events: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.separate-action > .pill-dismiss {
|
|
199
|
+
position: relative;
|
|
200
|
+
}
|
|
201
|
+
|
|
146
202
|
.pill {
|
|
147
203
|
display: inline-flex;
|
|
148
204
|
align-items: center;
|
|
@@ -176,6 +232,7 @@
|
|
|
176
232
|
flex-shrink: var(--pill-flex-shrink);
|
|
177
233
|
}
|
|
178
234
|
|
|
235
|
+
.pill.separate-action,
|
|
179
236
|
.pill[role='button'],
|
|
180
237
|
button.pill {
|
|
181
238
|
cursor: var(--pill-cursor, pointer);
|
|
@@ -66,11 +66,10 @@ export type MandatoryPillProperties = {
|
|
|
66
66
|
*/
|
|
67
67
|
export type PillTone = 'accent' | 'ok' | 'warn' | 'danger' | 'muted';
|
|
68
68
|
/**
|
|
69
|
-
* Root element for
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* imitation of them.
|
|
69
|
+
* Root element for a label or single-action pill. The default div is a label,
|
|
70
|
+
* or a keyboard-operable button role when onclick is supplied. A button root
|
|
71
|
+
* uses native activation. Interactive dismissible pills use a neutral container
|
|
72
|
+
* with independent native primary and dismiss buttons.
|
|
74
73
|
*/
|
|
75
74
|
export type PillRootTag = 'div' | 'button';
|
|
76
75
|
export type OptionalPillProperties = {
|
|
@@ -83,17 +82,10 @@ export type OptionalPillProperties = {
|
|
|
83
82
|
*/
|
|
84
83
|
tone?: PillTone;
|
|
85
84
|
/**
|
|
86
|
-
* Root element. Defaults to
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* Reach for it when the chip is a disclosure or a toggle (see `ariaExpanded` /
|
|
91
|
-
* `ariaPressed`), which a shimmed div cannot express correctly.
|
|
92
|
-
*
|
|
93
|
-
* One combination cannot be honoured: a `<button>` may not contain another button, and a
|
|
94
|
-
* `dismissible` pill is inherently two controls. `as="button"` together with
|
|
95
|
-
* `dismissible` therefore falls back to the `div` root and its shim rather than emitting
|
|
96
|
-
* invalid markup — the same rule `Card` applies to `as="a"` with no `href`.
|
|
85
|
+
* Root element for a label or single action. Defaults to 'div'; 'button' uses
|
|
86
|
+
* native keyboard activation and focus. Dismissible pills use a neutral div
|
|
87
|
+
* container. When onclick is supplied, that container holds independent native
|
|
88
|
+
* primary and dismiss buttons, including when as='button'.
|
|
97
89
|
*/
|
|
98
90
|
as?: PillRootTag;
|
|
99
91
|
/**
|
package/dist/Radio/Radio.svelte
CHANGED
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
|
|
103
103
|
<style>
|
|
104
104
|
.radio-container {
|
|
105
|
+
position: relative;
|
|
105
106
|
display: var(--radio-container-display, inline-flex);
|
|
106
107
|
align-items: var(--radio-container-align-items, center);
|
|
107
108
|
gap: var(--radio-container-gap, 8px);
|
|
@@ -116,8 +117,11 @@
|
|
|
116
117
|
.radio-input {
|
|
117
118
|
position: absolute;
|
|
118
119
|
opacity: 0;
|
|
119
|
-
width:
|
|
120
|
-
height:
|
|
120
|
+
width: var(--radio-size, 20px);
|
|
121
|
+
height: var(--radio-size, 20px);
|
|
122
|
+
inset-inline-start: 0;
|
|
123
|
+
top: 50%;
|
|
124
|
+
transform: translateY(-50%);
|
|
121
125
|
margin: 0;
|
|
122
126
|
pointer-events: none;
|
|
123
127
|
}
|
|
@@ -130,13 +130,23 @@
|
|
|
130
130
|
|
|
131
131
|
let root: HTMLDivElement | null = $state(null);
|
|
132
132
|
|
|
133
|
-
// The
|
|
134
|
-
// field
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
// The browser decides whether validation is interactive and which invalid
|
|
134
|
+
// field is first. Forward the carrier's actual focus to the visible slider;
|
|
135
|
+
// an invalid event alone must not move focus during checkValidity().
|
|
136
|
+
function handleNativeFocus(event: FocusEvent & { currentTarget: HTMLInputElement }): void {
|
|
137
|
+
const carrier = event.currentTarget;
|
|
138
|
+
// Let interactive validation finish before moving focus, so Firefox's
|
|
139
|
+
// native validation UI cannot swallow the user's next star click.
|
|
140
|
+
queueMicrotask(() => {
|
|
141
|
+
const carrierRoot = carrier.getRootNode();
|
|
142
|
+
if (
|
|
143
|
+
root?.isConnected &&
|
|
144
|
+
'activeElement' in carrierRoot &&
|
|
145
|
+
carrierRoot.activeElement === carrier
|
|
146
|
+
) {
|
|
147
|
+
root.focus();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
140
150
|
}
|
|
141
151
|
|
|
142
152
|
// A native number input can't distinguish "explicitly rated 0" from "never touched",
|
|
@@ -191,25 +201,27 @@
|
|
|
191
201
|
{/if}
|
|
192
202
|
</span>
|
|
193
203
|
{/each}
|
|
194
|
-
<input
|
|
195
|
-
type="number"
|
|
196
|
-
class="native-input"
|
|
197
|
-
tabindex={-1}
|
|
198
|
-
aria-hidden="true"
|
|
199
|
-
min={0}
|
|
200
|
-
max={hasValidMax ? max : null}
|
|
201
|
-
step={allowHalf ? 0.5 : 1}
|
|
202
|
-
value={submittedValue === null ? '' : submittedValue}
|
|
203
|
-
{disabled}
|
|
204
|
-
{required}
|
|
205
|
-
name={typeof name === 'string' && submittedValue !== null ? name : null}
|
|
206
|
-
form={typeof form === 'string' ? form : null}
|
|
207
|
-
oninvalid={handleInvalid}
|
|
208
|
-
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
209
|
-
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
210
|
-
/>
|
|
211
204
|
</div>
|
|
212
205
|
|
|
206
|
+
<!-- Native form participation remains outside the interactive accessibility owner. -->
|
|
207
|
+
<input
|
|
208
|
+
type="number"
|
|
209
|
+
class="native-input"
|
|
210
|
+
tabindex={-1}
|
|
211
|
+
aria-hidden="true"
|
|
212
|
+
min={0}
|
|
213
|
+
max={hasValidMax ? max : null}
|
|
214
|
+
step={allowHalf ? 0.5 : 1}
|
|
215
|
+
value={submittedValue === null ? '' : submittedValue}
|
|
216
|
+
{disabled}
|
|
217
|
+
{required}
|
|
218
|
+
name={typeof name === 'string' && submittedValue !== null ? name : null}
|
|
219
|
+
form={typeof form === 'string' ? form : null}
|
|
220
|
+
onfocus={handleNativeFocus}
|
|
221
|
+
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
222
|
+
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
223
|
+
/>
|
|
224
|
+
|
|
213
225
|
<style>
|
|
214
226
|
.rating-group {
|
|
215
227
|
display: var(--rating-group-display, inline-flex);
|
|
@@ -285,8 +297,8 @@
|
|
|
285
297
|
.native-input {
|
|
286
298
|
position: absolute;
|
|
287
299
|
opacity: 0;
|
|
288
|
-
width:
|
|
289
|
-
height:
|
|
300
|
+
width: 1px;
|
|
301
|
+
height: 1px;
|
|
290
302
|
pointer-events: none;
|
|
291
303
|
}
|
|
292
304
|
</style>
|
|
@@ -635,6 +635,14 @@
|
|
|
635
635
|
}
|
|
636
636
|
/* Outside the in-menu variant, Tab always means focus is leaving. */
|
|
637
637
|
if (!searchInMenu) {
|
|
638
|
+
// Closing is rendered after this keydown. Firefox otherwise includes
|
|
639
|
+
// the still-present scrollable dropdown in native Tab navigation,
|
|
640
|
+
// then loses focus to body when that temporary target is removed.
|
|
641
|
+
// Exclude the closing panel immediately so native forward and backward
|
|
642
|
+
// navigation keep their real next stop, including across a shadow root.
|
|
643
|
+
if (dropdownEl !== null) {
|
|
644
|
+
dropdownEl.inert = true;
|
|
645
|
+
}
|
|
638
646
|
close();
|
|
639
647
|
break;
|
|
640
648
|
}
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
}: StatCardProperties = $props();
|
|
32
32
|
|
|
33
33
|
const isInteractive = $derived(typeof onclick === 'function');
|
|
34
|
+
let actionButton: HTMLButtonElement | null = $state(null);
|
|
35
|
+
const actionName = $derived(
|
|
36
|
+
[title, value, subtitle]
|
|
37
|
+
.filter((text) => typeof text === 'string' && text.length > 0)
|
|
38
|
+
.join(' ') || 'Open metric details'
|
|
39
|
+
);
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Which ONE value on this card rolls.
|
|
@@ -154,21 +160,26 @@
|
|
|
154
160
|
hasTitle || Boolean(checkbox) || typeof headerRight === 'function'
|
|
155
161
|
);
|
|
156
162
|
|
|
157
|
-
const handleKeydown = (event: KeyboardEvent): void => {
|
|
158
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
159
|
-
if (event.key === ' ') {
|
|
160
|
-
event.preventDefault();
|
|
161
|
-
}
|
|
162
|
-
if (event.currentTarget instanceof HTMLElement) {
|
|
163
|
-
event.currentTarget.click();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
168
163
|
const handleCheckboxChange = (checked: boolean): void => {
|
|
169
164
|
oncheckboxchange?.(checked);
|
|
170
165
|
};
|
|
171
166
|
|
|
167
|
+
const handleCardClick = (event: MouseEvent): void => {
|
|
168
|
+
const target = event.composedPath()[0];
|
|
169
|
+
const control =
|
|
170
|
+
target instanceof Element
|
|
171
|
+
? target.closest(
|
|
172
|
+
'button, a[href], input, select, textarea, [role="button"], [role="checkbox"], [role="link"], [contenteditable="true"]'
|
|
173
|
+
)
|
|
174
|
+
: null;
|
|
175
|
+
// Snippet and slotted controls own their actions. Only this card's action
|
|
176
|
+
// button, or a click on its noninteractive body, delegates to onclick.
|
|
177
|
+
if (control !== null && control !== actionButton) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
onclick?.(event);
|
|
181
|
+
};
|
|
182
|
+
|
|
172
183
|
/**
|
|
173
184
|
* Keep header checkbox interaction from bubbling to the card's click/keydown
|
|
174
185
|
* handler — otherwise toggling the checkbox on an interactive card would also
|
|
@@ -179,18 +190,21 @@
|
|
|
179
190
|
};
|
|
180
191
|
</script>
|
|
181
192
|
|
|
182
|
-
<!--
|
|
193
|
+
<!-- Card pointer activation delegates to the same native action button's handler.
|
|
194
|
+
Child controls remain siblings of that button and keep their own semantics. -->
|
|
195
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
183
196
|
<div
|
|
184
197
|
{@attach measurePrimary(valueSignature)}
|
|
185
198
|
class="statcard {classes ?? ''}"
|
|
186
199
|
class:statcard-interactive={isInteractive}
|
|
187
200
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
188
201
|
testID={typeof testId === 'string' ? testId : null}
|
|
189
|
-
|
|
190
|
-
tabindex={isInteractive ? 0 : null}
|
|
191
|
-
onclick={isInteractive ? onclick : null}
|
|
192
|
-
onkeydown={isInteractive ? handleKeydown : null}
|
|
202
|
+
onclick={isInteractive ? handleCardClick : null}
|
|
193
203
|
>
|
|
204
|
+
{#if isInteractive}
|
|
205
|
+
<button bind:this={actionButton} type="button" class="statcard-action" aria-label={actionName}
|
|
206
|
+
></button>
|
|
207
|
+
{/if}
|
|
194
208
|
{#if hasHeaderContent}
|
|
195
209
|
<div
|
|
196
210
|
class="statcard-header"
|
|
@@ -465,14 +479,51 @@
|
|
|
465
479
|
}
|
|
466
480
|
|
|
467
481
|
.statcard-interactive {
|
|
482
|
+
position: relative;
|
|
468
483
|
cursor: var(--statcard-cursor, pointer);
|
|
469
484
|
}
|
|
470
485
|
|
|
471
|
-
.statcard-interactive:focus-visible {
|
|
486
|
+
.statcard-interactive:has(> .statcard-action:focus-visible) {
|
|
472
487
|
outline: var(--statcard-focus-outline, 2px solid currentColor);
|
|
473
488
|
outline-offset: var(--statcard-focus-outline-offset, 2px);
|
|
474
489
|
}
|
|
475
490
|
|
|
491
|
+
.statcard-action {
|
|
492
|
+
position: absolute;
|
|
493
|
+
inset: 0;
|
|
494
|
+
z-index: 0;
|
|
495
|
+
padding: 0;
|
|
496
|
+
border: 0;
|
|
497
|
+
border-radius: inherit;
|
|
498
|
+
background: transparent;
|
|
499
|
+
cursor: inherit;
|
|
500
|
+
outline: none;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.statcard-interactive > :not(.statcard-action) {
|
|
504
|
+
position: relative;
|
|
505
|
+
z-index: 1;
|
|
506
|
+
pointer-events: none;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* Text clicks reach the card action; controls and tooltip triggers stay usable. */
|
|
510
|
+
.statcard-interactive
|
|
511
|
+
:global(
|
|
512
|
+
:is(
|
|
513
|
+
button,
|
|
514
|
+
input,
|
|
515
|
+
select,
|
|
516
|
+
textarea,
|
|
517
|
+
a,
|
|
518
|
+
slot,
|
|
519
|
+
[role='checkbox'],
|
|
520
|
+
[tabindex],
|
|
521
|
+
.tooltip-container
|
|
522
|
+
)
|
|
523
|
+
) {
|
|
524
|
+
pointer-events: auto;
|
|
525
|
+
}
|
|
526
|
+
|
|
476
527
|
.statcard-header {
|
|
477
528
|
order: var(--statcard-header-order, 0);
|
|
478
529
|
display: flex;
|
|
@@ -183,7 +183,7 @@ export type OptionalStatCardProperties = {
|
|
|
183
183
|
classes?: string;
|
|
184
184
|
};
|
|
185
185
|
export type StatCardEventProperties = {
|
|
186
|
-
/** Makes the card interactive
|
|
186
|
+
/** Makes the card interactive with a native action button; child controls remain independent. */
|
|
187
187
|
onclick?: (event: MouseEvent) => void;
|
|
188
188
|
/** Fired when the header checkbox changes. */
|
|
189
189
|
oncheckboxchange?: (checked: boolean) => void;
|
package/dist/Stepper/Step.svelte
CHANGED
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
onkeydown={handleKeydown}
|
|
65
65
|
role={suppressRoleAndTabindex ? null : 'button'}
|
|
66
66
|
tabindex={suppressRoleAndTabindex ? null : 0}
|
|
67
|
-
aria-label={ariaLabel ?? null}
|
|
68
|
-
aria-labelledby={ariaLabel ? null : labelId}
|
|
67
|
+
aria-label={suppressRoleAndTabindex ? null : (ariaLabel ?? null)}
|
|
68
|
+
aria-labelledby={suppressRoleAndTabindex || ariaLabel ? null : labelId}
|
|
69
69
|
aria-current={status === 'active' ? 'step' : null}
|
|
70
70
|
>
|
|
71
71
|
{#if typeof icon === 'string' && icon.length > 0}
|
package/dist/Table/Table.svelte
CHANGED
|
@@ -225,11 +225,24 @@
|
|
|
225
225
|
// viewports, but a bare scroll container gives no visual hint that more
|
|
226
226
|
// columns exist. Track whether either edge has hidden content and surface
|
|
227
227
|
// it as edge scrims (see .table-scroll-shell styles).
|
|
228
|
+
// Caption identity must reach the actual keyboard scroll owner, not only its
|
|
229
|
+
// descendant table. Groups keep nested axes out of the landmark namespace and
|
|
230
|
+
// add no tab stop or keyboard handler; native overflow/fit focus stays intact.
|
|
231
|
+
let scrollCaption = $derived(
|
|
232
|
+
typeof caption === 'string' && caption.trim().length > 0 ? caption : null
|
|
233
|
+
);
|
|
234
|
+
let hasVerticalScrollOwner = $state(false);
|
|
228
235
|
let canScrollLeft = $state(false);
|
|
229
236
|
let canScrollRight = $state(false);
|
|
230
237
|
|
|
231
238
|
const trackHorizontalScroll = (scrollNode: HTMLElement) => {
|
|
239
|
+
const containerNode = scrollNode.closest<HTMLElement>('.table-container');
|
|
232
240
|
const updateScrollHints = () => {
|
|
241
|
+
const verticalOverflow = containerNode ? getComputedStyle(containerNode).overflowY : '';
|
|
242
|
+
hasVerticalScrollOwner =
|
|
243
|
+
containerNode !== null &&
|
|
244
|
+
(verticalOverflow === 'auto' || verticalOverflow === 'scroll') &&
|
|
245
|
+
containerNode.scrollHeight > containerNode.clientHeight;
|
|
233
246
|
canScrollLeft = scrollNode.scrollLeft > 2;
|
|
234
247
|
canScrollRight = scrollNode.scrollLeft + scrollNode.clientWidth < scrollNode.scrollWidth - 2;
|
|
235
248
|
};
|
|
@@ -239,6 +252,9 @@
|
|
|
239
252
|
// (async rows/columns arriving) change scrollWidth, so observe both.
|
|
240
253
|
const hintResizeObserver = new ResizeObserver(updateScrollHints);
|
|
241
254
|
hintResizeObserver.observe(scrollNode);
|
|
255
|
+
if (containerNode !== null) {
|
|
256
|
+
hintResizeObserver.observe(containerNode);
|
|
257
|
+
}
|
|
242
258
|
const tableElement = scrollNode.querySelector('table');
|
|
243
259
|
if (tableElement) {
|
|
244
260
|
hintResizeObserver.observe(tableElement);
|
|
@@ -836,6 +852,8 @@
|
|
|
836
852
|
<div
|
|
837
853
|
class="table-container {isTableScrollable ? 'scrollable-table' : ''} {classes ?? ''}"
|
|
838
854
|
class:table-mobile-cards={mobileCardLayout}
|
|
855
|
+
role={scrollCaption !== null && hasVerticalScrollOwner ? 'group' : null}
|
|
856
|
+
aria-label={hasVerticalScrollOwner ? scrollCaption : null}
|
|
839
857
|
data-pw={testId}
|
|
840
858
|
testID={testId}
|
|
841
859
|
>
|
|
@@ -844,7 +862,12 @@
|
|
|
844
862
|
class:scrollable-left={canScrollLeft}
|
|
845
863
|
class:scrollable-right={canScrollRight}
|
|
846
864
|
>
|
|
847
|
-
<div
|
|
865
|
+
<div
|
|
866
|
+
class="table-scroll"
|
|
867
|
+
role={scrollCaption !== null ? 'group' : null}
|
|
868
|
+
aria-label={scrollCaption}
|
|
869
|
+
use:trackHorizontalScroll
|
|
870
|
+
>
|
|
848
871
|
<table role={mobileRole('table')}>
|
|
849
872
|
{#if caption}
|
|
850
873
|
<caption class="sr-only">{caption}</caption>
|
|
@@ -878,8 +901,9 @@
|
|
|
878
901
|
/>
|
|
879
902
|
</th>
|
|
880
903
|
{:else if isCheckboxMode && isSingleSelect}
|
|
881
|
-
<!--
|
|
904
|
+
<!-- Selection is a real column even without a select-all action. -->
|
|
882
905
|
<th
|
|
906
|
+
aria-label={labels?.selectionColumn ?? 'Row selection'}
|
|
883
907
|
class="table-header table-checkbox-col"
|
|
884
908
|
class:table-header-sticky={isStickyHeader}
|
|
885
909
|
role={mobileRole('columnheader')}
|
|
@@ -1084,6 +1108,7 @@
|
|
|
1084
1108
|
{#each paginatedTableData as row, pageRowIndex (rowIdByRow.get(row) ?? pageRowIndex)}
|
|
1085
1109
|
{@const rowIndex = pageRowIndex + rowIndexOffset}
|
|
1086
1110
|
{@const originalIndex = originalIndexByRow.get(row) ?? rowIndex}
|
|
1111
|
+
{@const rowContext = rowLabels.get(row) ?? `row ${originalIndex + 1}`}
|
|
1087
1112
|
{@const rowId = rowIdByRow.get(row) ?? String(rowIndex)}
|
|
1088
1113
|
{@const rowDisabled = isCheckboxMode && isRowDisabled(rowId)}
|
|
1089
1114
|
{@const rowSelected = isCheckboxMode && isRowSelected(rowId)}
|
|
@@ -1094,6 +1119,9 @@
|
|
|
1094
1119
|
class:table-summary-row={summaryRowIndex !== null &&
|
|
1095
1120
|
originalIndex === summaryRowIndex}
|
|
1096
1121
|
role={mobileRole('row')}
|
|
1122
|
+
aria-label={isRowClickable
|
|
1123
|
+
? (labels?.rowAction?.(rowContext) ?? rowContext)
|
|
1124
|
+
: null}
|
|
1097
1125
|
data-pw={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
|
|
1098
1126
|
testID={typeof getRowTestId === 'function' ? getRowTestId(row, rowIndex) : null}
|
|
1099
1127
|
onclick={isRowClickable
|
|
@@ -1280,6 +1308,8 @@
|
|
|
1280
1308
|
{/if}
|
|
1281
1309
|
{#if !hideSteppers}
|
|
1282
1310
|
<Pagination
|
|
1311
|
+
ariaLabel={labels?.pageNavigation ??
|
|
1312
|
+
(tableTitle ? `${tableTitle} pagination` : 'Pagination')}
|
|
1283
1313
|
totalPages={paginationTotalPages}
|
|
1284
1314
|
currentPage={effectivePage}
|
|
1285
1315
|
hasMore={pagination.hasMore ?? false}
|
|
@@ -524,12 +524,11 @@ export type TableProperties = OptionalTableProperties & TableEventProperties;
|
|
|
524
524
|
* Accessible names Table generates for its own controls, for callers who do
|
|
525
525
|
* not ship an English UI.
|
|
526
526
|
*
|
|
527
|
-
* Every member is optional
|
|
528
|
-
*
|
|
529
|
-
* two add a name to a control that previously had none.
|
|
527
|
+
* Every member is optional. Control labels use English defaults; rowAction
|
|
528
|
+
* uses the readable source-row caption.
|
|
530
529
|
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
530
|
+
* Members that depend on a column or row are functions rather than templates
|
|
531
|
+
* with a placeholder: a translator needs to
|
|
533
532
|
* put the header where their grammar wants it, not where an English sentence
|
|
534
533
|
* happens to leave a slot.
|
|
535
534
|
*
|
|
@@ -546,6 +545,8 @@ export type TableLabels = {
|
|
|
546
545
|
* naming an empty row. Default: `Select row ${rowId || 'non-selectable'}`. */
|
|
547
546
|
selectRow?: (rowId: string) => string;
|
|
548
547
|
/** Header checkbox that selects every row on the page. Default: `Select all rows`. */
|
|
548
|
+
/** Accessible header name for the row-selection column, including single-selection mode. */
|
|
549
|
+
selectionColumn?: string;
|
|
549
550
|
selectAllRows?: string;
|
|
550
551
|
/** Button that empties the search box. Default: `Clear search`. */
|
|
551
552
|
clearSearch?: string;
|
|
@@ -553,8 +554,12 @@ export type TableLabels = {
|
|
|
553
554
|
closeSearch?: string;
|
|
554
555
|
/** Built-in editor name. Default: `${columnLabel} for ${rowLabel}`. */
|
|
555
556
|
cellEditor?: (columnLabel: string, rowLabel: string) => string;
|
|
557
|
+
/** Clickable row name. Receives stable readable source-row context. Default: rowLabel. */
|
|
558
|
+
rowAction?: (rowLabel: string) => string;
|
|
556
559
|
/** Page-size selector in the built-in paginator footer. Default: `Rows per page`. */
|
|
557
560
|
rowsPerPage?: string;
|
|
561
|
+
/** Page-navigation landmark name. Default: `Pagination`. */
|
|
562
|
+
pageNavigation?: string;
|
|
558
563
|
};
|
|
559
564
|
export type OptionalTableProperties = {
|
|
560
565
|
labels?: TableLabels;
|
|
@@ -669,6 +674,7 @@ export type OptionalTableProperties = {
|
|
|
669
674
|
isTableScrollable?: boolean;
|
|
670
675
|
isContentScrollable?: boolean;
|
|
671
676
|
testId?: string;
|
|
677
|
+
/** Visually hidden native table caption; non-empty also names existing scroll groups. */
|
|
672
678
|
caption?: string;
|
|
673
679
|
sortAscIcon?: Snippet;
|
|
674
680
|
sortDescIcon?: Snippet;
|