@juspay/svelte-ui-components 4.50.0 → 4.52.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/Animations/tokenizedFly.js +9 -2
- package/dist/AreaChart/AreaChart.svelte +41 -13
- package/dist/AreaChart/properties.d.ts +12 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte +7 -6
- package/dist/BarChart/BarChart.svelte +54 -6
- package/dist/BarChart/properties.d.ts +12 -0
- package/dist/Calendar/Calendar.svelte +124 -51
- package/dist/Card/Card.svelte +5 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -0
- package/dist/CheckListItem/CheckListItem.svelte +27 -5
- package/dist/ColorPicker/ColorPicker.svelte +40 -9
- package/dist/ColorPicker/properties.d.ts +8 -0
- package/dist/Combobox/Combobox.svelte +34 -2
- package/dist/CommandMenu/CommandMenu.svelte +35 -8
- package/dist/CommandMenu/properties.d.ts +6 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +5 -5
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +41 -7
- package/dist/DualAxisBarChart/properties.d.ts +4 -0
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +104 -10
- package/dist/FileDropzoneTrigger/properties.d.ts +14 -4
- package/dist/FileInput/FileInput.svelte +38 -9
- package/dist/FileInput/context.d.ts +20 -0
- package/dist/FileInput/context.js +8 -0
- package/dist/FileInput/properties.d.ts +25 -1
- package/dist/FunnelChart/FunnelChart.svelte +20 -1
- package/dist/FunnelChart/properties.d.ts +4 -0
- package/dist/Gallery/Gallery.svelte +72 -27
- package/dist/HITL/HITL.svelte +12 -3
- package/dist/LineChart/LineChart.svelte +105 -33
- package/dist/LineChart/properties.d.ts +12 -0
- package/dist/MediaPlayer/MediaPlayer.svelte +172 -2
- package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
- package/dist/MediaPlayer/properties.d.ts +31 -0
- package/dist/Menu/Menu.svelte +6 -4
- package/dist/Modal/Modal.svelte +15 -12
- package/dist/Modal/properties.d.ts +29 -1
- package/dist/Pagination/Pagination.svelte +1 -0
- package/dist/PieChart/PieChart.svelte +51 -29
- package/dist/PieChart/properties.d.ts +4 -0
- package/dist/Pill/Pill.svelte +5 -0
- package/dist/Radio/Radio.svelte +11 -2
- package/dist/Radio/properties.d.ts +7 -2
- package/dist/SankeyChart/SankeyChart.svelte +339 -211
- package/dist/SankeyChart/properties.d.ts +4 -0
- package/dist/Scroller/Scroller.svelte +247 -4
- package/dist/Scroller/properties.d.ts +8 -0
- package/dist/Select/Select.svelte +43 -5
- package/dist/Select/properties.d.ts +17 -1
- package/dist/Sheet/Sheet.svelte +31 -4
- package/dist/Sheet/properties.d.ts +6 -0
- package/dist/SplitInput/SplitInput.svelte +17 -0
- package/dist/SplitInput/field-name.d.ts +29 -0
- package/dist/SplitInput/field-name.js +50 -0
- package/dist/SplitInput/properties.d.ts +18 -4
- package/dist/Status/Status.svelte +24 -12
- package/dist/Status/properties.d.ts +11 -0
- package/dist/Table/BuiltinCell.svelte +13 -5
- package/dist/Table/BuiltinCell.svelte.d.ts +1 -0
- package/dist/Table/Table.svelte +78 -20
- package/dist/Table/a11y.d.ts +7 -0
- package/dist/Table/a11y.js +71 -0
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +13 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +48 -9
- package/dist/Toast/Toast.svelte +40 -4
- package/dist/Toggle/Toggle.svelte +2 -0
- package/dist/Toggle/properties.d.ts +2 -0
- package/dist/_chart/Axis.svelte +10 -0
- package/dist/_chart/ChartContainer.svelte +32 -1
- package/dist/_chart/ChartTooltip.svelte +18 -3
- package/dist/_chart/LabelPlate.svelte +114 -0
- package/dist/_chart/LabelPlate.svelte.d.ts +7 -0
- package/dist/_chart/a11y.d.ts +29 -0
- package/dist/_chart/a11y.js +44 -0
- package/dist/_chart/measure.d.ts +2 -0
- package/dist/_chart/measure.js +4 -0
- package/dist/_chart/tooltipPosition.d.ts +6 -0
- package/dist/_chart/tooltipPosition.js +30 -2
- package/dist/_chart/types.d.ts +22 -0
- package/dist/_interaction/focusable.d.ts +47 -0
- package/dist/_interaction/focusable.js +139 -0
- package/dist/assets/captions-on.svg +3 -0
- package/dist/assets/captions.svg +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/theme-dark.css +6 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +18 -2
- package/dist/utils.js +73 -28
- package/dist-codemod/codemod/migrate-audit.js +9 -7
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +9 -1
- package/dist-codemod/codemod/migration-assets-layout.js +61 -0
- package/dist-wc/index.js +5653 -4148
- package/package.json +4 -2
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
classes
|
|
13
13
|
}: CheckListItemProperties = $props();
|
|
14
14
|
|
|
15
|
+
/* The box (`role="checkbox"`) is what assistive technology reads, and the label the row shows is a
|
|
16
|
+
sibling of it, not inside <Checkbox> -- so nothing tied the two together and every item exposed an
|
|
17
|
+
unnamed control. `aria-labelledby` points the box at the label wrapper below, so the announced name
|
|
18
|
+
is exactly what is on screen: the plain `text`, or everything a `checkboxLabel` snippet renders
|
|
19
|
+
("Required business details Required"), which `text` alone would not carry. The id lives in the
|
|
20
|
+
component's own tree, so it resolves inside the shadow root `<sui-check-list-item>` renders into. */
|
|
21
|
+
const uid = $props.id();
|
|
22
|
+
const labelId = `check-list-item-label-${uid}`;
|
|
23
|
+
const checkboxAttributes = { 'aria-labelledby': labelId };
|
|
24
|
+
|
|
15
25
|
function handleClick(value: boolean): void {
|
|
16
26
|
checked = value;
|
|
17
27
|
onclick?.(checked);
|
|
@@ -20,19 +30,25 @@
|
|
|
20
30
|
|
|
21
31
|
<div class="container {classes ?? ''}" class:disabled data-pw={testId} testID={testId}>
|
|
22
32
|
<div class="checkbox-wrapper">
|
|
33
|
+
<!-- `ariaLabel` is the fallback name: it applies only if the label wrapper resolves to nothing
|
|
34
|
+
(an empty snippet), because `aria-labelledby` outranks it whenever it yields text. -->
|
|
23
35
|
<Checkbox
|
|
24
36
|
text=""
|
|
37
|
+
ariaLabel={text}
|
|
38
|
+
attributes={checkboxAttributes}
|
|
25
39
|
bind:checked
|
|
26
40
|
{disabled}
|
|
27
41
|
onclick={handleClick}
|
|
28
42
|
{...typeof testId === 'string' ? { testId: `${testId}-checkbox` } : {}}
|
|
29
43
|
/>
|
|
30
44
|
</div>
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
<span class="check-list-item-label" id={labelId}>
|
|
46
|
+
{#if typeof checkboxLabel === 'function'}
|
|
47
|
+
{@render checkboxLabel()}
|
|
48
|
+
{:else}
|
|
49
|
+
<span class="text" class:checked>{text}</span>
|
|
50
|
+
{/if}
|
|
51
|
+
</span>
|
|
36
52
|
</div>
|
|
37
53
|
|
|
38
54
|
<style>
|
|
@@ -52,6 +68,12 @@
|
|
|
52
68
|
flex-shrink: 0;
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
/* Names the checkbox but adds no box of its own: the text or snippet inside stays a direct flex item
|
|
72
|
+
of .container, so layout, gap and every --check-list-item-* token behave as before. */
|
|
73
|
+
.check-list-item-label {
|
|
74
|
+
display: contents;
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
.container.disabled .checkbox-wrapper {
|
|
56
78
|
--checkbox-disabled-opacity: var(--check-list-item-disabled-opacity, 0.4);
|
|
57
79
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
let {
|
|
22
22
|
value = $bindable('#000000'),
|
|
23
23
|
label,
|
|
24
|
+
ariaLabel,
|
|
24
25
|
disabled = false,
|
|
25
26
|
showValue = false,
|
|
26
27
|
testId,
|
|
@@ -50,6 +51,25 @@
|
|
|
50
51
|
);
|
|
51
52
|
const describedGroup = $derived(field.describedBy !== null || field.ariaInvalid !== null);
|
|
52
53
|
|
|
54
|
+
/* What this picker is called: its visible `label`, else the `ariaLabel` a caller
|
|
55
|
+
gives a swatch-only picker that has no caption. Every control in it is named
|
|
56
|
+
from this, because a page with three pickers otherwise offers three identical
|
|
57
|
+
"Pick a color" buttons and three nameless hex fields -- indistinguishable in a
|
|
58
|
+
screen reader's list of controls. `null` rather than a made-up default: with
|
|
59
|
+
no name supplied the controls keep the plain purpose names below, which is
|
|
60
|
+
the most that can honestly be said. */
|
|
61
|
+
const pickerName = $derived(
|
|
62
|
+
typeof label === 'string' && label.trim() !== ''
|
|
63
|
+
? label.trim()
|
|
64
|
+
: typeof ariaLabel === 'string' && ariaLabel.trim() !== ''
|
|
65
|
+
? ariaLabel.trim()
|
|
66
|
+
: null
|
|
67
|
+
);
|
|
68
|
+
const triggerName = $derived(
|
|
69
|
+
pickerName === null ? 'Pick a color' : `Pick a color: ${pickerName}`
|
|
70
|
+
);
|
|
71
|
+
const dialogName = $derived(pickerName === null ? 'Color picker' : `${pickerName} picker`);
|
|
72
|
+
|
|
53
73
|
// ── Internal state ──────────────────────────────────────────────
|
|
54
74
|
|
|
55
75
|
let open = $state(false);
|
|
@@ -293,7 +313,7 @@
|
|
|
293
313
|
class="color-picker-container {classes ?? ''}"
|
|
294
314
|
class:disabled
|
|
295
315
|
role={describedGroup ? 'group' : null}
|
|
296
|
-
aria-label={describedGroup
|
|
316
|
+
aria-label={describedGroup ? pickerName : null}
|
|
297
317
|
aria-describedby={field.describedBy}
|
|
298
318
|
aria-invalid={field.ariaInvalid}
|
|
299
319
|
data-pw={testId}
|
|
@@ -312,7 +332,7 @@
|
|
|
312
332
|
class:standalone={showValue === false}
|
|
313
333
|
bind:this={triggerWrapEl}
|
|
314
334
|
>
|
|
315
|
-
<Button onclick={togglePicker} {disabled} ariaLabel=
|
|
335
|
+
<Button onclick={togglePicker} {disabled} ariaLabel={triggerName} ariaExpanded={open}>
|
|
316
336
|
<span class="color-picker-checkerboard">
|
|
317
337
|
<span class="color-picker-swatch" style="background-color: {value};"></span>
|
|
318
338
|
</span>
|
|
@@ -322,6 +342,7 @@
|
|
|
322
342
|
<div class="color-picker-input-wrap">
|
|
323
343
|
<Input
|
|
324
344
|
bind:value
|
|
345
|
+
ariaLabel={`${pickerName ?? 'Color'} hex value`}
|
|
325
346
|
disable={disabled}
|
|
326
347
|
maxLength={7}
|
|
327
348
|
classes="color-picker-text-input"
|
|
@@ -333,7 +354,7 @@
|
|
|
333
354
|
|
|
334
355
|
<!-- Popover panel -->
|
|
335
356
|
{#if open}
|
|
336
|
-
<div class="color-picker-popover" role="dialog" aria-label=
|
|
357
|
+
<div class="color-picker-popover" role="dialog" aria-label={dialogName} use:dismissalAction>
|
|
337
358
|
<div
|
|
338
359
|
class="cp-sat-panel"
|
|
339
360
|
style="background-color: {hsvToHex(hue, 1, 1)};"
|
|
@@ -361,6 +382,7 @@
|
|
|
361
382
|
min={0}
|
|
362
383
|
max={360}
|
|
363
384
|
step={1}
|
|
385
|
+
ariaLabel="Hue"
|
|
364
386
|
{disabled}
|
|
365
387
|
classes="cp-hue-track"
|
|
366
388
|
oninput={handleHueInput}
|
|
@@ -375,6 +397,7 @@
|
|
|
375
397
|
<div class="cp-field-hex">
|
|
376
398
|
<Input
|
|
377
399
|
value={hexInputValue}
|
|
400
|
+
ariaLabel="Hex value"
|
|
378
401
|
maxLength={7}
|
|
379
402
|
classes="cp-field-input"
|
|
380
403
|
oninput={handleHexFieldInput}
|
|
@@ -384,10 +407,11 @@
|
|
|
384
407
|
{:else if mode === 'RGB'}
|
|
385
408
|
<SplitInput
|
|
386
409
|
values={[String(currentRgb.r), String(currentRgb.g), String(currentRgb.b)]}
|
|
410
|
+
ariaLabel="RGB channels"
|
|
387
411
|
fields={[
|
|
388
|
-
{ label: 'R', dataType: 'number', min: 0, max: 255 },
|
|
389
|
-
{ label: 'G', dataType: 'number', min: 0, max: 255 },
|
|
390
|
-
{ label: 'B', dataType: 'number', min: 0, max: 255 }
|
|
412
|
+
{ label: 'R', ariaLabel: 'Red', dataType: 'number', min: 0, max: 255 },
|
|
413
|
+
{ label: 'G', ariaLabel: 'Green', dataType: 'number', min: 0, max: 255 },
|
|
414
|
+
{ label: 'B', ariaLabel: 'Blue', dataType: 'number', min: 0, max: 255 }
|
|
391
415
|
]}
|
|
392
416
|
{disabled}
|
|
393
417
|
classes="cp-split-input"
|
|
@@ -396,10 +420,17 @@
|
|
|
396
420
|
{:else}
|
|
397
421
|
<SplitInput
|
|
398
422
|
values={[String(currentHsl.h), String(currentHsl.s), String(currentHsl.l)]}
|
|
423
|
+
ariaLabel="HSL channels"
|
|
399
424
|
fields={[
|
|
400
|
-
{ label: 'H', dataType: 'number', min: 0, max: 360 },
|
|
401
|
-
{
|
|
402
|
-
|
|
425
|
+
{ label: 'H', ariaLabel: 'Hue (degrees)', dataType: 'number', min: 0, max: 360 },
|
|
426
|
+
{
|
|
427
|
+
label: 'S',
|
|
428
|
+
ariaLabel: 'Saturation (percent)',
|
|
429
|
+
dataType: 'number',
|
|
430
|
+
min: 0,
|
|
431
|
+
max: 100
|
|
432
|
+
},
|
|
433
|
+
{ label: 'L', ariaLabel: 'Lightness (percent)', dataType: 'number', min: 0, max: 100 }
|
|
403
434
|
]}
|
|
404
435
|
{disabled}
|
|
405
436
|
classes="cp-split-input"
|
|
@@ -4,6 +4,14 @@ export type MandatoryColorPickerProperties = {
|
|
|
4
4
|
};
|
|
5
5
|
export type OptionalColorPickerProperties = {
|
|
6
6
|
label?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Names the picker when it has no visible `label` -- a swatch-only picker, say.
|
|
9
|
+
* Every control inside is named from `label` if there is one, else this ("Brand
|
|
10
|
+
* color hex value", "Pick a color: Brand color"), so several pickers on a page
|
|
11
|
+
* can be told apart. Without either, the controls keep their purpose names
|
|
12
|
+
* ("Hex value", "Red") and the trigger stays "Pick a color".
|
|
13
|
+
*/
|
|
14
|
+
ariaLabel?: string;
|
|
7
15
|
disabled?: boolean;
|
|
8
16
|
showValue?: boolean;
|
|
9
17
|
testId?: string;
|
|
@@ -78,6 +78,26 @@
|
|
|
78
78
|
);
|
|
79
79
|
const describedGroup = $derived(field.describedBy !== null || field.ariaInvalid !== null);
|
|
80
80
|
|
|
81
|
+
/* What names the text field. The input is where a screen reader lands, but the
|
|
82
|
+
name used to stop at the listbox: `ariaLabel` was applied to the popup only,
|
|
83
|
+
so the combobox itself announced as "combobox, blank" (or as its placeholder,
|
|
84
|
+
which is not a name and disappears from a multi-select once a pill is picked).
|
|
85
|
+
|
|
86
|
+
`inputProperties.ariaLabel` is the most specific and wins, as it always could;
|
|
87
|
+
then `ariaLabel`; then `inputProperties.label`, which docs/Combobox.md has long
|
|
88
|
+
said names the field but which Input never renders here (`actionInput`
|
|
89
|
+
suppresses its <label>), so it is used as the name rather than dropped. The
|
|
90
|
+
placeholder is the last resort so a consumer who named nothing still has a name
|
|
91
|
+
that does not vanish when the placeholder does. */
|
|
92
|
+
const accessibleName = (text?: string | null): string | null =>
|
|
93
|
+
typeof text === 'string' && text.trim() !== '' ? text : null;
|
|
94
|
+
const inputName = $derived(
|
|
95
|
+
accessibleName(inputProperties?.ariaLabel) ??
|
|
96
|
+
accessibleName(ariaLabel) ??
|
|
97
|
+
accessibleName(inputProperties?.label) ??
|
|
98
|
+
accessibleName(placeholder)
|
|
99
|
+
);
|
|
100
|
+
|
|
81
101
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
82
102
|
let inputRef: ReturnType<typeof Input> | null = $state(null);
|
|
83
103
|
|
|
@@ -368,6 +388,7 @@
|
|
|
368
388
|
class:disabled
|
|
369
389
|
bind:this={containerEl}
|
|
370
390
|
role={describedGroup ? 'group' : null}
|
|
391
|
+
aria-label={describedGroup ? inputName : null}
|
|
371
392
|
aria-describedby={field.describedBy}
|
|
372
393
|
aria-invalid={field.ariaInvalid}
|
|
373
394
|
data-pw={testId}
|
|
@@ -383,7 +404,13 @@
|
|
|
383
404
|
{#if typeof pillSnippet === 'function'}
|
|
384
405
|
{@render pillSnippet(id, () => !disabled && removeValue(id), disabled)}
|
|
385
406
|
{:else}
|
|
386
|
-
<Pill
|
|
407
|
+
<Pill
|
|
408
|
+
text={labelOf(id)}
|
|
409
|
+
dismissible
|
|
410
|
+
dismissLabel={`Remove ${labelOf(id)}${inputName ? ` from ${inputName}` : ''}`}
|
|
411
|
+
{disabled}
|
|
412
|
+
ondismiss={() => removeValue(id)}
|
|
413
|
+
/>
|
|
387
414
|
{/if}
|
|
388
415
|
{/each}
|
|
389
416
|
{/if}
|
|
@@ -393,6 +420,7 @@
|
|
|
393
420
|
bind:value={inputValue}
|
|
394
421
|
bind:this={inputRef}
|
|
395
422
|
placeholder={multiple && selected.length > 0 ? '' : placeholder}
|
|
423
|
+
ariaLabel={inputName}
|
|
396
424
|
{name}
|
|
397
425
|
disable={disabled}
|
|
398
426
|
autoComplete="off"
|
|
@@ -415,11 +443,15 @@
|
|
|
415
443
|
</div>
|
|
416
444
|
|
|
417
445
|
{#if open && !disabled}
|
|
446
|
+
<!-- tabindex="-1": Firefox gives keyboard focus to a scroll container that holds no
|
|
447
|
+
tabbable child, and these options are all tabindex="-1". Tab from the open input
|
|
448
|
+
then landed on the listbox, and the focus was lost when the Tab handler closed it. -->
|
|
418
449
|
<div
|
|
419
450
|
class="combobox-dropdown"
|
|
420
451
|
role="listbox"
|
|
421
452
|
id={listboxId}
|
|
422
|
-
aria-label={ariaLabel}
|
|
453
|
+
aria-label={ariaLabel ?? inputName}
|
|
454
|
+
tabindex="-1"
|
|
423
455
|
use:dismissalAction
|
|
424
456
|
>
|
|
425
457
|
{#if typeof dropdownHeader === 'function'}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { CommandMenuProperties, CommandItem } from './properties';
|
|
3
|
-
import { tick, onMount, onDestroy } from 'svelte';
|
|
4
|
-
import {
|
|
3
|
+
import { tick, onMount, onDestroy, untrack } from 'svelte';
|
|
4
|
+
import { toStore } from 'svelte/store';
|
|
5
|
+
import { acquireScrollLock, resolveScrollContainer } from '../utils';
|
|
5
6
|
import { registerDismissible } from '../_interaction/dismissal';
|
|
6
7
|
import { getActiveElement } from '../_interaction/focus';
|
|
7
8
|
import { tokenizedFly } from '../Animations/tokenizedFly';
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
let {
|
|
17
18
|
items,
|
|
18
19
|
open = $bindable(false),
|
|
20
|
+
scrollContainer,
|
|
19
21
|
query = $bindable(''),
|
|
20
22
|
shortcutEnabled = true,
|
|
21
23
|
placeholder = 'Search commands...',
|
|
@@ -289,10 +291,10 @@
|
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// onDestroy
|
|
295
|
-
//
|
|
294
|
+
// The action owns the scroll hold until its node goes away; the logical-open
|
|
295
|
+
// observer below replaces that hold if an outro is reversed with a new owner.
|
|
296
|
+
// onDestroy must NOT release it again: a second decrement could release a
|
|
297
|
+
// hold another open surface still needs.
|
|
296
298
|
//
|
|
297
299
|
// The dismissible layer is registered here too, for the same reason: this
|
|
298
300
|
// action's mount/destroy already brackets exactly the open span (the overlay
|
|
@@ -321,12 +323,35 @@
|
|
|
321
323
|
// input logic below it. Deferred rather than shipped half-verified: a wrong
|
|
322
324
|
// guess here risks a worse regression (stealing focus into the wrong
|
|
323
325
|
// element on every open) than the narrow bug it would fix.
|
|
326
|
+
let scrollLockNode: HTMLElement | null = null;
|
|
327
|
+
let releaseScrollLock: (() => void) | null = null;
|
|
328
|
+
let scrollLockSpanEnded = false;
|
|
329
|
+
|
|
330
|
+
onMount(() =>
|
|
331
|
+
toStore(() => open).subscribe((isOpen) => {
|
|
332
|
+
if (!isOpen) {
|
|
333
|
+
scrollLockSpanEnded = true;
|
|
334
|
+
} else if (scrollLockSpanEnded && scrollLockNode !== null) {
|
|
335
|
+
// A same-outro reopen keeps the node/action; it still starts a new
|
|
336
|
+
// scroll-owner span. Changes within an open remain intentionally frozen.
|
|
337
|
+
scrollLockSpanEnded = false;
|
|
338
|
+
const releaseNext = untrack(() =>
|
|
339
|
+
acquireScrollLock(resolveScrollContainer(scrollLockNode, scrollContainer))
|
|
340
|
+
);
|
|
341
|
+
releaseScrollLock?.();
|
|
342
|
+
releaseScrollLock = releaseNext;
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
|
|
324
347
|
function scrollLockAction(node: HTMLElement) {
|
|
325
348
|
const opener = getActiveElement(node);
|
|
326
349
|
if (openerElement === null && opener instanceof HTMLElement) {
|
|
327
350
|
openerElement = opener;
|
|
328
351
|
}
|
|
329
|
-
|
|
352
|
+
scrollLockNode = node;
|
|
353
|
+
scrollLockSpanEnded = false;
|
|
354
|
+
releaseScrollLock = acquireScrollLock(resolveScrollContainer(node, scrollContainer));
|
|
330
355
|
const releaseDismissible = registerDismissible({
|
|
331
356
|
element: () => dialogElement,
|
|
332
357
|
onEscape: close
|
|
@@ -339,7 +364,9 @@
|
|
|
339
364
|
return {
|
|
340
365
|
destroy() {
|
|
341
366
|
releaseDismissible();
|
|
342
|
-
|
|
367
|
+
releaseScrollLock?.();
|
|
368
|
+
releaseScrollLock = null;
|
|
369
|
+
scrollLockNode = null;
|
|
343
370
|
// Opt-in only (clearStateOnClose): a controlled parent can set
|
|
344
371
|
// `open = false` without calling close(), and the overlay action still
|
|
345
372
|
// unmounts on every close path either way -- this is the lifecycle
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ScrollContainer } from '../types';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
export type CommandItem = {
|
|
3
4
|
label: string;
|
|
@@ -12,6 +13,11 @@ export type MandatoryCommandMenuProperties = {
|
|
|
12
13
|
items: CommandItem[];
|
|
13
14
|
};
|
|
14
15
|
export type OptionalCommandMenuProperties = {
|
|
16
|
+
/** Scroll owner, resolved once per open span. Omit for the overlay's ownerDocument.body.
|
|
17
|
+
* Supply the embedded scroll element or a getter; explicit null means no lock.
|
|
18
|
+
* Changing ownership while open takes effect on the next open, and teardown
|
|
19
|
+
* always releases the originally acquired container. */
|
|
20
|
+
scrollContainer?: ScrollContainer;
|
|
15
21
|
open?: boolean;
|
|
16
22
|
/** Bindable search text. Omit to keep the built-in uncontrolled search. */
|
|
17
23
|
query?: string;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { focusTrapTabTarget, getFocusTrapBoundary } from '../_interaction/focus';
|
|
12
12
|
import { getActiveElement } from '../_interaction/focus';
|
|
13
13
|
import { registerDismissible } from '../_interaction/dismissal';
|
|
14
|
+
import { findInteractive } from '../_interaction/focusable';
|
|
14
15
|
import { describeField } from '../_field/description';
|
|
15
16
|
import chevronDownSvg from '../assets/chevron-down.svg?raw';
|
|
16
17
|
import checkmarkSvg from '../assets/checkmark.svg?raw';
|
|
@@ -339,9 +340,9 @@
|
|
|
339
340
|
}
|
|
340
341
|
|
|
341
342
|
function openPicker(): void {
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
panelFocusReturnEl =
|
|
343
|
+
// Pointer activation need not focus a button in WebKit. Return to the
|
|
344
|
+
// invoking control, not an unrelated previously focused field or body.
|
|
345
|
+
panelFocusReturnEl = triggerRef ? findInteractive(triggerRef) : null;
|
|
345
346
|
// Seed draft from current committed values
|
|
346
347
|
draftStart = rangeStart;
|
|
347
348
|
draftEnd = rangeEnd;
|
|
@@ -506,8 +507,7 @@
|
|
|
506
507
|
}
|
|
507
508
|
|
|
508
509
|
function openComparePicker(): void {
|
|
509
|
-
|
|
510
|
-
compareFocusReturnEl = compareOpener instanceof HTMLElement ? compareOpener : null;
|
|
510
|
+
compareFocusReturnEl = compareTriggerRef ? findInteractive(compareTriggerRef) : null;
|
|
511
511
|
draftCompareStart = compareStart ?? null;
|
|
512
512
|
draftCompareEnd = compareEnd ?? null;
|
|
513
513
|
openCompare = true;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
DualAxisTooltipContext
|
|
6
6
|
} from './properties';
|
|
7
7
|
import ChartContainer from '../_chart/ChartContainer.svelte';
|
|
8
|
+
import { defaultChartName, resolveChartName } from '../_chart/a11y';
|
|
8
9
|
import Axis from '../_chart/Axis.svelte';
|
|
9
10
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
10
11
|
import Legend from '../_chart/Legend.svelte';
|
|
@@ -32,7 +33,8 @@
|
|
|
32
33
|
import { SvelteSet } from 'svelte/reactivity';
|
|
33
34
|
|
|
34
35
|
// ── Per-instance uid for SVG <defs> ids ────────────────────────
|
|
35
|
-
const uid =
|
|
36
|
+
const uid = $props.id();
|
|
37
|
+
const emptyDescriptionId = `dual-axis-empty-desc-${uid}`;
|
|
36
38
|
|
|
37
39
|
// ── Props ──────────────────────────────────────────────────────
|
|
38
40
|
|
|
@@ -55,6 +57,8 @@
|
|
|
55
57
|
hideLegendBelow = 360,
|
|
56
58
|
tooltipSnippet,
|
|
57
59
|
onbarclick,
|
|
60
|
+
ariaLabel,
|
|
61
|
+
ariaDescription,
|
|
58
62
|
testId,
|
|
59
63
|
classes
|
|
60
64
|
}: DualAxisBarChartProperties = $props();
|
|
@@ -378,8 +382,20 @@
|
|
|
378
382
|
*/
|
|
379
383
|
type HoverRect = { x: number; width: number; catIdx: number };
|
|
380
384
|
|
|
385
|
+
const chartName = $derived(
|
|
386
|
+
resolveChartName(
|
|
387
|
+
ariaLabel,
|
|
388
|
+
defaultChartName('dual-axis', { seriesNames: series.map((s) => s.name) })
|
|
389
|
+
)
|
|
390
|
+
);
|
|
391
|
+
const chartDescription = $derived(
|
|
392
|
+
typeof ariaDescription === 'string' && ariaDescription.trim() !== '' ? ariaDescription : null
|
|
393
|
+
);
|
|
394
|
+
const hasMarks = $derived(
|
|
395
|
+
bars.length > 0 || lineSeriesData.some((line) => line.points.length > 0)
|
|
396
|
+
);
|
|
381
397
|
const hoverRects: HoverRect[] = $derived(
|
|
382
|
-
categories.map((_, catIdx) => ({
|
|
398
|
+
(hasMarks ? categories : []).map((_, catIdx) => ({
|
|
383
399
|
x: catScale(categories[catIdx]),
|
|
384
400
|
width: catScale.bandwidth,
|
|
385
401
|
catIdx
|
|
@@ -390,10 +406,13 @@
|
|
|
390
406
|
const parts = series
|
|
391
407
|
.map((s, si) => ({ s, si }))
|
|
392
408
|
.filter(({ si }) => !hiddenSeries.has(si))
|
|
393
|
-
.map(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
409
|
+
.map(({ s }) => {
|
|
410
|
+
const value = s.data[catIdx];
|
|
411
|
+
const text = Number.isFinite(value)
|
|
412
|
+
? (s.yAxisIndex === 0 ? leftFormat : rightFormat)(value)
|
|
413
|
+
: 'not available';
|
|
414
|
+
return `${s.name} ${text}`;
|
|
415
|
+
});
|
|
397
416
|
return `${categories[catIdx]}: ${parts.join(', ')}`;
|
|
398
417
|
}
|
|
399
418
|
|
|
@@ -474,6 +493,9 @@
|
|
|
474
493
|
{aspectRatio}
|
|
475
494
|
{maxHeight}
|
|
476
495
|
{minHeight}
|
|
496
|
+
ariaLabel={chartName}
|
|
497
|
+
{ariaDescription}
|
|
498
|
+
interactive={hasMarks}
|
|
477
499
|
>
|
|
478
500
|
<g transform="translate({dims.margin.left}, {dims.margin.top})">
|
|
479
501
|
<!-- Left Y-axis (index 0) -->
|
|
@@ -544,6 +566,7 @@
|
|
|
544
566
|
fill={bar.color}
|
|
545
567
|
aria-label="{categories[bar.categoryIndex]}: {bar.value}"
|
|
546
568
|
role="img"
|
|
569
|
+
aria-hidden="true"
|
|
547
570
|
/>
|
|
548
571
|
{/each}
|
|
549
572
|
|
|
@@ -570,6 +593,7 @@
|
|
|
570
593
|
style="stroke: var(--dual-axis-dot-stroke, light-dark(#fff, #111827)); stroke-width: var(--dual-axis-dot-stroke-width, 1.5);"
|
|
571
594
|
aria-label="{categories[ptIdx]}: {series[ls.seriesIndex]?.data[ptIdx] ?? 0}"
|
|
572
595
|
role="img"
|
|
596
|
+
aria-hidden="true"
|
|
573
597
|
/>
|
|
574
598
|
{/each}
|
|
575
599
|
{/each}
|
|
@@ -640,7 +664,17 @@
|
|
|
640
664
|
{/if}
|
|
641
665
|
</div>
|
|
642
666
|
{:else}
|
|
643
|
-
<div
|
|
667
|
+
<div
|
|
668
|
+
class="chart-empty"
|
|
669
|
+
role="img"
|
|
670
|
+
aria-label={chartName}
|
|
671
|
+
aria-describedby={chartDescription === null ? null : emptyDescriptionId}
|
|
672
|
+
>
|
|
673
|
+
No data available.
|
|
674
|
+
{#if chartDescription !== null}
|
|
675
|
+
<span id={emptyDescriptionId} hidden>{chartDescription}</span>
|
|
676
|
+
{/if}
|
|
677
|
+
</div>
|
|
644
678
|
{/if}
|
|
645
679
|
</div>
|
|
646
680
|
|
|
@@ -119,6 +119,10 @@ export type OptionalDualAxisBarChartProperties = {
|
|
|
119
119
|
*/
|
|
120
120
|
tooltipSnippet?: Snippet<[DualAxisTooltipContext]>;
|
|
121
121
|
/** Value set on `data-pw` for test targeting. */
|
|
122
|
+
/** Accessible name of the drawing; omit to derive one from the plotted data. */
|
|
123
|
+
ariaLabel?: string;
|
|
124
|
+
/** Longer chart description, resolved inside the drawing's own DOM/shadow root. */
|
|
125
|
+
ariaDescription?: string;
|
|
122
126
|
testId?: string;
|
|
123
127
|
/** Extra CSS class string on the root `<div>`. */
|
|
124
128
|
classes?: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Button from '../Button/Button.svelte';
|
|
3
3
|
import Img from '../Img/Img.svelte';
|
|
4
|
+
import { useFileInputOwner } from '../FileInput/context';
|
|
4
5
|
import type { FileDropzoneTriggerProperties } from './properties';
|
|
5
6
|
|
|
6
7
|
let {
|
|
@@ -12,8 +13,31 @@
|
|
|
12
13
|
testId,
|
|
13
14
|
classes
|
|
14
15
|
}: FileDropzoneTriggerProperties = $props();
|
|
16
|
+
|
|
17
|
+
/* An upload action gets one interactive control, never two. Inside a FileInput
|
|
18
|
+
the region is that control (one Tab stop, Enter/Space/click open the chooser),
|
|
19
|
+
so a native <button> in here would be a second stop announcing the same
|
|
20
|
+
action. The button is therefore rendered only when this trigger is the owner:
|
|
21
|
+
it was handed an `onclick` and nothing around it already owns activation.
|
|
22
|
+
Otherwise it is the same surface as plain content. */
|
|
23
|
+
const owner = useFileInputOwner();
|
|
24
|
+
const ownsActivation = $derived(
|
|
25
|
+
typeof onclick === 'function' && owner?.regionOwnsActivation !== true
|
|
26
|
+
);
|
|
15
27
|
</script>
|
|
16
28
|
|
|
29
|
+
{#snippet body()}
|
|
30
|
+
<div class="file-dropzone-trigger-icon" aria-hidden="true">
|
|
31
|
+
<Img inlineSvg src={icon} alt="" fallback="" classes="file-dropzone-trigger-icon-img" />
|
|
32
|
+
</div>
|
|
33
|
+
<p class="file-dropzone-trigger-heading-wrap">
|
|
34
|
+
<span class="file-dropzone-trigger-heading">{heading}</span>
|
|
35
|
+
</p>
|
|
36
|
+
{#if caption}
|
|
37
|
+
<p class="file-dropzone-trigger-caption">{caption}</p>
|
|
38
|
+
{/if}
|
|
39
|
+
{/snippet}
|
|
40
|
+
|
|
17
41
|
{#if compact}
|
|
18
42
|
<div class="file-dropzone-trigger-compact {classes ?? ''}">
|
|
19
43
|
<div class="file-dropzone-trigger-icon-sm" aria-hidden="true">
|
|
@@ -21,22 +45,22 @@
|
|
|
21
45
|
</div>
|
|
22
46
|
<span class="file-dropzone-trigger-heading" data-pw={testId} testID={testId}>{heading}</span>
|
|
23
47
|
</div>
|
|
24
|
-
{:else}
|
|
48
|
+
{:else if ownsActivation}
|
|
25
49
|
<Button
|
|
26
50
|
classes={`file-dropzone-trigger-button ${classes ?? ''}`}
|
|
27
51
|
{onclick}
|
|
28
52
|
{...typeof testId === 'string' ? { testId } : {}}
|
|
29
53
|
>
|
|
30
|
-
|
|
31
|
-
<Img inlineSvg src={icon} alt="" fallback="" classes="file-dropzone-trigger-icon-img" />
|
|
32
|
-
</div>
|
|
33
|
-
<p class="file-dropzone-trigger-heading-wrap">
|
|
34
|
-
<span class="file-dropzone-trigger-heading">{heading}</span>
|
|
35
|
-
</p>
|
|
36
|
-
{#if caption}
|
|
37
|
-
<p class="file-dropzone-trigger-caption">{caption}</p>
|
|
38
|
-
{/if}
|
|
54
|
+
{@render body()}
|
|
39
55
|
</Button>
|
|
56
|
+
{:else}
|
|
57
|
+
<div
|
|
58
|
+
class="file-dropzone-trigger-surface file-dropzone-trigger-button {classes ?? ''}"
|
|
59
|
+
data-pw={testId}
|
|
60
|
+
testID={testId}
|
|
61
|
+
>
|
|
62
|
+
{@render body()}
|
|
63
|
+
</div>
|
|
40
64
|
{/if}
|
|
41
65
|
|
|
42
66
|
<style>
|
|
@@ -47,6 +71,76 @@
|
|
|
47
71
|
gap: var(--file-dropzone-trigger-compact-gap, 8px);
|
|
48
72
|
}
|
|
49
73
|
|
|
74
|
+
/* The non-interactive form of the trigger. It reproduces the primary, medium Button
|
|
75
|
+
this component rendered before, hook for hook -- same `--button-*` tokens, same
|
|
76
|
+
fallbacks -- so a consumer's `classes` override and the demo's look carry over to
|
|
77
|
+
the one place the button is no longer rendered: inside a FileInput region, which
|
|
78
|
+
is already the control. Button cannot do this itself: it always renders a native
|
|
79
|
+
<button> (or <a>). Keep this in step with Button's `.button-el` rules. */
|
|
80
|
+
.file-dropzone-trigger-surface {
|
|
81
|
+
/* The primary variant fill, behind the same internal name Button uses, which is
|
|
82
|
+
how it is recorded as dark in both themes rather than as a light default. */
|
|
83
|
+
--_btn-color: #3a4550;
|
|
84
|
+
|
|
85
|
+
position: relative;
|
|
86
|
+
max-height: var(--button-max-height, none);
|
|
87
|
+
max-width: var(--button-max-width, none);
|
|
88
|
+
min-width: var(--button-min-width, auto);
|
|
89
|
+
font-family: var(--button-font-family, inherit);
|
|
90
|
+
font-weight: var(--button-font-weight, 500);
|
|
91
|
+
font-size: var(--button-font-size, 14px);
|
|
92
|
+
background-color: var(--button-color, var(--_btn-color));
|
|
93
|
+
background-image: var(--button-background, none);
|
|
94
|
+
color: var(--button-text-color, #ffffff);
|
|
95
|
+
height: var(--button-height, fit-content);
|
|
96
|
+
padding: var(--button-padding, 16px);
|
|
97
|
+
margin: var(--button-margin, 0);
|
|
98
|
+
border-radius: var(--button-border-radius, var(--radius, 6px));
|
|
99
|
+
width: var(--button-width, fit-content);
|
|
100
|
+
cursor: var(--cursor, pointer);
|
|
101
|
+
opacity: var(--opacity, 1);
|
|
102
|
+
border: var(--button-border, none);
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
text-align: center;
|
|
105
|
+
text-decoration: var(--button-text-decoration, none);
|
|
106
|
+
line-height: var(--button-line-height, normal);
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: var(--button-justify-content, center);
|
|
109
|
+
align-items: center;
|
|
110
|
+
flex-direction: var(--button-content-flex-direction, row);
|
|
111
|
+
gap: var(--button-content-gap, 16px);
|
|
112
|
+
visibility: var(--button-visibility, visible);
|
|
113
|
+
box-shadow: var(--button-box-shadow, none);
|
|
114
|
+
transition: var(--button-transition, none);
|
|
115
|
+
white-space: var(--button-white-space, nowrap);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.file-dropzone-trigger-surface:hover {
|
|
119
|
+
background: var(
|
|
120
|
+
--button-hover-color,
|
|
121
|
+
var(--button-background, var(--button-color, var(--_btn-color)))
|
|
122
|
+
);
|
|
123
|
+
color: var(--button-hover-text-color, var(--button-text-color, #ffffff));
|
|
124
|
+
border: var(--button-hover-border, var(--button-border, none));
|
|
125
|
+
transform: var(--button-hover-transform, none);
|
|
126
|
+
box-shadow: var(--button-hover-box-shadow, var(--button-box-shadow, none));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.file-dropzone-trigger-surface:active {
|
|
130
|
+
transform: var(--button-active-transform, none);
|
|
131
|
+
background: var(
|
|
132
|
+
--button-active-background,
|
|
133
|
+
var(--button-background, var(--button-color, var(--_btn-color)))
|
|
134
|
+
);
|
|
135
|
+
box-shadow: var(--button-active-box-shadow, var(--button-box-shadow, none));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@media (prefers-reduced-motion: reduce) {
|
|
139
|
+
.file-dropzone-trigger-surface {
|
|
140
|
+
transition: none;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
50
144
|
.file-dropzone-trigger-icon-sm,
|
|
51
145
|
.file-dropzone-trigger-icon {
|
|
52
146
|
display: flex;
|