@juspay/svelte-ui-components 2.80.2 → 2.80.4
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/Banner/Banner.svelte +1 -1
- package/dist/BarChart/BarChart.svelte +2 -1
- package/dist/BarChart/properties.d.ts +6 -0
- package/dist/Button/Button.svelte +150 -34
- package/dist/Button/properties.d.ts +40 -1
- package/dist/Combobox/Combobox.svelte +275 -27
- package/dist/Combobox/Combobox.svelte.d.ts +1 -1
- package/dist/Combobox/properties.d.ts +36 -0
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +62 -9
- package/dist/DualAxisBarChart/properties.d.ts +32 -1
- package/dist/FunnelChart/FunnelChart.svelte +3 -1
- package/dist/FunnelChart/properties.d.ts +7 -0
- package/dist/Input/Input.svelte +59 -1
- package/dist/Input/properties.d.ts +18 -0
- package/dist/LineChart/LineChart.svelte +9 -1
- package/dist/LineChart/properties.d.ts +4 -0
- package/dist/Pill/Pill.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +4 -2
- package/dist/SankeyChart/properties.d.ts +7 -0
- package/dist/Select/Select.svelte +34 -1
- package/dist/Select/properties.d.ts +7 -0
- package/dist/_chart/ChartTooltip.svelte +1 -1
- package/dist/_chart/Legend.svelte +1 -1
- package/dist/_chart/geometry.js +32 -1
- package/dist/_chart/types.d.ts +10 -0
- package/dist/_chart/types.js +11 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, tick } from 'svelte';
|
|
3
3
|
import Input from '../Input/Input.svelte';
|
|
4
|
+
import Pill from '../Pill/Pill.svelte';
|
|
4
5
|
import type { ComboboxItem, ComboboxProperties } from './properties';
|
|
5
6
|
|
|
6
7
|
function defaultFilter(item: ComboboxItem, query: string): boolean {
|
|
@@ -29,13 +30,27 @@
|
|
|
29
30
|
inputSuffix,
|
|
30
31
|
dropdownHeader,
|
|
31
32
|
dropdownFooter,
|
|
33
|
+
// multi-select + create/action
|
|
34
|
+
multiple = false,
|
|
35
|
+
selected = $bindable([]),
|
|
36
|
+
maxSelected,
|
|
37
|
+
maxSelectedText,
|
|
38
|
+
pillSnippet,
|
|
39
|
+
allowCreate = false,
|
|
40
|
+
createLabel = (query: string) => `Create "${query}"`,
|
|
41
|
+
action,
|
|
42
|
+
actionIcon,
|
|
32
43
|
onselect,
|
|
33
44
|
oninput,
|
|
34
45
|
onopen,
|
|
35
46
|
onclose,
|
|
36
47
|
onkeydown,
|
|
37
48
|
onfocus,
|
|
38
|
-
onblur
|
|
49
|
+
onblur,
|
|
50
|
+
onchange,
|
|
51
|
+
onadd,
|
|
52
|
+
onremove,
|
|
53
|
+
oncreate
|
|
39
54
|
}: ComboboxProperties = $props();
|
|
40
55
|
|
|
41
56
|
let containerEl: HTMLDivElement | null = $state(null);
|
|
@@ -45,20 +60,63 @@
|
|
|
45
60
|
return inputRef?.getInputRef() ?? null;
|
|
46
61
|
}
|
|
47
62
|
|
|
63
|
+
function focusInput(): void {
|
|
64
|
+
inputRef?.getInputRef()?.focus();
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
const listboxId = `combobox-listbox-${Math.random().toString(36).slice(2, 9)}`;
|
|
49
68
|
|
|
69
|
+
let selectedSet = $derived(new Set(selected));
|
|
70
|
+
let trimmedQuery = $derived(inputValue.trim());
|
|
71
|
+
|
|
50
72
|
let filteredItems: ComboboxItem[] = $derived(
|
|
51
|
-
|
|
73
|
+
items.filter((item) => {
|
|
74
|
+
if (multiple && selectedSet.has(item.id)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return inputValue.length > 0 ? filterFn(item, inputValue) : true;
|
|
78
|
+
})
|
|
52
79
|
);
|
|
53
80
|
|
|
54
81
|
let selectableItems: ComboboxItem[] = $derived(
|
|
55
82
|
filteredItems.filter((item) => item.disabled !== true)
|
|
56
83
|
);
|
|
57
84
|
|
|
85
|
+
let exactMatch: ComboboxItem | null = $derived(
|
|
86
|
+
items.find((item) => item.label.toLowerCase() === trimmedQuery.toLowerCase()) ?? null
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
let atLimit = $derived(
|
|
90
|
+
multiple && typeof maxSelected === 'number' && selected.length >= maxSelected
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
let limitText = $derived(
|
|
94
|
+
maxSelectedText ??
|
|
95
|
+
(typeof maxSelected === 'number'
|
|
96
|
+
? `You can select up to ${maxSelected}.`
|
|
97
|
+
: 'Selection limit reached.')
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
let showCreate = $derived(
|
|
101
|
+
allowCreate &&
|
|
102
|
+
!atLimit &&
|
|
103
|
+
trimmedQuery !== '' &&
|
|
104
|
+
exactMatch === null &&
|
|
105
|
+
!(multiple && selectedSet.has(trimmedQuery))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Navigable rows: selectable options (hidden at the limit) → create → action.
|
|
109
|
+
let navItemCount = $derived(atLimit ? 0 : selectableItems.length);
|
|
110
|
+
let createNavIndex = $derived(showCreate ? navItemCount : -1);
|
|
111
|
+
let actionNavIndex = $derived(action ? navItemCount + (showCreate ? 1 : 0) : -1);
|
|
112
|
+
let navCount = $derived(navItemCount + (showCreate ? 1 : 0) + (action ? 1 : 0));
|
|
113
|
+
|
|
58
114
|
let highlightedOptionId: string | null = $derived(
|
|
59
115
|
highlightedIndex >= 0 ? `${listboxId}-option-${highlightedIndex}` : null
|
|
60
116
|
);
|
|
61
117
|
|
|
118
|
+
const labelOf = (id: string): string => items.find((item) => item.id === id)?.label ?? id;
|
|
119
|
+
|
|
62
120
|
function openDropdown() {
|
|
63
121
|
if (disabled || open) {
|
|
64
122
|
return;
|
|
@@ -77,37 +135,75 @@
|
|
|
77
135
|
onclose?.();
|
|
78
136
|
}
|
|
79
137
|
|
|
138
|
+
function emitChange(): void {
|
|
139
|
+
onchange?.([...selected]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function addValue(id: string): void {
|
|
143
|
+
if (atLimit || selectedSet.has(id)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
selected = [...selected, id];
|
|
147
|
+
inputValue = '';
|
|
148
|
+
highlightedIndex = -1;
|
|
149
|
+
onadd?.(id);
|
|
150
|
+
emitChange();
|
|
151
|
+
focusInput();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function removeValue(id: string): void {
|
|
155
|
+
if (!selectedSet.has(id)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
selected = selected.filter((current) => current !== id);
|
|
159
|
+
onremove?.(id);
|
|
160
|
+
emitChange();
|
|
161
|
+
}
|
|
162
|
+
|
|
80
163
|
function selectItem(item: ComboboxItem) {
|
|
81
164
|
if (item.disabled === true) {
|
|
82
165
|
return;
|
|
83
166
|
}
|
|
167
|
+
onselect?.(item);
|
|
168
|
+
if (multiple) {
|
|
169
|
+
addValue(item.id);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
84
172
|
value = item.id;
|
|
85
173
|
inputValue = item.label;
|
|
86
|
-
onselect?.(item);
|
|
87
174
|
closeDropdown();
|
|
88
175
|
}
|
|
89
176
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
177
|
+
function create(): void {
|
|
178
|
+
const created = trimmedQuery;
|
|
179
|
+
if (created === '') {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
oncreate?.(created);
|
|
183
|
+
if (multiple) {
|
|
184
|
+
addValue(created);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
value = created;
|
|
188
|
+
inputValue = created;
|
|
189
|
+
closeDropdown();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runAction(): void {
|
|
193
|
+
action?.onClick();
|
|
194
|
+
if (!action?.keepOpen) {
|
|
195
|
+
closeDropdown();
|
|
99
196
|
}
|
|
100
|
-
return -1;
|
|
101
197
|
}
|
|
102
198
|
|
|
103
199
|
async function moveHighlight(delta: number): Promise<void> {
|
|
104
|
-
if (
|
|
200
|
+
if (navCount === 0) {
|
|
105
201
|
return;
|
|
106
202
|
}
|
|
107
203
|
let next = highlightedIndex + delta;
|
|
108
204
|
if (next < 0) {
|
|
109
|
-
next =
|
|
110
|
-
} else if (next >=
|
|
205
|
+
next = navCount - 1;
|
|
206
|
+
} else if (next >= navCount) {
|
|
111
207
|
next = 0;
|
|
112
208
|
}
|
|
113
209
|
highlightedIndex = next;
|
|
@@ -121,19 +217,38 @@
|
|
|
121
217
|
}
|
|
122
218
|
|
|
123
219
|
function selectHighlighted() {
|
|
124
|
-
if (highlightedIndex < 0 || highlightedIndex >=
|
|
220
|
+
if (highlightedIndex < 0 || highlightedIndex >= navCount) {
|
|
125
221
|
return;
|
|
126
222
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
223
|
+
if (highlightedIndex < navItemCount) {
|
|
224
|
+
const item = selectableItems.at(highlightedIndex);
|
|
225
|
+
if (item) {
|
|
226
|
+
selectItem(item);
|
|
227
|
+
}
|
|
228
|
+
} else if (highlightedIndex === createNavIndex) {
|
|
229
|
+
create();
|
|
230
|
+
} else if (highlightedIndex === actionNavIndex) {
|
|
231
|
+
runAction();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getFilteredSelectableIndex(item: ComboboxItem): number {
|
|
236
|
+
let selectableIdx = 0;
|
|
237
|
+
for (let i = 0; i < filteredItems.length; i++) {
|
|
238
|
+
if (filteredItems[i] === item) {
|
|
239
|
+
return filteredItems[i].disabled === true ? -1 : selectableIdx;
|
|
240
|
+
}
|
|
241
|
+
if (filteredItems[i].disabled !== true) {
|
|
242
|
+
selectableIdx++;
|
|
243
|
+
}
|
|
130
244
|
}
|
|
245
|
+
return -1;
|
|
131
246
|
}
|
|
132
247
|
|
|
133
|
-
function handleInput(val: string,
|
|
248
|
+
function handleInput(val: string, event: Event) {
|
|
134
249
|
inputValue = val;
|
|
135
250
|
oninput?.(val);
|
|
136
|
-
inputEventProperties?.onInput?.(val,
|
|
251
|
+
inputEventProperties?.onInput?.(val, event);
|
|
137
252
|
if (!open) {
|
|
138
253
|
openDropdown();
|
|
139
254
|
}
|
|
@@ -167,6 +282,17 @@
|
|
|
167
282
|
if (open && highlightedIndex >= 0) {
|
|
168
283
|
event.preventDefault();
|
|
169
284
|
selectHighlighted();
|
|
285
|
+
} else if (exactMatch && !(multiple && selectedSet.has(exactMatch.id))) {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
selectItem(exactMatch);
|
|
288
|
+
} else if (showCreate) {
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
create();
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
case 'Backspace':
|
|
294
|
+
if (multiple && inputValue === '' && selected.length > 0) {
|
|
295
|
+
removeValue(selected[selected.length - 1]);
|
|
170
296
|
}
|
|
171
297
|
break;
|
|
172
298
|
case 'Escape':
|
|
@@ -194,6 +320,13 @@
|
|
|
194
320
|
inputEventProperties?.onBlur?.(event);
|
|
195
321
|
}
|
|
196
322
|
|
|
323
|
+
function handleControlClick() {
|
|
324
|
+
if (multiple && !disabled) {
|
|
325
|
+
focusInput();
|
|
326
|
+
openDropdown();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
197
330
|
function handleClickOutside(event: Event) {
|
|
198
331
|
if (
|
|
199
332
|
event.target instanceof Node &&
|
|
@@ -213,16 +346,26 @@
|
|
|
213
346
|
</script>
|
|
214
347
|
|
|
215
348
|
<div class="combobox {classes ?? ''}" class:disabled bind:this={containerEl} data-pw={testId}>
|
|
216
|
-
|
|
349
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
350
|
+
<div class="combobox-input-wrapper" class:multiple onclick={handleControlClick}>
|
|
217
351
|
{#if typeof inputPrefix === 'function'}
|
|
218
352
|
<div class="combobox-input-prefix">{@render inputPrefix()}</div>
|
|
219
353
|
{/if}
|
|
354
|
+
{#if multiple}
|
|
355
|
+
{#each selected as id (id)}
|
|
356
|
+
{#if typeof pillSnippet === 'function'}
|
|
357
|
+
{@render pillSnippet(id, () => !disabled && removeValue(id), disabled)}
|
|
358
|
+
{:else}
|
|
359
|
+
<Pill text={labelOf(id)} dismissible {disabled} ondismiss={() => removeValue(id)} />
|
|
360
|
+
{/if}
|
|
361
|
+
{/each}
|
|
362
|
+
{/if}
|
|
220
363
|
<div class="combobox-input">
|
|
221
364
|
<Input
|
|
222
365
|
{...inputProperties}
|
|
223
366
|
bind:value={inputValue}
|
|
224
367
|
bind:this={inputRef}
|
|
225
|
-
{placeholder}
|
|
368
|
+
placeholder={multiple && selected.length > 0 ? '' : placeholder}
|
|
226
369
|
{name}
|
|
227
370
|
disable={disabled}
|
|
228
371
|
autoComplete="off"
|
|
@@ -249,7 +392,10 @@
|
|
|
249
392
|
{#if typeof dropdownHeader === 'function'}
|
|
250
393
|
<div class="combobox-dropdown-header">{@render dropdownHeader()}</div>
|
|
251
394
|
{/if}
|
|
252
|
-
|
|
395
|
+
|
|
396
|
+
{#if atLimit}
|
|
397
|
+
<div class="combobox-limit" role="alert">{limitText}</div>
|
|
398
|
+
{:else if filteredItems.length === 0 && !showCreate && !action}
|
|
253
399
|
{#if typeof emptySnippet === 'function'}
|
|
254
400
|
{@render emptySnippet()}
|
|
255
401
|
{:else}
|
|
@@ -263,11 +409,11 @@
|
|
|
263
409
|
<div
|
|
264
410
|
class="combobox-option"
|
|
265
411
|
class:highlighted={isHighlighted}
|
|
266
|
-
class:selected={item.id === value}
|
|
412
|
+
class:selected={!multiple && item.id === value}
|
|
267
413
|
class:combobox-option-disabled={item.disabled === true}
|
|
268
414
|
role="option"
|
|
269
415
|
id={`${listboxId}-option-${selectableIndex}`}
|
|
270
|
-
aria-selected={item.id === value}
|
|
416
|
+
aria-selected={!multiple && item.id === value}
|
|
271
417
|
aria-disabled={item.disabled === true ? 'true' : null}
|
|
272
418
|
tabindex="-1"
|
|
273
419
|
onclick={() => selectItem(item)}
|
|
@@ -286,6 +432,56 @@
|
|
|
286
432
|
</div>
|
|
287
433
|
{/each}
|
|
288
434
|
{/if}
|
|
435
|
+
|
|
436
|
+
{#if showCreate}
|
|
437
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
438
|
+
<div
|
|
439
|
+
class="combobox-option combobox-create"
|
|
440
|
+
class:highlighted={highlightedIndex === createNavIndex}
|
|
441
|
+
class:with-divider={navItemCount > 0}
|
|
442
|
+
role="option"
|
|
443
|
+
id={`${listboxId}-option-${createNavIndex}`}
|
|
444
|
+
aria-selected="false"
|
|
445
|
+
tabindex="-1"
|
|
446
|
+
onclick={() => create()}
|
|
447
|
+
onmouseenter={() => (highlightedIndex = createNavIndex)}
|
|
448
|
+
data-pw={typeof testId === 'string' ? `${testId}-create` : null}
|
|
449
|
+
>
|
|
450
|
+
<span class="combobox-create-icon" aria-hidden="true">
|
|
451
|
+
<svg viewBox="0 0 20 20" width="14" height="14" fill="none">
|
|
452
|
+
<path
|
|
453
|
+
d="M10 4v12M4 10h12"
|
|
454
|
+
stroke="currentColor"
|
|
455
|
+
stroke-width="1.7"
|
|
456
|
+
stroke-linecap="round"
|
|
457
|
+
/>
|
|
458
|
+
</svg>
|
|
459
|
+
</span>
|
|
460
|
+
{createLabel(trimmedQuery)}
|
|
461
|
+
</div>
|
|
462
|
+
{/if}
|
|
463
|
+
|
|
464
|
+
{#if action}
|
|
465
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
466
|
+
<div
|
|
467
|
+
class="combobox-option combobox-action"
|
|
468
|
+
class:highlighted={highlightedIndex === actionNavIndex}
|
|
469
|
+
class:with-divider={navItemCount > 0 || showCreate}
|
|
470
|
+
role="option"
|
|
471
|
+
id={`${listboxId}-option-${actionNavIndex}`}
|
|
472
|
+
aria-selected="false"
|
|
473
|
+
tabindex="-1"
|
|
474
|
+
onclick={() => runAction()}
|
|
475
|
+
onmouseenter={() => (highlightedIndex = actionNavIndex)}
|
|
476
|
+
data-pw={typeof testId === 'string' ? `${testId}-action` : null}
|
|
477
|
+
>
|
|
478
|
+
{#if typeof actionIcon === 'function'}
|
|
479
|
+
<span class="combobox-action-icon" aria-hidden="true">{@render actionIcon()}</span>
|
|
480
|
+
{/if}
|
|
481
|
+
{action.label}
|
|
482
|
+
</div>
|
|
483
|
+
{/if}
|
|
484
|
+
|
|
289
485
|
{#if typeof dropdownFooter === 'function'}
|
|
290
486
|
<div class="combobox-dropdown-footer">{@render dropdownFooter()}</div>
|
|
291
487
|
{/if}
|
|
@@ -317,6 +513,14 @@
|
|
|
317
513
|
transition: var(--combobox-input-transition, border-color 0.15s, box-shadow 0.15s);
|
|
318
514
|
}
|
|
319
515
|
|
|
516
|
+
/* Multi-select control: pills wrap above the typeahead input. */
|
|
517
|
+
.combobox-input-wrapper.multiple {
|
|
518
|
+
flex-wrap: wrap;
|
|
519
|
+
gap: var(--combobox-pill-gap, 4px);
|
|
520
|
+
padding: var(--combobox-multiple-padding, 4px 6px);
|
|
521
|
+
cursor: text;
|
|
522
|
+
}
|
|
523
|
+
|
|
320
524
|
.combobox-input-wrapper:hover {
|
|
321
525
|
border-color: var(--combobox-input-hover-border-color, #999999);
|
|
322
526
|
}
|
|
@@ -357,6 +561,12 @@
|
|
|
357
561
|
--input-radius: 0;
|
|
358
562
|
}
|
|
359
563
|
|
|
564
|
+
.combobox-input-wrapper.multiple .combobox-input {
|
|
565
|
+
flex: 1 1 60px;
|
|
566
|
+
min-width: 60px;
|
|
567
|
+
--input-padding: var(--combobox-multiple-input-padding, 2px 4px);
|
|
568
|
+
}
|
|
569
|
+
|
|
360
570
|
.combobox-input::placeholder {
|
|
361
571
|
color: var(--combobox-placeholder-color, #999999);
|
|
362
572
|
}
|
|
@@ -378,6 +588,9 @@
|
|
|
378
588
|
}
|
|
379
589
|
|
|
380
590
|
.combobox-option {
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
gap: var(--combobox-option-gap, 8px);
|
|
381
594
|
padding: var(--combobox-option-padding, 8px 12px);
|
|
382
595
|
color: var(--combobox-option-color, #333333);
|
|
383
596
|
font-size: var(--combobox-option-font-size, inherit);
|
|
@@ -414,6 +627,41 @@
|
|
|
414
627
|
pointer-events: none;
|
|
415
628
|
}
|
|
416
629
|
|
|
630
|
+
.combobox-create {
|
|
631
|
+
color: var(--combobox-create-color, #2563eb);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.combobox-action {
|
|
635
|
+
color: var(--combobox-action-color, #374151);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.combobox-option.with-divider {
|
|
639
|
+
border-top: var(--combobox-divider, 1px solid #e5e7eb);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.combobox-create-icon,
|
|
643
|
+
.combobox-action-icon {
|
|
644
|
+
display: inline-flex;
|
|
645
|
+
align-items: center;
|
|
646
|
+
flex-shrink: 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.combobox-action-icon :global(svg) {
|
|
650
|
+
width: 14px;
|
|
651
|
+
height: 14px;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.combobox-limit {
|
|
655
|
+
display: flex;
|
|
656
|
+
align-items: center;
|
|
657
|
+
gap: 6px;
|
|
658
|
+
padding: var(--combobox-limit-padding, 8px 12px);
|
|
659
|
+
font-size: var(--combobox-limit-font-size, 13px);
|
|
660
|
+
font-weight: 500;
|
|
661
|
+
color: var(--combobox-limit-color, #b45309);
|
|
662
|
+
background: var(--combobox-limit-background, #fffbeb);
|
|
663
|
+
}
|
|
664
|
+
|
|
417
665
|
.combobox-dropdown-header {
|
|
418
666
|
border-bottom: var(--combobox-dropdown-header-border, none);
|
|
419
667
|
padding: var(--combobox-dropdown-header-padding, 0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComboboxProperties } from './properties';
|
|
2
2
|
declare const Combobox: import("svelte").Component<ComboboxProperties, {
|
|
3
3
|
getInputRef: () => HTMLInputElement | HTMLTextAreaElement | null;
|
|
4
|
-
}, "value" | "open" | "inputValue" | "highlightedIndex">;
|
|
4
|
+
}, "value" | "open" | "selected" | "inputValue" | "highlightedIndex">;
|
|
5
5
|
type Combobox = ReturnType<typeof Combobox>;
|
|
6
6
|
export default Combobox;
|
|
@@ -5,6 +5,13 @@ export type ComboboxItem = {
|
|
|
5
5
|
label: string;
|
|
6
6
|
disabled?: boolean;
|
|
7
7
|
};
|
|
8
|
+
/** A persistent custom action row rendered at the foot of the dropdown. */
|
|
9
|
+
export type ComboboxAction = {
|
|
10
|
+
label: string;
|
|
11
|
+
onClick: () => void;
|
|
12
|
+
/** Keep the dropdown open after the action runs. Defaults to `false`. */
|
|
13
|
+
keepOpen?: boolean;
|
|
14
|
+
};
|
|
8
15
|
export type ComboboxProperties = MandatoryComboboxProperties & OptionalComboboxProperties & ComboboxEventProperties;
|
|
9
16
|
export type MandatoryComboboxProperties = {
|
|
10
17
|
items: ComboboxItem[];
|
|
@@ -30,6 +37,27 @@ export type OptionalComboboxProperties = {
|
|
|
30
37
|
inputSuffix?: Snippet;
|
|
31
38
|
dropdownHeader?: Snippet;
|
|
32
39
|
dropdownFooter?: Snippet;
|
|
40
|
+
/** Enable multi-select: picked options become removable pills inside the control. */
|
|
41
|
+
multiple?: boolean;
|
|
42
|
+
/** Bindable array of selected ids (multi-select mode). */
|
|
43
|
+
selected?: string[];
|
|
44
|
+
/** Cap the number of selections (multi-select mode). */
|
|
45
|
+
maxSelected?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Message shown in the dropdown once `maxSelected` is reached (option/create rows are hidden).
|
|
48
|
+
* Defaults to "You can select up to {maxSelected}.".
|
|
49
|
+
*/
|
|
50
|
+
maxSelectedText?: string;
|
|
51
|
+
/** Custom pill renderer; receives `(value, remove, disabled)`. */
|
|
52
|
+
pillSnippet?: Snippet<[string, () => void, boolean]>;
|
|
53
|
+
/** Show a "Create …" row when the query has no exact match. Default `false`. */
|
|
54
|
+
allowCreate?: boolean;
|
|
55
|
+
/** Build the create-row label from the current query. */
|
|
56
|
+
createLabel?: (query: string) => string;
|
|
57
|
+
/** A persistent custom action row shown at the foot of the dropdown. */
|
|
58
|
+
action?: ComboboxAction;
|
|
59
|
+
/** Custom leading icon for the persistent action row. */
|
|
60
|
+
actionIcon?: Snippet;
|
|
33
61
|
};
|
|
34
62
|
export type ComboboxEventProperties = {
|
|
35
63
|
onselect?: (item: ComboboxItem) => void;
|
|
@@ -39,4 +67,12 @@ export type ComboboxEventProperties = {
|
|
|
39
67
|
onkeydown?: (event: KeyboardEvent) => void;
|
|
40
68
|
onfocus?: (event: FocusEvent) => void;
|
|
41
69
|
onblur?: (event: FocusEvent) => void;
|
|
70
|
+
/** Multi-select: fires whenever the selection changes (add, remove, or create). */
|
|
71
|
+
onchange?: (selected: string[]) => void;
|
|
72
|
+
/** Multi-select: fires when a value is added. */
|
|
73
|
+
onadd?: (value: string) => void;
|
|
74
|
+
/** Multi-select: fires when a value is removed. */
|
|
75
|
+
onremove?: (value: string) => void;
|
|
76
|
+
/** Fires when the create row is chosen, with the trimmed query. */
|
|
77
|
+
oncreate?: (value: string) => void;
|
|
42
78
|
};
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { formatNumber } from '../_chart/format';
|
|
15
15
|
import { roundedRectPath, linePath } from '../_chart/paths';
|
|
16
16
|
import type { LegendItem, TooltipData, LinearScale, BandScale, Point } from '../_chart/types';
|
|
17
|
+
import { DEFAULT_CHART_CORNER_RADIUS } from '../_chart/types';
|
|
17
18
|
|
|
18
19
|
// ── Per-instance uid for SVG <defs> ids ────────────────────────
|
|
19
20
|
const uid = Math.random().toString(36).slice(2, 9);
|
|
@@ -27,9 +28,12 @@
|
|
|
27
28
|
rightAxis = {},
|
|
28
29
|
showGridlines = true,
|
|
29
30
|
showLegend = true,
|
|
30
|
-
barRadius =
|
|
31
|
+
barRadius = DEFAULT_CHART_CORNER_RADIUS,
|
|
31
32
|
barPadding = 0.25,
|
|
32
33
|
aspectRatio = 16 / 9,
|
|
34
|
+
minBarHeight = 2,
|
|
35
|
+
margin,
|
|
36
|
+
tooltipPortal = false,
|
|
33
37
|
tooltipSnippet,
|
|
34
38
|
onbarclick,
|
|
35
39
|
testId,
|
|
@@ -44,6 +48,8 @@
|
|
|
44
48
|
let hoveredCategoryIndex = $state<number | null>(null);
|
|
45
49
|
let mouseX = $state(0);
|
|
46
50
|
let mouseY = $state(0);
|
|
51
|
+
let mouseClientX = $state(0);
|
|
52
|
+
let mouseClientY = $state(0);
|
|
47
53
|
|
|
48
54
|
// ── Formatters ─────────────────────────────────────────────────
|
|
49
55
|
|
|
@@ -53,7 +59,13 @@
|
|
|
53
59
|
// ── Layout — wider right margin to accommodate right-axis labels ─
|
|
54
60
|
|
|
55
61
|
const dims = $derived(
|
|
56
|
-
computeChartDimensions(chartWidth, chartHeight, {
|
|
62
|
+
computeChartDimensions(chartWidth, chartHeight, {
|
|
63
|
+
top: 24,
|
|
64
|
+
right: 56,
|
|
65
|
+
bottom: 40,
|
|
66
|
+
left: 56,
|
|
67
|
+
...margin
|
|
68
|
+
})
|
|
57
69
|
);
|
|
58
70
|
|
|
59
71
|
// ── Scales ─────────────────────────────────────────────────────
|
|
@@ -152,7 +164,7 @@
|
|
|
152
164
|
const barX = bandStart + subIndex * subBandWidth + barGap;
|
|
153
165
|
const valueY = scale(value);
|
|
154
166
|
const barY = value >= 0 ? valueY : zeroY;
|
|
155
|
-
const barHeight = Math.max(
|
|
167
|
+
const barHeight = Math.max(minBarHeight, Math.abs(valueY - zeroY));
|
|
156
168
|
|
|
157
169
|
const path = roundedRectPath(barX, barY, barW, barHeight, barRadius, barRadius, 0, 0);
|
|
158
170
|
|
|
@@ -278,6 +290,22 @@
|
|
|
278
290
|
const rect = containerEl.getBoundingClientRect();
|
|
279
291
|
mouseX = event.clientX - rect.left;
|
|
280
292
|
mouseY = event.clientY - rect.top;
|
|
293
|
+
mouseClientX = event.clientX;
|
|
294
|
+
mouseClientY = event.clientY;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Svelte action: relocates the tooltip layer to `document.body` so a `position:fixed`
|
|
299
|
+
* tooltip is never clipped by an `overflow`/scroll ancestor. Used only when
|
|
300
|
+
* `tooltipPortal` is set; `use:` actions never run during SSR.
|
|
301
|
+
*/
|
|
302
|
+
const portalToBody = (node: HTMLElement) => {
|
|
303
|
+
document.body.appendChild(node);
|
|
304
|
+
return {
|
|
305
|
+
destroy: () => {
|
|
306
|
+
node.remove();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
281
309
|
};
|
|
282
310
|
|
|
283
311
|
const handleCategoryEnter = (event: MouseEvent, catIdx: number) => {
|
|
@@ -439,12 +467,31 @@
|
|
|
439
467
|
</ChartContainer>
|
|
440
468
|
|
|
441
469
|
<!-- Tooltip -->
|
|
442
|
-
{#if
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
470
|
+
{#if hoveredCategoryIndex !== null}
|
|
471
|
+
{#if tooltipPortal}
|
|
472
|
+
<!-- Portaled to <body> with fixed viewport coords so the tooltip is never
|
|
473
|
+
clipped by an overflow/scroll ancestor (e.g. a scrollable report sheet).
|
|
474
|
+
The inner elements keep their own +12/-12 offset relative to this layer. -->
|
|
475
|
+
<div
|
|
476
|
+
class="chart-tooltip-portal"
|
|
477
|
+
style="left: {mouseClientX}px; top: {mouseClientY}px;"
|
|
478
|
+
use:portalToBody
|
|
479
|
+
>
|
|
480
|
+
{#if typeof tooltipSnippet === 'function'}
|
|
481
|
+
<div class="chart-tooltip-slot" style="left: 12px; top: -12px;">
|
|
482
|
+
{@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
|
|
483
|
+
</div>
|
|
484
|
+
{:else}
|
|
485
|
+
<ChartTooltip data={tooltipData} mouseX={0} mouseY={0} />
|
|
486
|
+
{/if}
|
|
487
|
+
</div>
|
|
488
|
+
{:else if typeof tooltipSnippet === 'function'}
|
|
489
|
+
<div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
|
|
490
|
+
{@render tooltipSnippet(buildTooltipContext(hoveredCategoryIndex))}
|
|
491
|
+
</div>
|
|
492
|
+
{:else}
|
|
493
|
+
<ChartTooltip data={tooltipData} {mouseX} {mouseY} />
|
|
494
|
+
{/if}
|
|
448
495
|
{/if}
|
|
449
496
|
{:else}
|
|
450
497
|
<div class="chart-empty">No data available.</div>
|
|
@@ -513,6 +560,12 @@
|
|
|
513
560
|
pointer-events: none;
|
|
514
561
|
}
|
|
515
562
|
|
|
563
|
+
.chart-tooltip-portal {
|
|
564
|
+
position: fixed;
|
|
565
|
+
z-index: var(--chart-tooltip-z-index, 10);
|
|
566
|
+
pointer-events: none;
|
|
567
|
+
}
|
|
568
|
+
|
|
516
569
|
.chart-empty {
|
|
517
570
|
padding: var(--chart-empty-padding, 32px 24px);
|
|
518
571
|
color: var(--chart-empty-color, #9ca3af);
|
|
@@ -77,7 +77,12 @@ export type OptionalDualAxisBarChartProperties = {
|
|
|
77
77
|
showGridlines?: boolean;
|
|
78
78
|
/** Whether to render the shared legend below the chart. Default `true`. */
|
|
79
79
|
showLegend?: boolean;
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Corner radius on column/bar shapes in pixels. Defaults to
|
|
82
|
+
* `DEFAULT_CHART_CORNER_RADIUS` (4), mirroring the design system's base
|
|
83
|
+
* `--radius` token. SVG `rx`/`ry` cannot read CSS `var()`, so pass this
|
|
84
|
+
* prop explicitly to track a changed `--radius` at runtime.
|
|
85
|
+
*/
|
|
81
86
|
barRadius?: number;
|
|
82
87
|
/** Padding between category bands as a fraction of band width (0–1). Default `0.25`. */
|
|
83
88
|
barPadding?: number;
|
|
@@ -95,6 +100,32 @@ export type OptionalDualAxisBarChartProperties = {
|
|
|
95
100
|
testId?: string;
|
|
96
101
|
/** Extra CSS class string on the root `<div>`. */
|
|
97
102
|
classes?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Minimum rendered bar height in pixels. The default `2` keeps very small non-zero
|
|
105
|
+
* values visible, but it also paints a 2px stub for genuine zeros — set `0` so an
|
|
106
|
+
* all-zero/dormant series renders nothing (letting the consumer detect it and show
|
|
107
|
+
* an empty state instead of a row of indistinguishable baseline stubs). Default `2`.
|
|
108
|
+
*/
|
|
109
|
+
minBarHeight?: number;
|
|
110
|
+
/**
|
|
111
|
+
* Override the plot margins (px) reserved for axis titles and tick labels. Merged
|
|
112
|
+
* over the defaults `{ top: 24, right: 56, bottom: 40, left: 56 }`; widen `left`/`right`
|
|
113
|
+
* when long currency tick labels (e.g. `₹1,00,000`) would otherwise overlap the
|
|
114
|
+
* gridlines in a narrow container.
|
|
115
|
+
*/
|
|
116
|
+
margin?: {
|
|
117
|
+
top?: number;
|
|
118
|
+
right?: number;
|
|
119
|
+
bottom?: number;
|
|
120
|
+
left?: number;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Render the hover tooltip on `document.body` with `position: fixed` viewport
|
|
124
|
+
* coordinates instead of `position: absolute` inside the chart. Set `true` when the
|
|
125
|
+
* chart sits in an `overflow:auto`/scrollable container that would otherwise clip the
|
|
126
|
+
* tooltip. Default `false` (in-chart positioning, unchanged).
|
|
127
|
+
*/
|
|
128
|
+
tooltipPortal?: boolean;
|
|
98
129
|
};
|
|
99
130
|
export type DualAxisBarChartEventProperties = {
|
|
100
131
|
/**
|