@juspay/svelte-ui-components 4.27.6 → 4.28.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/README.md +140 -36
- package/dist/Animations/ModalAnimation.svelte +15 -1
- package/dist/Animations/OverlayAnimation.svelte +7 -1
- package/dist/AreaChart/AreaChart.svelte +366 -85
- package/dist/AspectRatio/AspectRatio.svelte +51 -0
- package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
- package/dist/AspectRatio/properties.d.ts +27 -0
- package/dist/AspectRatio/properties.js +1 -0
- package/dist/Banner/Banner.svelte +10 -4
- package/dist/Banner/properties.d.ts +8 -0
- package/dist/BarChart/BarChart.svelte +156 -49
- package/dist/Book/Book.svelte +14 -1
- package/dist/BrandLoader/BrandLoader.svelte +76 -12
- package/dist/Button/Button.svelte +56 -11
- package/dist/Calendar/Calendar.svelte +88 -61
- package/dist/Card/Card.svelte +1 -1
- package/dist/Carousel/Carousel.svelte +6 -3
- package/dist/ChatComposer/ChatComposer.svelte +54 -4
- package/dist/ChatComposer/dictationState.d.ts +15 -0
- package/dist/ChatComposer/dictationState.js +9 -0
- package/dist/ChatComposer/properties.d.ts +28 -1
- package/dist/ChatMessage/ChatMessage.svelte +27 -1
- package/dist/CheckListItem/CheckListItem.svelte +3 -3
- package/dist/Checkbox/Checkbox.svelte +84 -3
- package/dist/Checkbox/properties.d.ts +45 -13
- package/dist/ChipInput/ChipInput.svelte +70 -3
- package/dist/ChipInput/properties.d.ts +16 -0
- package/dist/Choicebox/Choicebox.svelte +156 -1
- package/dist/Choicebox/group.d.ts +63 -0
- package/dist/Choicebox/group.js +127 -0
- package/dist/Choicebox/properties.d.ts +46 -0
- package/dist/ColorPicker/ColorPicker.svelte +169 -20
- package/dist/ColorPicker/properties.d.ts +16 -0
- package/dist/Combobox/Combobox.svelte +87 -25
- package/dist/Combobox/properties.d.ts +23 -0
- package/dist/CommandMenu/CommandMenu.svelte +27 -11
- package/dist/ContextMenu/ContextMenu.svelte +31 -37
- package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
- package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
- package/dist/Draggable/Draggable.svelte +12 -7
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
- package/dist/EmptyState/EmptyState.svelte +1 -1
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
- package/dist/FileInput/FileInput.svelte +50 -1
- package/dist/FileInput/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +40 -12
- package/dist/FunnelChart/properties.d.ts +7 -1
- package/dist/Gallery/Gallery.svelte +20 -13
- package/dist/Gallery/properties.d.ts +8 -0
- package/dist/GridItem/GridItem.svelte +31 -1
- package/dist/HITL/HITL.svelte +35 -9
- package/dist/Icon/Icon.svelte +19 -3
- package/dist/Input/Input.svelte +24 -10
- package/dist/Input/properties.d.ts +37 -1
- package/dist/InputButton/InputButton.svelte +86 -11
- package/dist/InputButton/properties.d.ts +10 -1
- package/dist/Label/Label.svelte +54 -0
- package/dist/Label/Label.svelte.d.ts +3 -0
- package/dist/Label/properties.d.ts +26 -0
- package/dist/Label/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +385 -94
- package/dist/ListItem/ListItem.svelte +121 -15
- package/dist/Loader/Loader.svelte +22 -2
- package/dist/LoadingDots/LoadingDots.svelte +21 -0
- package/dist/MediaUpload/MediaUpload.svelte +13 -3
- package/dist/Menu/Menu.svelte +56 -28
- package/dist/Modal/Modal.svelte +77 -42
- package/dist/Pagination/Pagination.svelte +9 -2
- package/dist/Phone/Phone.svelte +4 -0
- package/dist/PieChart/PieChart.svelte +282 -38
- package/dist/PieChart/properties.d.ts +9 -0
- package/dist/Pill/Pill.svelte +14 -5
- package/dist/Progress/Progress.svelte +20 -0
- package/dist/Radio/Radio.svelte +58 -1
- package/dist/Radio/properties.d.ts +23 -0
- package/dist/RatingGroup/RatingGroup.svelte +283 -0
- package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
- package/dist/RatingGroup/properties.d.ts +73 -0
- package/dist/RatingGroup/properties.js +1 -0
- package/dist/RelativeTime/RelativeTime.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +272 -50
- package/dist/SankeyChart/properties.d.ts +9 -0
- package/dist/Scroller/Scroller.svelte +37 -2
- package/dist/Select/Select.svelte +166 -46
- package/dist/Select/properties.d.ts +7 -0
- package/dist/Separator/Separator.svelte +48 -0
- package/dist/Separator/Separator.svelte.d.ts +3 -0
- package/dist/Separator/properties.d.ts +22 -0
- package/dist/Separator/properties.js +1 -0
- package/dist/Sheet/Sheet.svelte +60 -21
- package/dist/Shimmer/Shimmer.svelte +15 -0
- package/dist/Slider/Slider.svelte +87 -6
- package/dist/Slider/properties.d.ts +26 -0
- package/dist/Snippet/Snippet.svelte +4 -0
- package/dist/Snippet/copyState.svelte.d.ts +13 -1
- package/dist/Snippet/copyState.svelte.js +27 -1
- package/dist/Snippet/properties.d.ts +7 -0
- package/dist/SoundKit/SoundKit.js +9 -8
- package/dist/SplitInput/SplitInput.svelte +68 -2
- package/dist/SplitInput/properties.d.ts +23 -0
- package/dist/StatCard/StatCard.svelte +6 -6
- package/dist/Status/Status.svelte +2 -2
- package/dist/Stepper/Step.svelte +19 -6
- package/dist/Stepper/Stepper.svelte +108 -35
- package/dist/Table/BuiltinCell.svelte +2 -2
- package/dist/Table/Table.svelte +373 -55
- package/dist/Table/properties.d.ts +134 -3
- package/dist/Tabs/Tabs.svelte +263 -65
- package/dist/Tabs/properties.d.ts +23 -0
- package/dist/TaskList/TaskList.svelte +14 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
- package/dist/Toast/Toast.svelte +27 -2
- package/dist/Toggle/Toggle.svelte +79 -7
- package/dist/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/Toggle/properties.d.ts +30 -7
- package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
- package/dist/ToolCallLog/properties.d.ts +10 -0
- package/dist/Toolbar/Toolbar.svelte +1 -1
- package/dist/Tooltip/Tooltip.svelte +20 -2
- package/dist/Tooltip/tooltip-action.js +39 -4
- package/dist/TypewriterText/TypewriterText.svelte +34 -2
- package/dist/TypewriterText/properties.d.ts +18 -5
- package/dist/_chart/ChartContainer.svelte +12 -0
- package/dist/_chart/ChartTooltip.svelte +18 -4
- package/dist/_chart/Legend.svelte +21 -0
- package/dist/_chart/Legend.svelte.d.ts +15 -0
- package/dist/_chart/geometry.d.ts +54 -1
- package/dist/_chart/geometry.js +122 -14
- package/dist/_chart/interactions.d.ts +5 -1
- package/dist/_chart/interactions.js +9 -5
- package/dist/_chart/scales.js +16 -0
- package/dist/_chart/temporal.d.ts +45 -0
- package/dist/_chart/temporal.js +116 -0
- package/dist/_field/description.d.ts +50 -0
- package/dist/_field/description.js +51 -0
- package/dist/_interaction/dismissal.d.ts +67 -0
- package/dist/_interaction/dismissal.js +137 -0
- package/dist/_interaction/focus.d.ts +73 -0
- package/dist/_interaction/focus.js +143 -0
- package/dist/assets/star.svg +5 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +21 -1
- package/dist/reduced-motion.svelte.d.ts +37 -0
- package/dist/reduced-motion.svelte.js +44 -0
- package/dist/styles/theme-dark.css +778 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +62 -1
- package/dist/utils.js +80 -21
- package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
- package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
- package/dist-codemod/codemod/migrate-audit.js +395 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
- package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
- package/dist-codemod/migrate/analyze.js +804 -0
- package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
- package/dist-wc/index.js +13992 -4323
- package/package.json +13 -6
- package/dist/Modal/focus-trap.d.ts +0 -42
- package/dist/Modal/focus-trap.js +0 -57
- /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
- /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
- /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutual exclusion and arrow-key roving for `Choicebox`es that share a `name`.
|
|
3
|
+
*
|
|
4
|
+
* A native radio group is a browser behaviour rather than a component one: the
|
|
5
|
+
* browser finds the other `<input type="radio">`s carrying the same name and
|
|
6
|
+
* unchecks them. Choicebox's selection lives in a per-instance `selected` prop,
|
|
7
|
+
* so nothing was doing that job -- two cards a consumer thought of as one group
|
|
8
|
+
* could both be selected, and a keyboard user tabbed through every card instead
|
|
9
|
+
* of arrowing between them, which is not how a radio group behaves.
|
|
10
|
+
*
|
|
11
|
+
* Membership is keyed by name AND by root node, so two forms on one page that
|
|
12
|
+
* both use `name="plan"` stay separate groups and a card inside a shadow root
|
|
13
|
+
* does not join one in the document. That is narrower than the platform, which
|
|
14
|
+
* scopes a radio group to the owning form; form ownership is not readable from
|
|
15
|
+
* a card that may not be inside a form at all, and the root node is the closest
|
|
16
|
+
* boundary that always exists.
|
|
17
|
+
*
|
|
18
|
+
* The registry is imperative on purpose. Roving tabindex is a property of the
|
|
19
|
+
* WHOLE group -- the single tab stop moves when any member's selection or
|
|
20
|
+
* disabled state changes -- and a per-instance `$derived` cannot observe a
|
|
21
|
+
* sibling's props. Each member re-registers whenever its own inputs change,
|
|
22
|
+
* which is what re-runs the group-wide recalculation below.
|
|
23
|
+
*/
|
|
24
|
+
const registry = new Set();
|
|
25
|
+
/** Members of `member`'s group, in DOM order. */
|
|
26
|
+
function groupOf(member) {
|
|
27
|
+
const root = member.element.getRootNode();
|
|
28
|
+
return [...registry]
|
|
29
|
+
.filter((other) => other.name === member.name && other.element.getRootNode() === root)
|
|
30
|
+
.sort((a, b) => (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
|
|
31
|
+
? -1
|
|
32
|
+
: 1);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* One tab stop per group, on the selected card -- or on the first enabled card
|
|
36
|
+
* while nothing is selected, which is what keeps an untouched group reachable
|
|
37
|
+
* by Tab at all.
|
|
38
|
+
*/
|
|
39
|
+
function refreshRoving(group) {
|
|
40
|
+
const enabled = group.filter((candidate) => !candidate.isDisabled());
|
|
41
|
+
const tabbable = enabled.find((candidate) => candidate.isSelected()) ?? enabled.at(0) ?? null;
|
|
42
|
+
for (const member of group) {
|
|
43
|
+
member.element.tabIndex = member.isDisabled() ? -1 : member === tabbable ? 0 : -1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Adds a card to the group named `name`. Returns the function that removes it
|
|
48
|
+
* again; both ends recalculate the group's tab stop, so a card unmounting while
|
|
49
|
+
* it held the tab stop hands it on rather than leaving the group unreachable.
|
|
50
|
+
*/
|
|
51
|
+
export function joinChoiceboxGroup(name, member) {
|
|
52
|
+
const registration = { ...member, name };
|
|
53
|
+
registry.add(registration);
|
|
54
|
+
refreshRoving(groupOf(registration));
|
|
55
|
+
return () => {
|
|
56
|
+
registry.delete(registration);
|
|
57
|
+
refreshRoving(groupOf(registration));
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** The registration this element was added under, or null when ungrouped. */
|
|
61
|
+
function registrationFor(element) {
|
|
62
|
+
for (const candidate of registry) {
|
|
63
|
+
if (candidate.element === element) {
|
|
64
|
+
return candidate;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Selects one card and deselects the rest, the way checking a native radio
|
|
71
|
+
* unchecks its group. Siblings are told through `setSelected` so a consumer
|
|
72
|
+
* binding `selected` on each card sees the change, and through `notify` so a
|
|
73
|
+
* consumer listening on `onclick` is not left believing a deselected card is
|
|
74
|
+
* still selected.
|
|
75
|
+
*/
|
|
76
|
+
export function selectExclusively(element) {
|
|
77
|
+
const registration = registrationFor(element);
|
|
78
|
+
if (registration === null) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const group = groupOf(registration);
|
|
82
|
+
for (const member of group) {
|
|
83
|
+
if (member === registration || !member.isSelected()) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
member.setSelected(false);
|
|
87
|
+
member.notify(false);
|
|
88
|
+
}
|
|
89
|
+
registration.setSelected(true);
|
|
90
|
+
refreshRoving(group);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Arrow-key movement inside a radio group. Moving selects, which is what a
|
|
94
|
+
* native radio group does and what makes the group usable without a mouse: the
|
|
95
|
+
* arrow keys are the only way to change the choice once Tab has entered it.
|
|
96
|
+
*
|
|
97
|
+
* Returns false when the element is not in a group, so the caller can leave the
|
|
98
|
+
* key alone rather than swallowing it.
|
|
99
|
+
*/
|
|
100
|
+
export function moveWithinChoiceboxGroup(element, step) {
|
|
101
|
+
const registration = registrationFor(element);
|
|
102
|
+
if (registration === null) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
const enabled = groupOf(registration).filter((candidate) => !candidate.isDisabled());
|
|
106
|
+
if (enabled.length === 0) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
const current = enabled.indexOf(registration);
|
|
110
|
+
if (current === -1) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const target = step === 'first'
|
|
114
|
+
? enabled.at(0)
|
|
115
|
+
: step === 'last'
|
|
116
|
+
? enabled.at(-1)
|
|
117
|
+
: // Wraps, like a native radio group: arrowing past the last member
|
|
118
|
+
// returns to the first rather than stopping dead.
|
|
119
|
+
enabled.at((current + (step === 'next' ? 1 : -1) + enabled.length) % enabled.length);
|
|
120
|
+
if (typeof target === 'undefined' || target === registration) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
selectExclusively(target.element);
|
|
124
|
+
target.notify(true);
|
|
125
|
+
target.element.focus();
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
@@ -9,6 +9,52 @@ export type OptionalChoiceboxProperties = {
|
|
|
9
9
|
showIndicator?: boolean;
|
|
10
10
|
testId?: string;
|
|
11
11
|
classes?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Text shown, and announced, when the card is in error. Linked to the card
|
|
14
|
+
* through `aria-describedby`, and sets `aria-invalid` while present.
|
|
15
|
+
*/
|
|
16
|
+
errorMessage?: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Persistent helper text describing the card. Linked through
|
|
19
|
+
* `aria-describedby` too, so it is read before the user trips an error rather
|
|
20
|
+
* than only after.
|
|
21
|
+
*/
|
|
22
|
+
infoMessage?: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Marks the card invalid without supplying a message, for a consumer driving
|
|
25
|
+
* validity from a server or its own rules.
|
|
26
|
+
*/
|
|
27
|
+
invalid?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Submits the card under this name when it sits inside (or is associated with)
|
|
30
|
+
* a form. Omitted entirely when absent, unselected or disabled, which is what
|
|
31
|
+
* a native checkbox or radio does.
|
|
32
|
+
*
|
|
33
|
+
* In `radio` mode a name also GROUPS the cards that share it: selecting one
|
|
34
|
+
* deselects the rest, and Arrow/Home/End move the selection between them under
|
|
35
|
+
* a single tab stop, the way a native radio group behaves. Grouping is scoped
|
|
36
|
+
* to cards sharing a root node, so two forms on one page can both use
|
|
37
|
+
* `name="plan"` without interfering. Leave `name` off and each card stays the
|
|
38
|
+
* independent toggle it was before this prop existed.
|
|
39
|
+
*/
|
|
40
|
+
name?: string;
|
|
41
|
+
/** Value submitted while selected. Defaults to `'on'`, the native default. */
|
|
42
|
+
value?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Makes an unselected card block submission. In a named `radio` group this is
|
|
45
|
+
* the native "pick one" rule over the whole group. Invalid validation moves
|
|
46
|
+
* focus to the visible card, since the form control behind it is deliberately
|
|
47
|
+
* not a tab stop.
|
|
48
|
+
*/
|
|
49
|
+
required?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* `id` of a form elsewhere in the same document, for a card rendered outside
|
|
52
|
+
* it. Custom elements have no form association through this prop: inside
|
|
53
|
+
* `<sui-choicebox>` the control lives in a shadow root and cannot reach a form
|
|
54
|
+
* in the host document, which is what `ElementInternals` handles there
|
|
55
|
+
* instead.
|
|
56
|
+
*/
|
|
57
|
+
form?: string;
|
|
12
58
|
};
|
|
13
59
|
export type ChoiceboxEventProperties = {
|
|
14
60
|
onclick?: (selected: boolean) => void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { registerDismissible } from '../_interaction/dismissal';
|
|
3
|
+
import { describeField } from '../_field/description';
|
|
2
4
|
import Button from '../Button/Button.svelte';
|
|
3
5
|
import Input from '../Input/Input.svelte';
|
|
4
6
|
import Slider from '../Slider/Slider.svelte';
|
|
@@ -24,13 +26,35 @@
|
|
|
24
26
|
testId,
|
|
25
27
|
onchange,
|
|
26
28
|
oninput,
|
|
27
|
-
classes
|
|
29
|
+
classes,
|
|
30
|
+
errorMessage,
|
|
31
|
+
infoMessage,
|
|
32
|
+
invalid = false
|
|
28
33
|
}: ColorPickerProperties = $props();
|
|
29
34
|
|
|
35
|
+
/* The control and the text that explains it were never linked: a screen-reader
|
|
36
|
+
user reaching the swatch heard "Pick a color" and nothing about why the last
|
|
37
|
+
one was rejected. `describeField` composes the reference from the messages
|
|
38
|
+
that are actually rendered, so aria-describedby never points at an id that is
|
|
39
|
+
not in the DOM -- which passes an attribute assertion and resolves to nothing
|
|
40
|
+
in a real reader.
|
|
41
|
+
|
|
42
|
+
It describes the group holding the swatch button and the hex field, because
|
|
43
|
+
the message is about the colour rather than about either control, and because
|
|
44
|
+
neither `Button` nor `Input` exposes an `aria-describedby` seam to bind it to
|
|
45
|
+
one of them. The role is conditional so a ColorPicker with no messages keeps
|
|
46
|
+
the accessibility tree it had before these props existed. */
|
|
47
|
+
const fieldUid = $props.id();
|
|
48
|
+
const field = $derived(
|
|
49
|
+
describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
|
|
50
|
+
);
|
|
51
|
+
const describedGroup = $derived(field.describedBy !== null || field.ariaInvalid !== null);
|
|
52
|
+
|
|
30
53
|
// ── Internal state ──────────────────────────────────────────────
|
|
31
54
|
|
|
32
55
|
let open = $state(false);
|
|
33
56
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
57
|
+
let triggerWrapEl: HTMLDivElement | null = $state(null);
|
|
34
58
|
|
|
35
59
|
let hue = $state(0);
|
|
36
60
|
let sat = $state(1);
|
|
@@ -46,21 +70,43 @@
|
|
|
46
70
|
let currentRgb = $derived(hsvToRgb(hue, sat, val));
|
|
47
71
|
let currentHsl = $derived(hsvToHsl(hue, sat, val));
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
// The hex string this component itself last wrote to `value` via commitColor.
|
|
74
|
+
// Plain (non-`$state`) bookkeeping -- like Tabs' `lastSelectionKey` -- so reading
|
|
75
|
+
// it never creates a reactive dependency; only `value` needs to.
|
|
76
|
+
let lastCommittedHex: string | null = null;
|
|
77
|
+
|
|
78
|
+
// Keeps hue/sat/val honest against `value` for every route that is NOT
|
|
79
|
+
// commitColor -- an externally-changed `value` prop, or the popover's own hex
|
|
80
|
+
// field/trigger input, which write `value` directly. `{@attach}` runs its
|
|
81
|
+
// callback inside a real reactive effect (see attachments.js), so reading
|
|
82
|
+
// `value` here re-runs it on change without reaching for the banned $effect.
|
|
83
|
+
// Skipped entirely when `value` merely echoes our own last commit, so the
|
|
84
|
+
// hue we just set from a drag never gets round-tripped back through hex.
|
|
85
|
+
function syncFromValue(_node: HTMLDivElement): void {
|
|
86
|
+
if (value === lastCommittedHex) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
50
89
|
const hsv = hexToHsv(value);
|
|
51
|
-
if (hsv
|
|
90
|
+
if (hsv === null) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
sat = hsv.s;
|
|
94
|
+
val = hsv.v;
|
|
95
|
+
// A grey hex (sat 0, from d === 0) has no recoverable hue -- rgbToHsv defaults
|
|
96
|
+
// it to 0, which is not "the" hue, just an artifact of the formula. Leaving
|
|
97
|
+
// `hue` alone here is what keeps a drag down to grey from snapping the hue
|
|
98
|
+
// slider to red.
|
|
99
|
+
if (hsv.s > 0) {
|
|
52
100
|
hue = hsv.h;
|
|
53
|
-
sat = hsv.s;
|
|
54
|
-
val = hsv.v;
|
|
55
101
|
}
|
|
56
|
-
|
|
57
|
-
});
|
|
102
|
+
}
|
|
58
103
|
|
|
59
104
|
// ── Color commit ────────────────────────────────────────────────
|
|
60
105
|
|
|
61
106
|
function commitColor() {
|
|
62
107
|
const hex = hsvToHex(hue, sat, val);
|
|
63
108
|
if (hex !== value) {
|
|
109
|
+
lastCommittedHex = hex;
|
|
64
110
|
value = hex;
|
|
65
111
|
oninput?.(value);
|
|
66
112
|
onchange?.(value);
|
|
@@ -75,15 +121,29 @@
|
|
|
75
121
|
}
|
|
76
122
|
}
|
|
77
123
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
124
|
+
// Registered for exactly the popover's open span (the {#if open} block below),
|
|
125
|
+
// same as Menu's dismissalAction, so this popover only answers Escape/outside-press
|
|
126
|
+
// while it is the topmost dismissible surface -- a picker opened inside a Modal or
|
|
127
|
+
// Sheet no longer closes the layer behind it. `element` is containerEl (the whole
|
|
128
|
+
// component, trigger included) rather than just the popover: a press on the swatch
|
|
129
|
+
// button that reopens/toggles the popover must not also be read as an outside press
|
|
130
|
+
// by the module, matching the old handleWindowClick's boundary exactly.
|
|
131
|
+
function dismissalAction(_node: HTMLDivElement) {
|
|
132
|
+
const release = registerDismissible({
|
|
133
|
+
element: () => containerEl,
|
|
134
|
+
// Wherever focus happens to be inside the popover -- the sat panel, a field,
|
|
135
|
+
// the mode-toggle button -- matching ContextMenu's "wherever focus happens to
|
|
136
|
+
// be" rule, then hands focus back to the trigger so keyboard users don't lose
|
|
137
|
+
// their place.
|
|
138
|
+
onEscape: () => {
|
|
139
|
+
open = false;
|
|
140
|
+
triggerWrapEl?.querySelector('button')?.focus();
|
|
141
|
+
},
|
|
142
|
+
onOutside: () => {
|
|
143
|
+
open = false;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
return { destroy: release };
|
|
87
147
|
}
|
|
88
148
|
|
|
89
149
|
// ── Saturation panel drag ───────────────────────────────────────
|
|
@@ -123,6 +183,53 @@
|
|
|
123
183
|
commitColor();
|
|
124
184
|
}
|
|
125
185
|
|
|
186
|
+
// Small step for the arrow keys, larger step for Page Up/Down -- mirrors the
|
|
187
|
+
// native <input type="range"> convention Slider relies on.
|
|
188
|
+
const SAT_STEP = 0.01;
|
|
189
|
+
const SAT_PAGE_STEP = 0.1;
|
|
190
|
+
|
|
191
|
+
function handleSatKeydown(e: KeyboardEvent) {
|
|
192
|
+
if (disabled) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
switch (e.key) {
|
|
196
|
+
case 'ArrowLeft':
|
|
197
|
+
sat = Math.max(0, sat - SAT_STEP);
|
|
198
|
+
break;
|
|
199
|
+
case 'ArrowRight':
|
|
200
|
+
sat = Math.min(1, sat + SAT_STEP);
|
|
201
|
+
break;
|
|
202
|
+
case 'ArrowUp':
|
|
203
|
+
val = Math.min(1, val + SAT_STEP);
|
|
204
|
+
break;
|
|
205
|
+
case 'ArrowDown':
|
|
206
|
+
val = Math.max(0, val - SAT_STEP);
|
|
207
|
+
break;
|
|
208
|
+
case 'PageUp':
|
|
209
|
+
// Larger step on the same axis Home/End jump on -- see below.
|
|
210
|
+
sat = Math.min(1, sat + SAT_PAGE_STEP);
|
|
211
|
+
break;
|
|
212
|
+
case 'PageDown':
|
|
213
|
+
sat = Math.max(0, sat - SAT_PAGE_STEP);
|
|
214
|
+
break;
|
|
215
|
+
case 'Home':
|
|
216
|
+
// Home/End follow the ARIA slider contract for "the" value this role
|
|
217
|
+
// formally exposes -- aria-valuenow/min/max track saturation, so that
|
|
218
|
+
// is the axis being adjusted here. Brightness is left untouched, same
|
|
219
|
+
// as arrow keys never touch the axis they aren't bound to.
|
|
220
|
+
sat = 0;
|
|
221
|
+
break;
|
|
222
|
+
case 'End':
|
|
223
|
+
sat = 1;
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
// Not one of ours -- leave it unconsumed (Tab, Enter, etc.).
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
commitColor();
|
|
231
|
+
}
|
|
232
|
+
|
|
126
233
|
// ── Hue slider ─────────────────────────────────────────────────
|
|
127
234
|
|
|
128
235
|
let hueDegrees = $derived(Math.round(hue * 360));
|
|
@@ -182,14 +289,17 @@
|
|
|
182
289
|
}
|
|
183
290
|
</script>
|
|
184
291
|
|
|
185
|
-
<svelte:window onclick={handleWindowClick} />
|
|
186
|
-
|
|
187
292
|
<div
|
|
188
293
|
class="color-picker-container {classes ?? ''}"
|
|
189
294
|
class:disabled
|
|
295
|
+
role={describedGroup ? 'group' : null}
|
|
296
|
+
aria-label={describedGroup && typeof label === 'string' ? label : null}
|
|
297
|
+
aria-describedby={field.describedBy}
|
|
298
|
+
aria-invalid={field.ariaInvalid}
|
|
190
299
|
data-pw={testId}
|
|
191
300
|
testID={testId}
|
|
192
301
|
bind:this={containerEl}
|
|
302
|
+
{@attach syncFromValue}
|
|
193
303
|
>
|
|
194
304
|
{#if typeof label === 'string'}
|
|
195
305
|
<span class="color-picker-label">{label}</span>
|
|
@@ -197,7 +307,11 @@
|
|
|
197
307
|
|
|
198
308
|
<!-- Trigger: swatch button + optional hex input -->
|
|
199
309
|
<div class="color-picker-row">
|
|
200
|
-
<div
|
|
310
|
+
<div
|
|
311
|
+
class="color-picker-swatch-btn"
|
|
312
|
+
class:standalone={showValue === false}
|
|
313
|
+
bind:this={triggerWrapEl}
|
|
314
|
+
>
|
|
201
315
|
<Button onclick={togglePicker} {disabled} ariaLabel="Pick a color" ariaExpanded={open}>
|
|
202
316
|
<span class="color-picker-checkerboard">
|
|
203
317
|
<span class="color-picker-swatch" style="background-color: {value};"></span>
|
|
@@ -219,7 +333,7 @@
|
|
|
219
333
|
|
|
220
334
|
<!-- Popover panel -->
|
|
221
335
|
{#if open}
|
|
222
|
-
<div class="color-picker-popover" role="dialog" aria-label="Color picker">
|
|
336
|
+
<div class="color-picker-popover" role="dialog" aria-label="Color picker" use:dismissalAction>
|
|
223
337
|
<div
|
|
224
338
|
class="cp-sat-panel"
|
|
225
339
|
style="background-color: {hsvToHex(hue, 1, 1)};"
|
|
@@ -227,6 +341,7 @@
|
|
|
227
341
|
onpointerdown={handleSatDown}
|
|
228
342
|
onpointermove={handleSatMove}
|
|
229
343
|
onpointerup={handleSatUp}
|
|
344
|
+
onkeydown={handleSatKeydown}
|
|
230
345
|
role="slider"
|
|
231
346
|
aria-label="Saturation and brightness"
|
|
232
347
|
aria-valuenow={Math.round(sat * 100)}
|
|
@@ -303,7 +418,41 @@
|
|
|
303
418
|
{/if}
|
|
304
419
|
</div>
|
|
305
420
|
|
|
421
|
+
{#if field.showsError}
|
|
422
|
+
<div
|
|
423
|
+
id={field.errorId}
|
|
424
|
+
role="alert"
|
|
425
|
+
class="field-error"
|
|
426
|
+
data-pw={typeof testId === 'string' ? `${testId}-error-message` : null}
|
|
427
|
+
testID={typeof testId === 'string' ? `${testId}-error-message` : null}
|
|
428
|
+
>
|
|
429
|
+
{errorMessage}
|
|
430
|
+
</div>
|
|
431
|
+
{/if}
|
|
432
|
+
{#if field.showsInfo}
|
|
433
|
+
<div
|
|
434
|
+
id={field.infoId}
|
|
435
|
+
class="field-info"
|
|
436
|
+
data-pw={typeof testId === 'string' ? `${testId}-info-message` : null}
|
|
437
|
+
testID={typeof testId === 'string' ? `${testId}-info-message` : null}
|
|
438
|
+
>
|
|
439
|
+
{infoMessage}
|
|
440
|
+
</div>
|
|
441
|
+
{/if}
|
|
442
|
+
|
|
306
443
|
<style>
|
|
444
|
+
.field-error {
|
|
445
|
+
color: var(--field-error-color, #c5120a);
|
|
446
|
+
font-size: var(--field-error-font-size, 12px);
|
|
447
|
+
margin: var(--field-error-margin, 4px 0 0 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.field-info {
|
|
451
|
+
color: var(--field-info-color, #6b7280);
|
|
452
|
+
font-size: var(--field-info-font-size, 12px);
|
|
453
|
+
margin: var(--field-info-margin, 4px 0 0 0);
|
|
454
|
+
}
|
|
455
|
+
|
|
307
456
|
/* ── Container ─────────────────────────────────────────────── */
|
|
308
457
|
|
|
309
458
|
.color-picker-container {
|
|
@@ -8,6 +8,22 @@ export type OptionalColorPickerProperties = {
|
|
|
8
8
|
showValue?: boolean;
|
|
9
9
|
testId?: string;
|
|
10
10
|
classes?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Text shown, and announced, when the chosen colour is in error ("not enough
|
|
13
|
+
* contrast on white"). Referenced by `aria-describedby` on the group holding
|
|
14
|
+
* the swatch and the hex field, and sets `aria-invalid` while present.
|
|
15
|
+
*/
|
|
16
|
+
errorMessage?: string | null;
|
|
17
|
+
/**
|
|
18
|
+
* Persistent helper text describing the control. Referenced the same way, so
|
|
19
|
+
* it is read before the user trips an error rather than only after.
|
|
20
|
+
*/
|
|
21
|
+
infoMessage?: string | null;
|
|
22
|
+
/**
|
|
23
|
+
* Marks the control invalid without supplying a message, for a consumer
|
|
24
|
+
* driving validity from a server or its own rules.
|
|
25
|
+
*/
|
|
26
|
+
invalid?: boolean;
|
|
11
27
|
};
|
|
12
28
|
export type ColorPickerEventProperties = {
|
|
13
29
|
onchange?: (value: string) => void;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { registerDismissible } from '../_interaction/dismissal';
|
|
3
|
+
import { describeField } from '../_field/description';
|
|
4
|
+
import { tick } from 'svelte';
|
|
3
5
|
import Input from '../Input/Input.svelte';
|
|
4
6
|
import Pill from '../Pill/Pill.svelte';
|
|
5
7
|
import type { ComboboxItem, ComboboxProperties } from './properties';
|
|
@@ -50,9 +52,32 @@
|
|
|
50
52
|
onchange,
|
|
51
53
|
onadd,
|
|
52
54
|
onremove,
|
|
53
|
-
oncreate
|
|
55
|
+
oncreate,
|
|
56
|
+
errorMessage,
|
|
57
|
+
infoMessage,
|
|
58
|
+
invalid = false
|
|
54
59
|
}: ComboboxProperties = $props();
|
|
55
60
|
|
|
61
|
+
/* The control and the text that explains it were never linked: a screen-reader
|
|
62
|
+
user reaching this combobox heard its name and nothing about why the value
|
|
63
|
+
was rejected. `describeField` composes the reference from the messages that
|
|
64
|
+
are actually rendered, so aria-describedby never points at an id that is not
|
|
65
|
+
in the DOM -- which passes an attribute assertion and resolves to nothing in
|
|
66
|
+
a real reader.
|
|
67
|
+
|
|
68
|
+
It goes on a group around the control rather than on the `role="combobox"`
|
|
69
|
+
element itself, because that element is inside `Input` and `Input` exposes
|
|
70
|
+
no `aria-describedby` seam -- it derives its own from the messages IT is
|
|
71
|
+
given, which are a different pair of props. Per-control is the stronger
|
|
72
|
+
placement and needs that seam; see docs/Combobox.md. The role is conditional
|
|
73
|
+
so a Combobox with no messages keeps the accessibility tree it had before
|
|
74
|
+
these props existed. */
|
|
75
|
+
const fieldUid = $props.id();
|
|
76
|
+
const field = $derived(
|
|
77
|
+
describeField(fieldUid, { error: errorMessage, info: infoMessage, invalid })
|
|
78
|
+
);
|
|
79
|
+
const describedGroup = $derived(field.describedBy !== null || field.ariaInvalid !== null);
|
|
80
|
+
|
|
56
81
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
57
82
|
let inputRef: ReturnType<typeof Input> | null = $state(null);
|
|
58
83
|
|
|
@@ -295,12 +320,6 @@
|
|
|
295
320
|
removeValue(selected[selected.length - 1]);
|
|
296
321
|
}
|
|
297
322
|
break;
|
|
298
|
-
case 'Escape':
|
|
299
|
-
if (open) {
|
|
300
|
-
event.preventDefault();
|
|
301
|
-
closeDropdown();
|
|
302
|
-
}
|
|
303
|
-
break;
|
|
304
323
|
case 'Tab':
|
|
305
324
|
if (open) {
|
|
306
325
|
closeDropdown();
|
|
@@ -327,28 +346,30 @@
|
|
|
327
346
|
}
|
|
328
347
|
}
|
|
329
348
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
// Registered for exactly the dropdown's open span (the `{#if open && !disabled}`
|
|
350
|
+
// block below), same as Menu's dismissalAction, so this dropdown only answers
|
|
351
|
+
// Escape/outside-press while it is the topmost dismissible surface -- a combobox
|
|
352
|
+
// opened inside a Modal, Sheet, Menu, ContextMenu or CommandMenu no longer closes
|
|
353
|
+
// the layer behind it. Unlike Select's dropdown, this one is never portaled --
|
|
354
|
+
// it is always a DOM descendant of containerEl -- so containerEl alone is enough
|
|
355
|
+
// for the module's own inside/outside test; no second hit-test is needed here.
|
|
356
|
+
function dismissalAction(_node: HTMLDivElement) {
|
|
357
|
+
const release = registerDismissible({
|
|
358
|
+
element: () => containerEl,
|
|
359
|
+
onOutside: closeDropdown,
|
|
360
|
+
onEscape: closeDropdown
|
|
361
|
+
});
|
|
362
|
+
return { destroy: release };
|
|
338
363
|
}
|
|
339
|
-
|
|
340
|
-
onMount(() => {
|
|
341
|
-
document.addEventListener('click', handleClickOutside);
|
|
342
|
-
return () => {
|
|
343
|
-
document.removeEventListener('click', handleClickOutside);
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
364
|
</script>
|
|
347
365
|
|
|
348
366
|
<div
|
|
349
367
|
class="combobox {classes ?? ''}"
|
|
350
368
|
class:disabled
|
|
351
369
|
bind:this={containerEl}
|
|
370
|
+
role={describedGroup ? 'group' : null}
|
|
371
|
+
aria-describedby={field.describedBy}
|
|
372
|
+
aria-invalid={field.ariaInvalid}
|
|
352
373
|
data-pw={testId}
|
|
353
374
|
testID={testId}
|
|
354
375
|
>
|
|
@@ -394,7 +415,13 @@
|
|
|
394
415
|
</div>
|
|
395
416
|
|
|
396
417
|
{#if open && !disabled}
|
|
397
|
-
<div
|
|
418
|
+
<div
|
|
419
|
+
class="combobox-dropdown"
|
|
420
|
+
role="listbox"
|
|
421
|
+
id={listboxId}
|
|
422
|
+
aria-label={ariaLabel}
|
|
423
|
+
use:dismissalAction
|
|
424
|
+
>
|
|
398
425
|
{#if typeof dropdownHeader === 'function'}
|
|
399
426
|
<div class="combobox-dropdown-header">{@render dropdownHeader()}</div>
|
|
400
427
|
{/if}
|
|
@@ -498,7 +525,41 @@
|
|
|
498
525
|
{/if}
|
|
499
526
|
</div>
|
|
500
527
|
|
|
528
|
+
{#if field.showsError}
|
|
529
|
+
<div
|
|
530
|
+
id={field.errorId}
|
|
531
|
+
role="alert"
|
|
532
|
+
class="field-error"
|
|
533
|
+
data-pw={typeof testId === 'string' ? `${testId}-error-message` : null}
|
|
534
|
+
testID={typeof testId === 'string' ? `${testId}-error-message` : null}
|
|
535
|
+
>
|
|
536
|
+
{errorMessage}
|
|
537
|
+
</div>
|
|
538
|
+
{/if}
|
|
539
|
+
{#if field.showsInfo}
|
|
540
|
+
<div
|
|
541
|
+
id={field.infoId}
|
|
542
|
+
class="field-info"
|
|
543
|
+
data-pw={typeof testId === 'string' ? `${testId}-info-message` : null}
|
|
544
|
+
testID={typeof testId === 'string' ? `${testId}-info-message` : null}
|
|
545
|
+
>
|
|
546
|
+
{infoMessage}
|
|
547
|
+
</div>
|
|
548
|
+
{/if}
|
|
549
|
+
|
|
501
550
|
<style>
|
|
551
|
+
.field-error {
|
|
552
|
+
color: var(--field-error-color, #c5120a);
|
|
553
|
+
font-size: var(--field-error-font-size, 12px);
|
|
554
|
+
margin: var(--field-error-margin, 4px 0 0 0);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.field-info {
|
|
558
|
+
color: var(--field-info-color, #6b7280);
|
|
559
|
+
font-size: var(--field-info-font-size, 12px);
|
|
560
|
+
margin: var(--field-info-margin, 4px 0 0 0);
|
|
561
|
+
}
|
|
562
|
+
|
|
502
563
|
.combobox {
|
|
503
564
|
position: relative;
|
|
504
565
|
width: var(--combobox-width, 100%);
|
|
@@ -605,7 +666,8 @@
|
|
|
605
666
|
font-size: var(--combobox-option-font-size, inherit);
|
|
606
667
|
font-weight: var(--combobox-option-font-weight, inherit);
|
|
607
668
|
cursor: pointer;
|
|
608
|
-
transition: background 0.1s
|
|
669
|
+
transition: background var(--combobox-option-transition-duration, var(--motion-duration, 0.1s))
|
|
670
|
+
var(--combobox-option-transition-easing, var(--motion-easing, ease));
|
|
609
671
|
}
|
|
610
672
|
|
|
611
673
|
.combobox-option:hover,
|
|
@@ -58,6 +58,29 @@ export type OptionalComboboxProperties = {
|
|
|
58
58
|
action?: ComboboxAction;
|
|
59
59
|
/** Custom leading icon for the persistent action row. */
|
|
60
60
|
actionIcon?: Snippet;
|
|
61
|
+
/**
|
|
62
|
+
* Text shown, and announced, when the control is in error. Referenced by
|
|
63
|
+
* `aria-describedby` on a group wrapping the control, and sets `aria-invalid`
|
|
64
|
+
* while present.
|
|
65
|
+
*
|
|
66
|
+
* Placed on the group rather than on the `role="combobox"` element itself:
|
|
67
|
+
* that element is rendered by `Input`, which derives its own description from
|
|
68
|
+
* its own `onErrorMessage` / `infoMessage` props and exposes no seam for an
|
|
69
|
+
* outside one. A group description is announced on entering the group rather
|
|
70
|
+
* than on the control, which is weaker; pass `inputProperties.onErrorMessage`
|
|
71
|
+
* instead when you want the message bound to the text field itself.
|
|
72
|
+
*/
|
|
73
|
+
errorMessage?: string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Persistent helper text describing the control. Referenced the same way, so
|
|
76
|
+
* it is read before the user trips an error rather than only after.
|
|
77
|
+
*/
|
|
78
|
+
infoMessage?: string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Marks the control invalid without supplying a message, for a consumer
|
|
81
|
+
* driving validity from a server or its own rules.
|
|
82
|
+
*/
|
|
83
|
+
invalid?: boolean;
|
|
61
84
|
};
|
|
62
85
|
export type ComboboxEventProperties = {
|
|
63
86
|
onselect?: (item: ComboboxItem) => void;
|