@juspay/svelte-ui-components 2.135.0 → 2.136.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Chat/Chat.svelte +12 -0
- package/dist/Chat/properties.d.ts +7 -0
- package/dist/ChatMessageList/ChatMessageList.svelte +54 -1
- package/dist/CheckListItem/CheckListItem.svelte +4 -1
- package/dist/ChipInput/ChipInput.svelte +150 -10
- package/dist/ChipInput/properties.d.ts +18 -1
- package/dist/CommandMenu/CommandMenu.svelte +7 -3
- package/dist/EmptyState/EmptyState.svelte +17 -3
- package/dist/Input/Input.svelte +1 -0
- package/dist/ListItem/ListItem.svelte +14 -12
- package/dist/ListItem/properties.d.ts +7 -0
- package/dist/Menu/Menu.svelte +2 -1
- package/dist/Menu/properties.d.ts +2 -0
- package/dist/Modal/Modal.svelte +3 -3
- package/dist/Pill/Pill.svelte +2 -0
- package/dist/Pill/properties.d.ts +1 -0
- package/dist/Sheet/Sheet.svelte +3 -23
- package/dist/StatCard/StatCard.svelte +64 -1
- package/dist/StatCard/properties.d.ts +44 -2
- package/dist/Status/Status.svelte +2 -1
- package/dist/Status/properties.d.ts +10 -0
- package/dist/Stepper/Step.svelte +95 -2
- package/dist/Stepper/Stepper.svelte +52 -7
- package/dist/Stepper/properties.d.ts +34 -1
- package/dist/Table/BuiltinCell.svelte +46 -23
- package/dist/Table/Table.svelte +63 -40
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +70 -0
- package/dist/TypewriterText/TypewriterText.svelte +67 -2
- package/dist/TypewriterText/properties.d.ts +97 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +30 -0
- package/dist-wc/index.js +1186 -926
- package/package.json +1 -1
package/dist/Chat/Chat.svelte
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
streaming = false,
|
|
19
19
|
recording = false,
|
|
20
20
|
autoscroll = true,
|
|
21
|
+
scrollPolicy = 'near-bottom',
|
|
22
|
+
pinHold = false,
|
|
23
|
+
jump = true,
|
|
24
|
+
jumpLabel = 'Jump to latest',
|
|
25
|
+
jumpIcon,
|
|
21
26
|
toolStatus = null,
|
|
22
27
|
suggestions = [],
|
|
23
28
|
attachments = $bindable([]),
|
|
@@ -46,6 +51,7 @@
|
|
|
46
51
|
onattach,
|
|
47
52
|
onretry,
|
|
48
53
|
onfeedback,
|
|
54
|
+
onscrollstate,
|
|
49
55
|
testId,
|
|
50
56
|
classes
|
|
51
57
|
}: ChatProperties = $props();
|
|
@@ -92,6 +98,11 @@
|
|
|
92
98
|
<ChatMessageList
|
|
93
99
|
{messages}
|
|
94
100
|
{autoscroll}
|
|
101
|
+
{scrollPolicy}
|
|
102
|
+
{pinHold}
|
|
103
|
+
{jump}
|
|
104
|
+
{jumpLabel}
|
|
105
|
+
{jumpIcon}
|
|
95
106
|
{message}
|
|
96
107
|
{messageBody}
|
|
97
108
|
{messageAttachments}
|
|
@@ -99,6 +110,7 @@
|
|
|
99
110
|
{allowCopy}
|
|
100
111
|
{onretry}
|
|
101
112
|
{onfeedback}
|
|
113
|
+
{onscrollstate}
|
|
102
114
|
/>
|
|
103
115
|
|
|
104
116
|
<div class="footer">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { ChatMessageData, ChatToolStatus } from './types';
|
|
3
3
|
import type { ChatMessageFeedback } from '../ChatMessage/properties';
|
|
4
|
+
import type { ChatMessageListProperties } from '../ChatMessageList/properties';
|
|
4
5
|
import type { ChatSuggestion } from '../ChatSuggestions/properties';
|
|
5
6
|
export type ChatProperties = OptionalChatProperties & ChatEventProperties & MandatoryChatProperties;
|
|
6
7
|
export type MandatoryChatProperties = {
|
|
@@ -17,6 +18,11 @@ export type OptionalChatProperties = {
|
|
|
17
18
|
streaming?: boolean;
|
|
18
19
|
recording?: boolean;
|
|
19
20
|
autoscroll?: boolean;
|
|
21
|
+
scrollPolicy?: ChatMessageListProperties['scrollPolicy'];
|
|
22
|
+
pinHold?: ChatMessageListProperties['pinHold'];
|
|
23
|
+
jump?: ChatMessageListProperties['jump'];
|
|
24
|
+
jumpLabel?: ChatMessageListProperties['jumpLabel'];
|
|
25
|
+
jumpIcon?: ChatMessageListProperties['jumpIcon'];
|
|
20
26
|
toolStatus?: ChatToolStatus | null;
|
|
21
27
|
suggestions?: ChatSuggestion[];
|
|
22
28
|
attachments?: File[];
|
|
@@ -50,4 +56,5 @@ export type ChatEventProperties = {
|
|
|
50
56
|
onattach?: (files: File[]) => void;
|
|
51
57
|
onretry?: () => void;
|
|
52
58
|
onfeedback?: (value: ChatMessageFeedback, message: ChatMessageData) => void;
|
|
59
|
+
onscrollstate?: ChatMessageListProperties['onscrollstate'];
|
|
53
60
|
};
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
let atBottom = $state(true);
|
|
36
36
|
let scrollable = $state(false);
|
|
37
37
|
let pinActive = false;
|
|
38
|
+
// Set when a turn without pinHold has asked for its reservation back, but the reply is not yet
|
|
39
|
+
// tall enough to hold the pinned position without it. Cleared by tryReleasePin.
|
|
40
|
+
let pinReleasePending = false;
|
|
41
|
+
// The scroll offset the pin put the sender message at, so a later release can restore it.
|
|
42
|
+
let pinnedScrollTop = 0;
|
|
38
43
|
|
|
39
44
|
let scrollKey = $derived(`${messages.length}:${messages.at(-1)?.content.length ?? 0}`);
|
|
40
45
|
let showJump = $derived(!atBottom && messages.length > 0);
|
|
@@ -133,7 +138,13 @@
|
|
|
133
138
|
pinActive = true;
|
|
134
139
|
const listRect = listEl.getBoundingClientRect();
|
|
135
140
|
const paddingTop = Number.parseFloat(getComputedStyle(listEl).paddingTop) || 0;
|
|
136
|
-
|
|
141
|
+
// This is a layout correction, not a reader-initiated scroll, so it must land instantly.
|
|
142
|
+
// .chat-message-list sets scroll-behavior: smooth, under which `scrollTop = x` starts an
|
|
143
|
+
// ANIMATION: scrollTop still reads its old value on the next line, and anything that shrinks
|
|
144
|
+
// the scrollable range before the animation finishes clamps it partway. Releasing the
|
|
145
|
+
// reservation does exactly that, which is how the pin was being lost.
|
|
146
|
+
pinnedScrollTop = listEl.scrollTop + (targetRect.top - listRect.top - paddingTop);
|
|
147
|
+
listEl.scrollTo({ top: pinnedScrollTop, behavior: 'instant' });
|
|
137
148
|
reportScrollState(listEl);
|
|
138
149
|
}
|
|
139
150
|
|
|
@@ -142,11 +153,45 @@
|
|
|
142
153
|
innerEl.style.minHeight = '';
|
|
143
154
|
}
|
|
144
155
|
pinActive = false;
|
|
156
|
+
pinReleasePending = false;
|
|
145
157
|
if (listEl !== null) {
|
|
146
158
|
reportScrollState(listEl);
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Give the reserved headroom back only once the turn's own content can hold the pinned position.
|
|
164
|
+
*
|
|
165
|
+
* The reservation is what makes the sender message able to sit at the top at all. Clearing it
|
|
166
|
+
* while the reply is still shorter than the frame shrinks the scrollable range below the pinned
|
|
167
|
+
* offset, the browser clamps, and the question slides back down -- on short answers, which is
|
|
168
|
+
* where pinning matters most. So measure rather than assume: lift the reservation, check whether
|
|
169
|
+
* the natural content still reaches the pinned offset, and put it straight back if it does not.
|
|
170
|
+
* Reply growth re-runs this through the resize observer, so the reservation disappears by itself
|
|
171
|
+
* the moment it stops being load-bearing.
|
|
172
|
+
*/
|
|
173
|
+
function tryReleasePin(): void {
|
|
174
|
+
if (!pinReleasePending || listEl === null || innerEl === null) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const reserved = innerEl.style.minHeight;
|
|
178
|
+
// Lifting the reservation to measure also shrinks the scrollable range, and the browser
|
|
179
|
+
// clamps scrollTop to the smaller maximum the moment it does. Putting the reservation back
|
|
180
|
+
// does NOT undo that clamp, so the probe would silently destroy the very position it is
|
|
181
|
+
// checking. Restore the offset explicitly on both paths.
|
|
182
|
+
innerEl.style.minHeight = '';
|
|
183
|
+
const naturalMaxScroll = listEl.scrollHeight - listEl.clientHeight;
|
|
184
|
+
if (naturalMaxScroll >= pinnedScrollTop) {
|
|
185
|
+
listEl.scrollTo({ top: pinnedScrollTop, behavior: 'instant' });
|
|
186
|
+
pinActive = false;
|
|
187
|
+
pinReleasePending = false;
|
|
188
|
+
reportScrollState(listEl);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
innerEl.style.minHeight = reserved;
|
|
192
|
+
listEl.scrollTo({ top: pinnedScrollTop, behavior: 'instant' });
|
|
193
|
+
}
|
|
194
|
+
|
|
150
195
|
const pinToBottom: Action<HTMLElement, string> = (node) => {
|
|
151
196
|
let previousCount = messages.length;
|
|
152
197
|
let previousSenderId = lastSenderId();
|
|
@@ -162,6 +207,13 @@
|
|
|
162
207
|
if (newMessage && senderId !== null && senderId !== previousSenderId) {
|
|
163
208
|
queueMicrotask(() => {
|
|
164
209
|
pinLatestSenderMessage();
|
|
210
|
+
// A host opts into reserved reply headroom with pinHold. Without it the reservation
|
|
211
|
+
// is still doing work until the reply itself can hold the pin, so mark it for release
|
|
212
|
+
// and let tryReleasePin decide when that is actually true.
|
|
213
|
+
if (!pinHold) {
|
|
214
|
+
pinReleasePending = true;
|
|
215
|
+
tryReleasePin();
|
|
216
|
+
}
|
|
165
217
|
});
|
|
166
218
|
}
|
|
167
219
|
previousSenderId = senderId;
|
|
@@ -194,6 +246,7 @@
|
|
|
194
246
|
if (scrollPolicy === 'near-bottom' && autoscroll && atBottom) {
|
|
195
247
|
listEl.scrollTop = listEl.scrollHeight;
|
|
196
248
|
}
|
|
249
|
+
tryReleasePin();
|
|
197
250
|
reportScrollState(listEl);
|
|
198
251
|
});
|
|
199
252
|
observer.observe(node);
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
.container.disabled {
|
|
48
|
-
opacity: var(--check-list-item-disabled-opacity, 0.4);
|
|
49
48
|
cursor: not-allowed;
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -53,6 +52,10 @@
|
|
|
53
52
|
flex-shrink: 0;
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
.container.disabled .checkbox-wrapper {
|
|
56
|
+
--checkbox-disabled-opacity: var(--check-list-item-disabled-opacity, 0.4);
|
|
57
|
+
}
|
|
58
|
+
|
|
56
59
|
.text {
|
|
57
60
|
font-size: var(--check-list-item-text-size, 14px);
|
|
58
61
|
color: var(--check-list-item-text-color);
|
|
@@ -8,15 +8,26 @@
|
|
|
8
8
|
ariaLabel,
|
|
9
9
|
placeholder = 'Add value…',
|
|
10
10
|
disabled = false,
|
|
11
|
+
editable = false,
|
|
11
12
|
testId,
|
|
12
13
|
classes,
|
|
13
14
|
onadd,
|
|
14
15
|
ondismiss,
|
|
16
|
+
onedit,
|
|
15
17
|
onchange
|
|
16
18
|
}: ChipInputProperties = $props();
|
|
17
19
|
|
|
18
20
|
let draft = $state('');
|
|
19
21
|
|
|
22
|
+
// The chip currently swapped for an inline edit field, held by VALUE rather than by slot
|
|
23
|
+
// index. `values` is dedup-only by construction, so the text is a stable identity, and it is
|
|
24
|
+
// already what the {#each} is keyed by. An index is only a position: if the parent reorders or
|
|
25
|
+
// removes entries while an edit is open, the same index points at a different chip and the
|
|
26
|
+
// commit lands on the wrong one. Only one chip can be mid-edit at a time.
|
|
27
|
+
let editingChip = $state<string | null>(null);
|
|
28
|
+
let editDraft = $state('');
|
|
29
|
+
let editInputRef = $state<{ focus: () => void } | null>(null);
|
|
30
|
+
|
|
20
31
|
function addChip(): void {
|
|
21
32
|
const trimmed = draft.trim();
|
|
22
33
|
draft = '';
|
|
@@ -38,6 +49,11 @@
|
|
|
38
49
|
return;
|
|
39
50
|
}
|
|
40
51
|
values = [...values.slice(0, chipIndex), ...values.slice(chipIndex + 1)];
|
|
52
|
+
// Identity survives a delete elsewhere in the list, so only the edited chip's own removal
|
|
53
|
+
// has to close the field. No index fix-up is needed.
|
|
54
|
+
if (editingChip === chip) {
|
|
55
|
+
cancelEdit();
|
|
56
|
+
}
|
|
41
57
|
ondismiss?.(chip);
|
|
42
58
|
onchange?.([...values]);
|
|
43
59
|
}
|
|
@@ -48,18 +64,106 @@
|
|
|
48
64
|
addChip();
|
|
49
65
|
}
|
|
50
66
|
}
|
|
67
|
+
|
|
68
|
+
function startEdit(chip: string): void {
|
|
69
|
+
if (disabled) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
editingChip = chip;
|
|
73
|
+
editDraft = chip;
|
|
74
|
+
queueMicrotask(() => editInputRef?.focus());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cancelEdit(): void {
|
|
78
|
+
editingChip = null;
|
|
79
|
+
editDraft = '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A commit is only ever valid while the component is enabled and the chip it started on is
|
|
83
|
+
// still present. `disabled` can flip mid-edit, and a pending blur can arrive after it does.
|
|
84
|
+
//
|
|
85
|
+
// Together with the `!disabled` guard on the edit field's {#if}, this is what makes disabling
|
|
86
|
+
// mid-edit safe without an $effect: the field stops rendering, and any Enter or blur already in
|
|
87
|
+
// flight is refused here rather than writing to `values` behind a disabled control.
|
|
88
|
+
function commitEdit(chip: string): void {
|
|
89
|
+
if (disabled) {
|
|
90
|
+
cancelEdit();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const index = values.indexOf(chip);
|
|
94
|
+
if (index === -1) {
|
|
95
|
+
cancelEdit();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const original = chip;
|
|
99
|
+
const trimmed = editDraft.trim();
|
|
100
|
+
const isUnchanged = trimmed === original;
|
|
101
|
+
// Mirrors addChip's dedup-only contract: a blank edit or one that collides with a
|
|
102
|
+
// DIFFERENT existing chip is silently discarded rather than surfaced as an error.
|
|
103
|
+
const isBlankOrDuplicate = trimmed.length === 0 || (!isUnchanged && values.includes(trimmed));
|
|
104
|
+
if (isUnchanged || isBlankOrDuplicate) {
|
|
105
|
+
cancelEdit();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
values = values.map((value, valueIndex) => (valueIndex === index ? trimmed : value));
|
|
109
|
+
onedit?.(trimmed, original);
|
|
110
|
+
onchange?.([...values]);
|
|
111
|
+
cancelEdit();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleEditKeyDown(event: KeyboardEvent, chip: string): void {
|
|
115
|
+
if (event.key === 'Enter') {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
commitEdit(chip);
|
|
118
|
+
} else if (event.key === 'Escape') {
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
cancelEdit();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleEditBlur(chip: string): void {
|
|
125
|
+
if (editingChip !== chip) {
|
|
126
|
+
// Already resolved (Escape, or the chip removed) before this trailing blur arrived --
|
|
127
|
+
// committing again would act on an edit that is no longer open.
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
commitEdit(chip);
|
|
131
|
+
}
|
|
51
132
|
</script>
|
|
52
133
|
|
|
53
134
|
<div class="chip-input {classes ?? ''}" data-pw={testId} testID={testId}>
|
|
54
|
-
{#each values as chip (chip)}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
135
|
+
{#each values as chip, index (chip)}
|
|
136
|
+
{#if editable && editingChip === chip && !disabled}
|
|
137
|
+
<div class="chip-input-edit-wrap">
|
|
138
|
+
<Input
|
|
139
|
+
value={editDraft}
|
|
140
|
+
dataType="text"
|
|
141
|
+
name=""
|
|
142
|
+
autoComplete="off"
|
|
143
|
+
actionInput={false}
|
|
144
|
+
classes="chip-input-edit"
|
|
145
|
+
bind:this={editInputRef}
|
|
146
|
+
onInput={(nextValue) => {
|
|
147
|
+
editDraft = nextValue;
|
|
148
|
+
}}
|
|
149
|
+
onKeyDown={(event) => handleEditKeyDown(event, chip)}
|
|
150
|
+
onBlur={() => handleEditBlur(chip)}
|
|
151
|
+
{...typeof testId === 'string' ? { testId: `${testId}-item-${index}-edit` } : {}}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
{:else}
|
|
155
|
+
<Pill
|
|
156
|
+
text={chip}
|
|
157
|
+
classes="chip-input-pill"
|
|
158
|
+
dismissible={!disabled}
|
|
159
|
+
{disabled}
|
|
160
|
+
ondismiss={() => removeChip(chip)}
|
|
161
|
+
{...editable && !disabled
|
|
162
|
+
? { title: `Edit "${chip}"`, onclick: () => startEdit(chip) }
|
|
163
|
+
: {}}
|
|
164
|
+
{...typeof testId === 'string' ? { testId: `${testId}-item-${index}` } : {}}
|
|
165
|
+
/>
|
|
166
|
+
{/if}
|
|
63
167
|
{/each}
|
|
64
168
|
<div class="chip-input-draft-wrap">
|
|
65
169
|
<Input
|
|
@@ -77,7 +181,7 @@
|
|
|
77
181
|
}}
|
|
78
182
|
onKeyDown={handleKeyDown}
|
|
79
183
|
onBlur={addChip}
|
|
80
|
-
{...typeof testId === 'string' ? { testId: `${testId}-
|
|
184
|
+
{...typeof testId === 'string' ? { testId: `${testId}-add` } : {}}
|
|
81
185
|
/>
|
|
82
186
|
</div>
|
|
83
187
|
</div>
|
|
@@ -126,6 +230,10 @@
|
|
|
126
230
|
flex: var(--chip-input-draft-flex, 0 1 auto);
|
|
127
231
|
}
|
|
128
232
|
|
|
233
|
+
.chip-input-edit-wrap {
|
|
234
|
+
flex: var(--chip-input-edit-flex, 0 1 auto);
|
|
235
|
+
}
|
|
236
|
+
|
|
129
237
|
.chip-input :global(.chip-input-pill) {
|
|
130
238
|
--pill-gap: var(--chip-input-pill-gap, var(--chip-input-pill-gap-default));
|
|
131
239
|
--pill-background: var(--chip-input-pill-background, var(--chip-input-pill-background-default));
|
|
@@ -170,4 +278,36 @@
|
|
|
170
278
|
--input-margin: 0;
|
|
171
279
|
--input-box-shadow: none;
|
|
172
280
|
}
|
|
281
|
+
|
|
282
|
+
/* The in-place edit field replaces a Pill for the duration of one edit, so it reuses the same
|
|
283
|
+
captured --input-* cascade (--chip-input-draft-*-default, above) the draft field maps from --
|
|
284
|
+
an app that themes Input app-wide gets a matching edit field for free. Only sizing/padding
|
|
285
|
+
get their own --chip-input-edit-* tokens: the field holds an EXISTING (often longer) value
|
|
286
|
+
rather than a short in-progress draft, so it defaults wider. */
|
|
287
|
+
.chip-input-edit-wrap :global(.chip-input-edit) {
|
|
288
|
+
--input-width: var(--chip-input-edit-width, 110px);
|
|
289
|
+
--input-height: var(--chip-input-edit-height, 28px);
|
|
290
|
+
/* Fall through the DRAFT token before the captured default: the edit field is the same
|
|
291
|
+
control as the draft field, wearing the same clothes, so a consumer who themed the draft
|
|
292
|
+
with --chip-input-draft-* must get a matching edit field without naming it twice. */
|
|
293
|
+
--input-border: var(
|
|
294
|
+
--chip-input-edit-border,
|
|
295
|
+
var(--chip-input-draft-border, var(--chip-input-draft-border-default))
|
|
296
|
+
);
|
|
297
|
+
--input-radius: var(
|
|
298
|
+
--chip-input-edit-radius,
|
|
299
|
+
var(--chip-input-draft-radius, var(--chip-input-draft-radius-default))
|
|
300
|
+
);
|
|
301
|
+
--input-focus-border: var(
|
|
302
|
+
--chip-input-edit-focus-border,
|
|
303
|
+
var(--chip-input-draft-focus-border, var(--chip-input-draft-focus-border-default))
|
|
304
|
+
);
|
|
305
|
+
--input-font-size: var(
|
|
306
|
+
--chip-input-edit-font-size,
|
|
307
|
+
var(--chip-input-draft-font-size, var(--chip-input-draft-font-size-default))
|
|
308
|
+
);
|
|
309
|
+
--input-padding: var(--chip-input-edit-padding, 0 8px);
|
|
310
|
+
--input-margin: 0;
|
|
311
|
+
--input-box-shadow: none;
|
|
312
|
+
}
|
|
173
313
|
</style>
|
|
@@ -17,12 +17,29 @@ export type OptionalChipInputProperties = {
|
|
|
17
17
|
ariaLabel?: string;
|
|
18
18
|
placeholder?: string;
|
|
19
19
|
disabled?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Reach for this when a consumer's chips are prone to being ALMOST right -- a misspelled tag,
|
|
22
|
+
* a near-correct email -- and forcing a delete-and-retype round trip for every small correction
|
|
23
|
+
* would be annoying. When `true`, activating a committed chip (click, or Enter/Space once
|
|
24
|
+
* tabbed to it) swaps it for an inline text field pre-filled with its current value: Enter or
|
|
25
|
+
* blurring the field commits the edit back into `values`, Escape restores the original text and
|
|
26
|
+
* leaves `values` untouched. An edit that comes back blank or duplicates another chip is
|
|
27
|
+
* silently discarded, same as a blank/duplicate draft on add. Defaults to `false` -- chips stay
|
|
28
|
+
* display-only and the only way to change one is still to delete it and retype it, unchanged
|
|
29
|
+
* from before this prop existed.
|
|
30
|
+
*/
|
|
31
|
+
editable?: boolean;
|
|
20
32
|
testId?: string;
|
|
21
33
|
classes?: string;
|
|
22
34
|
};
|
|
23
35
|
export type ChipInputEventProperties = {
|
|
24
36
|
onadd?: (value: string) => void;
|
|
25
37
|
ondismiss?: (value: string) => void;
|
|
26
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Fires after an in-place edit (see `editable`) commits a value that actually changed. Not
|
|
40
|
+
* fired when the edit is cancelled (Escape) or committed with the text unchanged.
|
|
41
|
+
*/
|
|
42
|
+
onedit?: (value: string, previousValue: string) => void;
|
|
43
|
+
/** Fires alongside `onadd`/`ondismiss`/`onedit`, after any of them has already updated `values`. */
|
|
27
44
|
onchange?: (values: string[]) => void;
|
|
28
45
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { CommandMenuProperties, CommandItem } from './properties';
|
|
3
3
|
import { tick, onMount, onDestroy } from 'svelte';
|
|
4
|
+
import { lockBodyScroll, unlockBodyScroll } from '../utils';
|
|
4
5
|
import { SvelteMap } from 'svelte/reactivity';
|
|
5
6
|
import Img from '../Img/Img.svelte';
|
|
6
7
|
import searchSvg from '../assets/search.svg?raw';
|
|
@@ -161,8 +162,12 @@
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// Lock and unlock strictly in this action so the pairing is 1:1. Svelte destroys
|
|
166
|
+
// the action when the node goes away, including on component teardown, so the
|
|
167
|
+
// onDestroy below must NOT unlock as well: with a shared reference count a second
|
|
168
|
+
// decrement would release a lock another open surface still needs.
|
|
164
169
|
function scrollLockAction(_node: HTMLElement) {
|
|
165
|
-
|
|
170
|
+
lockBodyScroll();
|
|
166
171
|
tick().then(() => {
|
|
167
172
|
if (inputElement !== null) {
|
|
168
173
|
inputElement.focus();
|
|
@@ -170,7 +175,7 @@
|
|
|
170
175
|
});
|
|
171
176
|
return {
|
|
172
177
|
destroy() {
|
|
173
|
-
|
|
178
|
+
unlockBodyScroll();
|
|
174
179
|
}
|
|
175
180
|
};
|
|
176
181
|
}
|
|
@@ -181,7 +186,6 @@
|
|
|
181
186
|
|
|
182
187
|
onDestroy(() => {
|
|
183
188
|
if (typeof window !== 'undefined') {
|
|
184
|
-
document.body.style.overflow = '';
|
|
185
189
|
window.removeEventListener('keydown', handleGlobalKeyDown);
|
|
186
190
|
}
|
|
187
191
|
});
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
{@render icon()}
|
|
24
24
|
</div>
|
|
25
25
|
{/if}
|
|
26
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
class="empty-state-title"
|
|
28
|
+
data-pw={typeof testId === 'string' ? `${testId}-title` : null}
|
|
29
|
+
testID={typeof testId === 'string' ? `${testId}-title` : null}
|
|
30
|
+
>
|
|
27
31
|
{#if typeof titleSnippet === 'function'}
|
|
28
32
|
{@render titleSnippet()}
|
|
29
33
|
{:else}
|
|
@@ -31,11 +35,21 @@
|
|
|
31
35
|
{/if}
|
|
32
36
|
</div>
|
|
33
37
|
{#if typeof descriptionSnippet === 'function'}
|
|
34
|
-
<div
|
|
38
|
+
<div
|
|
39
|
+
class="empty-state-description"
|
|
40
|
+
data-pw={typeof testId === 'string' ? `${testId}-description` : null}
|
|
41
|
+
testID={typeof testId === 'string' ? `${testId}-description` : null}
|
|
42
|
+
>
|
|
35
43
|
{@render descriptionSnippet()}
|
|
36
44
|
</div>
|
|
37
45
|
{:else if typeof description === 'string' && description.length > 0}
|
|
38
|
-
<div
|
|
46
|
+
<div
|
|
47
|
+
class="empty-state-description"
|
|
48
|
+
data-pw={typeof testId === 'string' ? `${testId}-description` : null}
|
|
49
|
+
testID={typeof testId === 'string' ? `${testId}-description` : null}
|
|
50
|
+
>
|
|
51
|
+
{description}
|
|
52
|
+
</div>
|
|
39
53
|
{/if}
|
|
40
54
|
{#if typeof children === 'function'}
|
|
41
55
|
<div class="empty-state-actions">
|
package/dist/Input/Input.svelte
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
showRightContentLoader = false,
|
|
21
21
|
expand = $bindable(false),
|
|
22
22
|
preventFocus = false,
|
|
23
|
+
suppressRoleAndTabindex = false,
|
|
23
24
|
leftContent,
|
|
24
25
|
centerContent,
|
|
25
26
|
rightContent,
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
ontopSectionClick,
|
|
32
33
|
onkeydown,
|
|
33
34
|
classes,
|
|
35
|
+
transformSvg,
|
|
34
36
|
role: itemRole,
|
|
35
37
|
ariaSelected,
|
|
36
38
|
id
|
|
@@ -68,8 +70,8 @@
|
|
|
68
70
|
class:prevent-focus={preventFocus}
|
|
69
71
|
onclick={handleItemClick}
|
|
70
72
|
{onkeydown}
|
|
71
|
-
role={itemRole ?? 'button'}
|
|
72
|
-
tabindex={itemRole === 'option' ? -1 : 0}
|
|
73
|
+
role={suppressRoleAndTabindex ? null : (itemRole ?? 'button')}
|
|
74
|
+
tabindex={suppressRoleAndTabindex ? null : itemRole === 'option' ? -1 : 0}
|
|
73
75
|
aria-selected={ariaSelected}
|
|
74
76
|
{id}
|
|
75
77
|
data-pw={testId}
|
|
@@ -80,8 +82,8 @@
|
|
|
80
82
|
class:prevent-focus={preventFocus}
|
|
81
83
|
onclick={handleTopSectionClick}
|
|
82
84
|
{onkeydown}
|
|
83
|
-
role=
|
|
84
|
-
tabindex=
|
|
85
|
+
role={suppressRoleAndTabindex ? null : 'button'}
|
|
86
|
+
tabindex={suppressRoleAndTabindex ? null : 0}
|
|
85
87
|
data-pw={topSectionTestId}
|
|
86
88
|
testID={topSectionTestId}
|
|
87
89
|
>
|
|
@@ -91,12 +93,12 @@
|
|
|
91
93
|
class:prevent-focus={preventFocus}
|
|
92
94
|
onclick={handleLeftImageClick}
|
|
93
95
|
{onkeydown}
|
|
94
|
-
role=
|
|
95
|
-
tabindex=
|
|
96
|
+
role={suppressRoleAndTabindex ? null : 'button'}
|
|
97
|
+
tabindex={suppressRoleAndTabindex ? null : 0}
|
|
96
98
|
data-pw={leftImageTestId}
|
|
97
99
|
testID={leftImageTestId}
|
|
98
100
|
>
|
|
99
|
-
<Img src={leftImageUrl} alt="" fallback={leftImageFallbackUrl} />
|
|
101
|
+
<Img src={leftImageUrl} alt="" fallback={leftImageFallbackUrl} {transformSvg} />
|
|
100
102
|
</div>
|
|
101
103
|
{/if}
|
|
102
104
|
{#if typeof leftContent === 'function'}
|
|
@@ -110,8 +112,8 @@
|
|
|
110
112
|
class:prevent-focus={preventFocus}
|
|
111
113
|
onclick={handleCenterTextClick}
|
|
112
114
|
{onkeydown}
|
|
113
|
-
role=
|
|
114
|
-
tabindex=
|
|
115
|
+
role={suppressRoleAndTabindex ? null : 'button'}
|
|
116
|
+
tabindex={suppressRoleAndTabindex ? null : 0}
|
|
115
117
|
data-pw={centerTextTestId}
|
|
116
118
|
testID={centerTextTestId}
|
|
117
119
|
>
|
|
@@ -132,13 +134,13 @@
|
|
|
132
134
|
class:prevent-focus={preventFocus}
|
|
133
135
|
onclick={handleRightImageClick}
|
|
134
136
|
{onkeydown}
|
|
135
|
-
role=
|
|
136
|
-
tabindex=
|
|
137
|
+
role={suppressRoleAndTabindex ? null : 'button'}
|
|
138
|
+
tabindex={suppressRoleAndTabindex ? null : 0}
|
|
137
139
|
data-pw={rightImageTestId}
|
|
138
140
|
testID={rightImageTestId}
|
|
139
141
|
>
|
|
140
142
|
<div class="right-img-wrapper">
|
|
141
|
-
<Img src={rightImageUrl} alt="" />
|
|
143
|
+
<Img src={rightImageUrl} alt="" {transformSvg} />
|
|
142
144
|
</div>
|
|
143
145
|
</div>
|
|
144
146
|
{/if}
|
|
@@ -3,6 +3,8 @@ export type ListItemProperties = ListItemEventProperties & {
|
|
|
3
3
|
leftImageUrl?: string | null;
|
|
4
4
|
leftImageFallbackUrl?: string | null;
|
|
5
5
|
rightImageUrl?: string | null;
|
|
6
|
+
/** Rewrites SVG markup before either image is inlined. Providing it enables SVG inlining. */
|
|
7
|
+
transformSvg?: (svg: string) => string;
|
|
6
8
|
label?: string | null;
|
|
7
9
|
useAccordion?: boolean;
|
|
8
10
|
rightContentText?: string | null;
|
|
@@ -15,6 +17,11 @@ export type ListItemProperties = ListItemEventProperties & {
|
|
|
15
17
|
showRightContentLoader?: boolean;
|
|
16
18
|
expand?: boolean;
|
|
17
19
|
preventFocus?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Removes ListItem's synthetic roles and tab stops while retaining its mouse handlers.
|
|
22
|
+
* Opt in when an ancestor or consumer supplies the semantic interactive control.
|
|
23
|
+
*/
|
|
24
|
+
suppressRoleAndTabindex?: boolean;
|
|
18
25
|
leftContent?: Snippet;
|
|
19
26
|
centerContent?: Snippet;
|
|
20
27
|
rightContent?: Snippet;
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
onopen,
|
|
16
16
|
onclose,
|
|
17
17
|
classes,
|
|
18
|
+
transformSvg,
|
|
18
19
|
selectedValue = null,
|
|
19
20
|
role: menuRole = 'menu',
|
|
20
21
|
ariaLabel: menuAriaLabel,
|
|
@@ -474,7 +475,7 @@
|
|
|
474
475
|
<!-- Inline the SVG so currentColor strokes/fills inherit the item's
|
|
475
476
|
text colour (danger items tint their icon red, dark theme works);
|
|
476
477
|
non-SVG URLs fall back to the plain <img> render automatically. -->
|
|
477
|
-
<Img inlineSvg src={item.icon} alt="" fallback="" />
|
|
478
|
+
<Img inlineSvg src={item.icon} alt="" fallback="" {transformSvg} />
|
|
478
479
|
</span>
|
|
479
480
|
{/if}
|
|
480
481
|
<span class="menu-item-label">{item.label}</span>
|
|
@@ -52,6 +52,8 @@ export type OptionalMenuProperties = {
|
|
|
52
52
|
*/
|
|
53
53
|
interactiveTrigger?: boolean;
|
|
54
54
|
classes?: string;
|
|
55
|
+
/** Rewrites each item icon's SVG markup before it is inlined. */
|
|
56
|
+
transformSvg?: (svg: string) => string;
|
|
55
57
|
/** Value of the currently selected item. When set, opening the menu focuses the
|
|
56
58
|
* selected option instead of the first item, the matching item gets the
|
|
57
59
|
* `menu-item-selected` class (stylable via --menu-item-selected-*), and
|
package/dist/Modal/Modal.svelte
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { onMount, onDestroy } from 'svelte';
|
|
4
4
|
import ModalAnimation from '../Animations/ModalAnimation.svelte';
|
|
5
5
|
import OverlayAnimation from '../Animations/OverlayAnimation.svelte';
|
|
6
|
-
import { createDebouncer } from '../utils';
|
|
6
|
+
import { createDebouncer, lockBodyScroll, unlockBodyScroll } from '../utils';
|
|
7
7
|
import Button from '../Button/Button.svelte';
|
|
8
8
|
import Img from '../Img/Img.svelte';
|
|
9
9
|
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
|
|
123
123
|
onMount(() => {
|
|
124
124
|
if (lockScroll) {
|
|
125
|
-
|
|
125
|
+
lockBodyScroll();
|
|
126
126
|
}
|
|
127
127
|
if (typeof autoDismissAfter === 'number') {
|
|
128
128
|
dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
}
|
|
140
140
|
if (typeof window !== 'undefined') {
|
|
141
141
|
if (lockScroll) {
|
|
142
|
-
|
|
142
|
+
unlockBodyScroll();
|
|
143
143
|
}
|
|
144
144
|
if (supportHardwareBackPress) {
|
|
145
145
|
if (!backPressed) {
|
package/dist/Pill/Pill.svelte
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
dismissible = false,
|
|
9
9
|
disabled = false,
|
|
10
10
|
testId,
|
|
11
|
+
title,
|
|
11
12
|
dismissIcon,
|
|
12
13
|
leadingIcon,
|
|
13
14
|
onclick,
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
tabindex={interactive ? 0 : null}
|
|
53
54
|
aria-disabled={interactive && disabled ? true : null}
|
|
54
55
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
56
|
+
title={title ?? null}
|
|
55
57
|
testID={typeof testId === 'string' ? testId : null}
|
|
56
58
|
>
|
|
57
59
|
{#if typeof leadingIcon === 'function'}
|