@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
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
removeIcon,
|
|
27
27
|
fileIcon,
|
|
28
28
|
testId,
|
|
29
|
-
classes
|
|
29
|
+
classes,
|
|
30
|
+
ariaLabel = 'Pending attachments'
|
|
30
31
|
}: AttachmentChipRowProperties = $props();
|
|
31
32
|
</script>
|
|
32
33
|
|
|
33
34
|
{#if images.length > 0 || videos.length > 0 || files.length > 0}
|
|
34
35
|
<Scroller
|
|
35
36
|
direction="horizontal"
|
|
36
|
-
ariaLabel
|
|
37
|
+
{ariaLabel}
|
|
37
38
|
showArrows={false}
|
|
38
39
|
hideScrollbar={false}
|
|
39
40
|
showGradient={false}
|
package/dist/Book/Book.svelte
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
showNavigation = true,
|
|
12
12
|
showPageIndicator = true,
|
|
13
13
|
enableSwipe = false,
|
|
14
|
+
ariaLabel = 'Book',
|
|
14
15
|
testId,
|
|
15
16
|
previousIcon,
|
|
16
17
|
nextIcon,
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
onmousedown={handleMouseDown}
|
|
109
110
|
onmouseup={handleMouseUp}
|
|
110
111
|
role="region"
|
|
112
|
+
aria-label={ariaLabel}
|
|
111
113
|
tabindex="0"
|
|
112
114
|
>
|
|
113
115
|
<div class="book-viewport">
|
|
@@ -14,6 +14,8 @@ export type OptionalBookProperties = {
|
|
|
14
14
|
showNavigation?: boolean;
|
|
15
15
|
showPageIndicator?: boolean;
|
|
16
16
|
enableSwipe?: boolean;
|
|
17
|
+
/** Accessible name for the keyboard-operated book region. Defaults to "Book". */
|
|
18
|
+
ariaLabel?: string;
|
|
17
19
|
testId?: string;
|
|
18
20
|
previousIcon?: Snippet;
|
|
19
21
|
nextIcon?: Snippet;
|
package/dist/Chat/Chat.svelte
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
hideScrollbar = false,
|
|
24
24
|
jump = true,
|
|
25
25
|
jumpLabel = 'Jump to latest',
|
|
26
|
+
messageListLabel,
|
|
26
27
|
jumpIcon,
|
|
27
28
|
toolStatus = null,
|
|
28
29
|
suggestions = [],
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
{/if}
|
|
98
99
|
|
|
99
100
|
<ChatMessageList
|
|
101
|
+
ariaLabel={messageListLabel ?? (title.trim() ? `${title} messages` : 'Conversation messages')}
|
|
100
102
|
{messages}
|
|
101
103
|
{autoscroll}
|
|
102
104
|
{scrollPolicy}
|
|
@@ -23,6 +23,8 @@ export type OptionalChatProperties = {
|
|
|
23
23
|
hideScrollbar?: ChatMessageListProperties['hideScrollbar'];
|
|
24
24
|
jump?: ChatMessageListProperties['jump'];
|
|
25
25
|
jumpLabel?: ChatMessageListProperties['jumpLabel'];
|
|
26
|
+
/** Transcript name; defaults to the chat title plus `messages`, or `Conversation messages`. */
|
|
27
|
+
messageListLabel?: ChatMessageListProperties['ariaLabel'];
|
|
26
28
|
jumpIcon?: ChatMessageListProperties['jumpIcon'];
|
|
27
29
|
toolStatus?: ChatToolStatus | null;
|
|
28
30
|
suggestions?: ChatSuggestion[];
|
package/dist/Chat/types.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type ChatMessageData = {
|
|
|
11
11
|
* MarkdownText). Wins over `html` and `content` in the bubble.
|
|
12
12
|
*/
|
|
13
13
|
markdown?: string;
|
|
14
|
+
/** Accessible table scroll-region name for static and streamed markdown bubbles. */
|
|
15
|
+
markdownTableLabel?: string;
|
|
14
16
|
streaming?: boolean;
|
|
15
17
|
/**
|
|
16
18
|
* Reveal this message progressively as it grows. Opt-in per message so it cannot stack with
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
import { onDestroy, onMount } from 'svelte';
|
|
11
11
|
import { partyOf } from '../Chat/roles';
|
|
12
12
|
import type { ChatMessageProperties } from './properties';
|
|
13
|
+
import type { RenderMarkdownOptions } from '../MarkdownText/properties';
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
16
|
role,
|
|
16
17
|
content = '',
|
|
17
18
|
html,
|
|
18
19
|
markdown,
|
|
20
|
+
markdownTableLabel,
|
|
19
21
|
body,
|
|
20
22
|
streaming = false,
|
|
21
23
|
typewriter = false,
|
|
@@ -90,7 +92,9 @@
|
|
|
90
92
|
`marked` — a peer dependency — is only pulled in at runtime, never during
|
|
91
93
|
SSR, and rendering `markdown` degrades to the `html`/`content` fallback
|
|
92
94
|
until the module resolves or when the peer is absent. */
|
|
93
|
-
let renderMarkdownFn = $state<
|
|
95
|
+
let renderMarkdownFn = $state<
|
|
96
|
+
((source: string, options?: RenderMarkdownOptions) => string) | null
|
|
97
|
+
>(null);
|
|
94
98
|
|
|
95
99
|
onMount(() => {
|
|
96
100
|
void import('../MarkdownText/markdown')
|
|
@@ -102,8 +106,19 @@
|
|
|
102
106
|
});
|
|
103
107
|
});
|
|
104
108
|
|
|
109
|
+
// Both rendering branches share the same options-aware renderer. Capture the label
|
|
110
|
+
// reactively so a locale/name update also refreshes already-revealed typewriter HTML.
|
|
111
|
+
let messageMarkdownRenderer = $derived.by(() => {
|
|
112
|
+
const renderer = renderMarkdownFn;
|
|
113
|
+
if (renderer === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const options: RenderMarkdownOptions =
|
|
117
|
+
typeof markdownTableLabel === 'string' ? { tableLabel: markdownTableLabel } : {};
|
|
118
|
+
return (source: string) => renderer(source, options);
|
|
119
|
+
});
|
|
105
120
|
let effectiveHtml = $derived(
|
|
106
|
-
hasMarkdown &&
|
|
121
|
+
hasMarkdown && messageMarkdownRenderer !== null ? messageMarkdownRenderer(markdown ?? '') : html
|
|
107
122
|
);
|
|
108
123
|
let hasHtml = $derived(typeof effectiveHtml === 'string' && effectiveHtml.length > 0);
|
|
109
124
|
|
|
@@ -128,7 +143,7 @@
|
|
|
128
143
|
(!hasMarkdown || renderMarkdownFn !== null)
|
|
129
144
|
);
|
|
130
145
|
let typewriterRenderText = $derived(
|
|
131
|
-
hasMarkdown &&
|
|
146
|
+
hasMarkdown && messageMarkdownRenderer !== null ? { renderText: messageMarkdownRenderer } : {}
|
|
132
147
|
);
|
|
133
148
|
// Clamped at the boundary the way LoadingDots clamps `dots`, so the prop's floor is part
|
|
134
149
|
// of this component's contract rather than something a consumer discovers. The finite
|
|
@@ -20,6 +20,12 @@ export type OptionalChatMessageProperties = {
|
|
|
20
20
|
* (for server-rendered markdown use `MarkdownText` or `renderMarkdown`).
|
|
21
21
|
*/
|
|
22
22
|
markdown?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Accessible name for each table scroll region rendered from `markdown`, including
|
|
25
|
+
* progressive/typewriter rendering. Omitted or empty leaves the shared renderer's
|
|
26
|
+
* unnamed, keyboard-scrollable wrapper unchanged. Does not alter `html` or `body`.
|
|
27
|
+
*/
|
|
28
|
+
markdownTableLabel?: string;
|
|
23
29
|
/**
|
|
24
30
|
* Replaces the rendered bubble body with arbitrary markup while keeping the
|
|
25
31
|
* message chrome (role styling, avatar, header, attachments, copy/retry/
|
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
onretry,
|
|
29
29
|
onfeedback,
|
|
30
30
|
testId,
|
|
31
|
-
classes
|
|
31
|
+
classes,
|
|
32
|
+
ariaLabel = 'Conversation messages'
|
|
32
33
|
}: ChatMessageListProperties = $props();
|
|
33
34
|
|
|
34
35
|
let listEl: HTMLElement | null = $state(null);
|
|
35
36
|
let innerEl: HTMLElement | null = $state(null);
|
|
36
37
|
let atBottom = $state(true);
|
|
37
38
|
let scrollable = $state(false);
|
|
39
|
+
let keyboardScrollable = $state(false);
|
|
38
40
|
let pinActive = false;
|
|
39
41
|
// Set when a turn without pinHold has asked for its reservation back, but the reply is not yet
|
|
40
42
|
// tall enough to hold the pinned position without it. Cleared by tryReleasePin.
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
|
|
79
81
|
function reportScrollState(node: HTMLElement): void {
|
|
80
82
|
atBottom = isNearBottom(node);
|
|
83
|
+
keyboardScrollable = node.scrollHeight - node.clientHeight > 1;
|
|
81
84
|
scrollable = node.scrollHeight - node.clientHeight > NEAR_BOTTOM_THRESHOLD;
|
|
82
85
|
onscrollstate?.({ atBottom, scrollable });
|
|
83
86
|
}
|
|
@@ -260,19 +263,29 @@
|
|
|
260
263
|
if (typeof ResizeObserver === 'undefined') {
|
|
261
264
|
return;
|
|
262
265
|
}
|
|
266
|
+
let pendingFrame: number | null = null;
|
|
263
267
|
const observer = new ResizeObserver(() => {
|
|
264
|
-
if (
|
|
268
|
+
if (pendingFrame !== null) {
|
|
265
269
|
return;
|
|
266
270
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
pendingFrame = requestAnimationFrame(() => {
|
|
272
|
+
pendingFrame = null;
|
|
273
|
+
if (listEl === null || !node.isConnected) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (scrollPolicy === 'near-bottom' && autoscroll && atBottom) {
|
|
277
|
+
listEl.scrollTop = listEl.scrollHeight;
|
|
278
|
+
}
|
|
279
|
+
tryReleasePin();
|
|
280
|
+
reportScrollState(listEl);
|
|
281
|
+
});
|
|
272
282
|
});
|
|
273
283
|
observer.observe(node);
|
|
274
284
|
return {
|
|
275
285
|
destroy(): void {
|
|
286
|
+
if (pendingFrame !== null) {
|
|
287
|
+
cancelAnimationFrame(pendingFrame);
|
|
288
|
+
}
|
|
276
289
|
observer.disconnect();
|
|
277
290
|
}
|
|
278
291
|
};
|
|
@@ -291,9 +304,14 @@
|
|
|
291
304
|
};
|
|
292
305
|
</script>
|
|
293
306
|
|
|
307
|
+
<!-- The transcript is an independent scrolling surface. Native keyboard focus
|
|
308
|
+
enables the browser's scroll keys while it overflows, even without actions. -->
|
|
309
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
294
310
|
<div
|
|
295
311
|
class="chat-message-list {classes ?? ''}"
|
|
296
312
|
role="log"
|
|
313
|
+
aria-label={ariaLabel}
|
|
314
|
+
tabindex={keyboardScrollable ? 0 : null}
|
|
297
315
|
aria-live="polite"
|
|
298
316
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
299
317
|
testID={typeof testId === 'string' ? testId : null}
|
|
@@ -323,6 +341,7 @@
|
|
|
323
341
|
content={msg.content}
|
|
324
342
|
html={msg.html}
|
|
325
343
|
markdown={msg.markdown}
|
|
344
|
+
markdownTableLabel={msg.markdownTableLabel}
|
|
326
345
|
body={typeof messageBody === 'function' ? bodyFor : null}
|
|
327
346
|
streaming={msg.streaming}
|
|
328
347
|
typewriter={msg.typewriter}
|
|
@@ -352,6 +371,11 @@
|
|
|
352
371
|
</div>
|
|
353
372
|
|
|
354
373
|
<style>
|
|
374
|
+
.chat-message-list:focus-visible {
|
|
375
|
+
outline: var(--chat-message-list-focus-outline, 2px solid currentColor);
|
|
376
|
+
outline-offset: -2px;
|
|
377
|
+
}
|
|
378
|
+
|
|
355
379
|
.chat-message-list {
|
|
356
380
|
box-sizing: border-box;
|
|
357
381
|
display: flex;
|
|
@@ -44,6 +44,8 @@ export type OptionalChatMessageListProperties = {
|
|
|
44
44
|
allowCopy?: boolean;
|
|
45
45
|
testId?: string;
|
|
46
46
|
classes?: string;
|
|
47
|
+
/** Accessible transcript name. Default: `Conversation messages`. */
|
|
48
|
+
ariaLabel?: string;
|
|
47
49
|
};
|
|
48
50
|
export type ChatMessageListEventProperties = {
|
|
49
51
|
/**
|
|
@@ -82,12 +82,20 @@
|
|
|
82
82
|
onclick?.(selected);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
// Let native validation choose the first invalid control. Redirect only its
|
|
86
|
+
// actual focus to the visible card, so checkValidity() stays a silent query
|
|
87
|
+
// and later invalid fields cannot replace the first field's focus.
|
|
88
|
+
function handleNativeFocus(event: FocusEvent & { currentTarget: HTMLInputElement }): void {
|
|
89
|
+
const carrier = event.currentTarget;
|
|
90
|
+
// Finish native interactive validation before moving focus. Firefox can
|
|
91
|
+
// open its validation UI after a synchronous handoff and swallow the next
|
|
92
|
+
// pointer click. A later owner or an unmounted card must keep its focus.
|
|
93
|
+
queueMicrotask(() => {
|
|
94
|
+
const root = carrier.getRootNode();
|
|
95
|
+
if (card?.isConnected && 'activeElement' in root && root.activeElement === carrier) {
|
|
96
|
+
card.focus();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
function handleKeyDown(e: KeyboardEvent): void {
|
|
@@ -143,21 +151,6 @@
|
|
|
143
151
|
testID={testId}
|
|
144
152
|
{@attach groupMembership}
|
|
145
153
|
>
|
|
146
|
-
<input
|
|
147
|
-
type={mode === 'radio' ? 'radio' : 'checkbox'}
|
|
148
|
-
class="native-control"
|
|
149
|
-
checked={selected}
|
|
150
|
-
{disabled}
|
|
151
|
-
{value}
|
|
152
|
-
{required}
|
|
153
|
-
name={typeof name === 'string' ? name : null}
|
|
154
|
-
form={typeof form === 'string' ? form : null}
|
|
155
|
-
oninvalid={handleInvalid}
|
|
156
|
-
tabindex={-1}
|
|
157
|
-
aria-hidden="true"
|
|
158
|
-
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
159
|
-
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
160
|
-
/>
|
|
161
154
|
{#if typeof children === 'function'}
|
|
162
155
|
{@render children()}
|
|
163
156
|
{/if}
|
|
@@ -171,6 +164,23 @@
|
|
|
171
164
|
{/if}
|
|
172
165
|
</div>
|
|
173
166
|
|
|
167
|
+
<!-- Native form participation remains outside the interactive accessibility owner. -->
|
|
168
|
+
<input
|
|
169
|
+
type={mode === 'radio' ? 'radio' : 'checkbox'}
|
|
170
|
+
class="native-control"
|
|
171
|
+
checked={selected}
|
|
172
|
+
{disabled}
|
|
173
|
+
{value}
|
|
174
|
+
{required}
|
|
175
|
+
name={typeof name === 'string' ? name : null}
|
|
176
|
+
form={typeof form === 'string' ? form : null}
|
|
177
|
+
onfocus={handleNativeFocus}
|
|
178
|
+
tabindex={-1}
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
data-pw={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
181
|
+
testID={typeof testId === 'string' ? `${testId}-native-input` : null}
|
|
182
|
+
/>
|
|
183
|
+
|
|
174
184
|
{#if field.showsError}
|
|
175
185
|
<div
|
|
176
186
|
id={field.errorId}
|
|
@@ -197,8 +207,8 @@
|
|
|
197
207
|
.native-control {
|
|
198
208
|
position: absolute;
|
|
199
209
|
opacity: 0;
|
|
200
|
-
width:
|
|
201
|
-
height:
|
|
210
|
+
width: 1px;
|
|
211
|
+
height: 1px;
|
|
202
212
|
margin: 0;
|
|
203
213
|
pointer-events: none;
|
|
204
214
|
}
|
|
@@ -134,13 +134,15 @@
|
|
|
134
134
|
}
|
|
135
135
|
</script>
|
|
136
136
|
|
|
137
|
-
<!--
|
|
138
|
-
|
|
137
|
+
<!-- This named content group moves with arrow keys; it can contain independent
|
|
138
|
+
controls, so button semantics would hide their accessibility semantics. -->
|
|
139
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
|
|
139
140
|
<div
|
|
140
141
|
class="draggable {classes ?? ''}"
|
|
141
142
|
class:dragging={active !== null}
|
|
142
143
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
143
144
|
tabindex={disabled ? -1 : 0}
|
|
145
|
+
role="group"
|
|
144
146
|
aria-label={dragLabel}
|
|
145
147
|
style:transform={`translate(${x}px, ${y}px)`}
|
|
146
148
|
onpointerdown={startDrag}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
src,
|
|
18
18
|
type,
|
|
19
19
|
alt = '',
|
|
20
|
+
ariaLabel = 'Video player',
|
|
20
21
|
autoplay = true,
|
|
21
22
|
loop = false,
|
|
22
23
|
controls = false,
|
|
@@ -212,13 +213,6 @@
|
|
|
212
213
|
onpause?.(event);
|
|
213
214
|
}
|
|
214
215
|
|
|
215
|
-
function handleVideoKeydown(event: KeyboardEvent): void {
|
|
216
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
217
|
-
event.preventDefault();
|
|
218
|
-
togglePlayback();
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
216
|
function handleTimeUpdate(): void {
|
|
223
217
|
if (videoPlayer === null) {
|
|
224
218
|
return;
|
|
@@ -390,6 +384,7 @@
|
|
|
390
384
|
bind:muted
|
|
391
385
|
{src}
|
|
392
386
|
class="media"
|
|
387
|
+
aria-label={ariaLabel}
|
|
393
388
|
{controls}
|
|
394
389
|
{autoplay}
|
|
395
390
|
{loop}
|
|
@@ -399,11 +394,6 @@
|
|
|
399
394
|
onpause={handlePause}
|
|
400
395
|
ontimeupdate={handleTimeUpdate}
|
|
401
396
|
onloadedmetadata={handleLoadedMetadata}
|
|
402
|
-
onclick={controls ? null : togglePlayback}
|
|
403
|
-
onkeydown={controls ? null : handleVideoKeydown}
|
|
404
|
-
role={controls ? null : 'button'}
|
|
405
|
-
tabindex={controls ? null : 0}
|
|
406
|
-
aria-label={controls ? null : playing ? 'Pause video' : 'Play video'}
|
|
407
397
|
>
|
|
408
398
|
{#if hasCaptions}
|
|
409
399
|
<track
|
|
@@ -417,6 +407,14 @@
|
|
|
417
407
|
</video>
|
|
418
408
|
|
|
419
409
|
{#if !controls}
|
|
410
|
+
<!-- A video cannot carry role="button". The full-surface native button
|
|
411
|
+
preserves pointer/keyboard playback without nesting the other controls. -->
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
class="media-surface-action"
|
|
415
|
+
aria-label={playing ? 'Pause video' : 'Play video'}
|
|
416
|
+
onclick={togglePlayback}
|
|
417
|
+
></button>
|
|
420
418
|
<div class="overlay">
|
|
421
419
|
<div class="center-controls">
|
|
422
420
|
<div class="control center-control">
|
|
@@ -571,6 +569,23 @@
|
|
|
571
569
|
--image-margin: 0px;
|
|
572
570
|
}
|
|
573
571
|
|
|
572
|
+
.media-surface-action {
|
|
573
|
+
position: absolute;
|
|
574
|
+
inset: 0;
|
|
575
|
+
padding: 0;
|
|
576
|
+
border: 0;
|
|
577
|
+
border-radius: inherit;
|
|
578
|
+
background: transparent;
|
|
579
|
+
cursor: pointer;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.media-surface-action:focus-visible {
|
|
583
|
+
/* Keep both colors inside the clipping container and visible over imagery. */
|
|
584
|
+
outline: var(--media-player-focus-outline, 2px solid #ffffff);
|
|
585
|
+
outline-offset: var(--media-player-focus-outline-offset, -2px);
|
|
586
|
+
box-shadow: var(--media-player-focus-box-shadow, inset 0 0 0 4px #000000);
|
|
587
|
+
}
|
|
588
|
+
|
|
574
589
|
.overlay {
|
|
575
590
|
/* Decorative overlay space leaves the native video pointer route operative. */
|
|
576
591
|
pointer-events: none;
|
|
@@ -601,7 +616,7 @@
|
|
|
601
616
|
reachable for keyboard users too -- :hover alone left them visibility:hidden (and
|
|
602
617
|
so out of the tab order) for anyone not using a mouse. */
|
|
603
618
|
.media-player:hover .overlay,
|
|
604
|
-
.
|
|
619
|
+
.media-player:focus-within .overlay {
|
|
605
620
|
background-color: var(--media-player-overlay-hover-color, #0000004d);
|
|
606
621
|
--center-controls-visibility: visible;
|
|
607
622
|
--bottom-controls-visibility: visible;
|
|
@@ -7,6 +7,8 @@ export type MandatoryMediaPlayerProperties = {
|
|
|
7
7
|
};
|
|
8
8
|
export type OptionalMediaPlayerProperties = {
|
|
9
9
|
alt?: string;
|
|
10
|
+
/** Accessible name for the video. Image naming uses alt. Defaults to "Video player". */
|
|
11
|
+
ariaLabel?: string;
|
|
10
12
|
fallback?: string;
|
|
11
13
|
autoplay?: boolean;
|
|
12
14
|
loop?: boolean;
|
package/dist/Modal/Modal.svelte
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import Img from '../Img/Img.svelte';
|
|
11
11
|
|
|
12
12
|
let overlayDiv: HTMLDivElement | null = $state(null);
|
|
13
|
+
let overlayButton: HTMLButtonElement | null = $state(null);
|
|
13
14
|
let modalContent: HTMLDivElement | null = $state(null);
|
|
14
15
|
let backPressed = $state(false);
|
|
15
16
|
// Captured in onMount right before focus moves in, restored in onDestroy.
|
|
@@ -54,12 +55,8 @@
|
|
|
54
55
|
overlayAriaLabel
|
|
55
56
|
}: ModalProperties = $props();
|
|
56
57
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// or activating it is already a no-op (see handleOverlayClick's guard) --
|
|
60
|
-
// announcing it as a focusable "button" in that case is worse for assistive
|
|
61
|
-
// tech than leaving it unannounced, same reasoning already applied to the
|
|
62
|
-
// header images below.
|
|
58
|
+
// A dismissible backdrop has its own native button, beside the content panel.
|
|
59
|
+
// It must never make the whole panel an interactive descendant of a button.
|
|
63
60
|
const overlayDismissible = $derived(
|
|
64
61
|
typeof onoverlayclick === 'function' || typeof ondismiss === 'function'
|
|
65
62
|
);
|
|
@@ -142,7 +139,7 @@
|
|
|
142
139
|
};
|
|
143
140
|
|
|
144
141
|
const handleOverlayClick = (event: MouseEvent): void => {
|
|
145
|
-
if (event.target === overlayDiv) {
|
|
142
|
+
if (event.target === overlayDiv || event.target === overlayButton) {
|
|
146
143
|
debounce(() => {
|
|
147
144
|
onoverlayclick?.();
|
|
148
145
|
ondismiss?.();
|
|
@@ -185,37 +182,6 @@
|
|
|
185
182
|
});
|
|
186
183
|
};
|
|
187
184
|
|
|
188
|
-
// The overlay's own Enter/Space activation, additional to the shared
|
|
189
|
-
// handleKeyDown (Tab/onkeydown-prop -- Escape is the dismissal module's,
|
|
190
|
-
// via handleEscape) that svelte:window already delivers here via
|
|
191
|
-
// bubbling -- this handler must not also call handleKeyDown itself, or
|
|
192
|
-
// every keydown on/under the overlay would run it twice (once from this
|
|
193
|
-
// direct call, once from the window listener seeing the same event bubble
|
|
194
|
-
// past it), double-firing onkeydown and re-running the Tab trap. Enter/Space
|
|
195
|
-
// activation is scoped to this handler, not folded into handleKeyDown itself,
|
|
196
|
-
// because handleKeyDown also runs
|
|
197
|
-
// from the window listener for a keypress anywhere in the modal (e.g. an
|
|
198
|
-
// input field) -- it must not treat every Enter/Space in the modal as an
|
|
199
|
-
// overlay dismissal. This handler is bound to the overlay div, so a keydown
|
|
200
|
-
// on any focusable descendant (the back button, a footer button, an input
|
|
201
|
-
// field) bubbles up and reaches it too -- the target check below is what
|
|
202
|
-
// actually stops that bubbled Enter/Space from being read as an activation
|
|
203
|
-
// of the overlay itself, mirroring the guard handleOverlayClick already has
|
|
204
|
-
// for the click case.
|
|
205
|
-
const handleOverlayKeyDown = (event: KeyboardEvent): void => {
|
|
206
|
-
if (
|
|
207
|
-
(event.key === 'Enter' || event.key === ' ') &&
|
|
208
|
-
overlayDismissible &&
|
|
209
|
-
event.target === overlayDiv
|
|
210
|
-
) {
|
|
211
|
-
event.preventDefault();
|
|
212
|
-
debounce(() => {
|
|
213
|
-
onoverlayclick?.();
|
|
214
|
-
ondismiss?.();
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
185
|
// Fix [major]: role=button image divs need Enter/Space handlers for WCAG 2.1 SC 2.1.1.
|
|
220
186
|
// Keyboard activation (Enter/Space) invokes the same prop callbacks as click.
|
|
221
187
|
const handleLeftImageKeyDown = (event: KeyboardEvent): void => {
|
|
@@ -307,7 +273,8 @@
|
|
|
307
273
|
|
|
308
274
|
{#if typeof content === 'function'}
|
|
309
275
|
<OverlayAnimation fadeIn={overlayFadeIn}>
|
|
310
|
-
<!--
|
|
276
|
+
<!-- Native backdrop activation bubbles here; only the backdrop target dismisses. -->
|
|
277
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
311
278
|
<div
|
|
312
279
|
bind:this={overlayDiv}
|
|
313
280
|
use:portalAction={{ usePortal }}
|
|
@@ -316,13 +283,18 @@
|
|
|
316
283
|
? `--modal-overlay-backdrop-filter: ${overlayBackdropFilter};`
|
|
317
284
|
: null}
|
|
318
285
|
onclick={handleOverlayClick}
|
|
319
|
-
onkeydown={handleOverlayKeyDown}
|
|
320
|
-
role={overlayDismissible ? 'button' : null}
|
|
321
|
-
tabindex={overlayDismissible ? 0 : null}
|
|
322
|
-
aria-label={overlayAriaLabel ?? null}
|
|
323
286
|
data-pw={testId}
|
|
324
287
|
testID={testId}
|
|
325
288
|
>
|
|
289
|
+
{#if overlayDismissible}
|
|
290
|
+
<button
|
|
291
|
+
bind:this={overlayButton}
|
|
292
|
+
type="button"
|
|
293
|
+
class="modal-overlay-action"
|
|
294
|
+
aria-label={overlayAriaLabel ?? 'Dismiss modal'}
|
|
295
|
+
tabindex={role === 'dialog' || role === 'alertdialog' ? -1 : 0}
|
|
296
|
+
></button>
|
|
297
|
+
{/if}
|
|
326
298
|
<ModalAnimation enable={enableTransition} {align} {transitionType} {entryAnimation}>
|
|
327
299
|
<div
|
|
328
300
|
bind:this={modalContent}
|
|
@@ -437,6 +409,21 @@
|
|
|
437
409
|
margin: var(--modal-margin);
|
|
438
410
|
}
|
|
439
411
|
|
|
412
|
+
.modal-overlay-action {
|
|
413
|
+
position: absolute;
|
|
414
|
+
inset: 0;
|
|
415
|
+
z-index: -1;
|
|
416
|
+
padding: 0;
|
|
417
|
+
border: 0;
|
|
418
|
+
background: transparent;
|
|
419
|
+
cursor: pointer;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.modal-overlay-action:focus-visible {
|
|
423
|
+
outline: var(--modal-overlay-focus-outline, 2px solid #2563eb);
|
|
424
|
+
outline-offset: var(--modal-overlay-focus-outline-offset, -2px);
|
|
425
|
+
}
|
|
426
|
+
|
|
440
427
|
.overlay-active {
|
|
441
428
|
/* Prefer the modal-specific token; fall back to the legacy generic
|
|
442
429
|
--background-color (kept for backward compatibility), then the default.
|
|
@@ -58,12 +58,13 @@ export type OptionalModalProperties = {
|
|
|
58
58
|
/** Accessible name for the left header image's role="button" wrapper (e.g. a back control). Rendered as aria-label. */
|
|
59
59
|
leftImageAriaLabel?: string;
|
|
60
60
|
/**
|
|
61
|
-
* Accessible name for the
|
|
61
|
+
* Accessible name for the independent native backdrop button, rendered as
|
|
62
62
|
* `aria-label` -- same pattern as `leftImageAriaLabel` and
|
|
63
63
|
* `header.buttonAriaLabel`. Only meaningful when the overlay is dismissible
|
|
64
64
|
* (an `onoverlayclick` or `ondismiss` handler is supplied), which is the
|
|
65
|
-
* only state where
|
|
66
|
-
*
|
|
65
|
+
* only state where a backdrop action exists. Named dialogs keep that action
|
|
66
|
+
* outside their Tab cycle; generic panels retain Enter/Space activation.
|
|
67
|
+
* Default: "Dismiss modal".
|
|
67
68
|
*/
|
|
68
69
|
overlayAriaLabel?: string;
|
|
69
70
|
testId?: string;
|
|
@@ -107,7 +108,7 @@ export type OptionalModalProperties = {
|
|
|
107
108
|
* ARIA role for the modal content panel. Default: unset -- the panel
|
|
108
109
|
* renders no `role` and no `aria-modal` at all, byte-for-byte matching
|
|
109
110
|
* behavior prior to this prop's introduction, so an existing consumer who
|
|
110
|
-
* never touches `role` sees no
|
|
111
|
+
* never touches `role` sees no panel role/aria-modal change. Set it explicitly (e.g.
|
|
111
112
|
* `'dialog'` or `'alertdialog'`) to opt in: the panel then also carries
|
|
112
113
|
* `aria-modal="true"`, since a role without aria-modal is not a complete
|
|
113
114
|
* dialog announcement.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
testId,
|
|
10
10
|
onchange,
|
|
11
11
|
classes,
|
|
12
|
+
ariaLabel = 'Pagination',
|
|
12
13
|
hasMore = false,
|
|
13
14
|
prevButtonTestId,
|
|
14
15
|
nextButtonTestId,
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
<nav
|
|
70
71
|
class="pagination {classes ?? ''}"
|
|
71
72
|
class:disabled
|
|
73
|
+
aria-label={ariaLabel}
|
|
72
74
|
data-pw={typeof testId === 'string' && testId.length > 0 ? testId : null}
|
|
73
75
|
testID={typeof testId === 'string' && testId.length > 0 ? testId : null}
|
|
74
76
|
>
|
|
@@ -8,6 +8,8 @@ export type OptionalPaginationProperties = {
|
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
testId?: string;
|
|
10
10
|
classes?: string;
|
|
11
|
+
/** Name of the page-navigation landmark. Default: `Pagination`. */
|
|
12
|
+
ariaLabel?: string;
|
|
11
13
|
/**
|
|
12
14
|
* Cursor-pagination hint. When `true`, the next button stays enabled even
|
|
13
15
|
* when `currentPage >= totalPages` — use this when the total page count is
|