@happyvertical/smrt-ui 0.55.1 → 0.55.2
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { untrack } from 'svelte';
|
|
2
|
+
import { type Snippet, untrack } from 'svelte';
|
|
3
3
|
import {
|
|
4
4
|
emitControlChange,
|
|
5
5
|
highlightControl,
|
|
@@ -43,6 +43,31 @@ export interface Props {
|
|
|
43
43
|
* field stays empty rather than flashing the raw value (an id).
|
|
44
44
|
*/
|
|
45
45
|
valueLabel?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether typing filters `options` by label. Set `false` when the caller
|
|
48
|
+
* supplies the matching options itself (server-side search via `onquery`),
|
|
49
|
+
* so a result that matched on something other than its label stays listed.
|
|
50
|
+
*/
|
|
51
|
+
filter?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Called with the text to search for when the list opens (empty) and on
|
|
54
|
+
* every keystroke. Pair with `filter={false}` and fresh `options`.
|
|
55
|
+
*/
|
|
56
|
+
onquery?: (query: string) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Message announced politely to assistive technology while the list is open
|
|
59
|
+
* ("Searching…", "3 results", "No results"). It is also shown inside the
|
|
60
|
+
* list while there are no options to list.
|
|
61
|
+
*/
|
|
62
|
+
status?: string;
|
|
63
|
+
/** Marks the combobox busy while options are being fetched. */
|
|
64
|
+
busy?: boolean;
|
|
65
|
+
/** Marks the field invalid (`aria-invalid`). */
|
|
66
|
+
invalid?: boolean;
|
|
67
|
+
/** Ids of elements describing the field (error text, hints). */
|
|
68
|
+
describedby?: string;
|
|
69
|
+
/** Custom content for one option row; defaults to the label. */
|
|
70
|
+
optionContent?: Snippet<[ControlOption]>;
|
|
46
71
|
/** Callback when the selected value changes. */
|
|
47
72
|
onvaluechange?: (value: string) => void;
|
|
48
73
|
/** CSS class to apply to the combobox container. */
|
|
@@ -59,6 +84,13 @@ let {
|
|
|
59
84
|
allowCustom = false,
|
|
60
85
|
interaction,
|
|
61
86
|
valueLabel,
|
|
87
|
+
filter = true,
|
|
88
|
+
onquery,
|
|
89
|
+
status,
|
|
90
|
+
busy = false,
|
|
91
|
+
invalid = false,
|
|
92
|
+
describedby,
|
|
93
|
+
optionContent,
|
|
62
94
|
onvaluechange,
|
|
63
95
|
class: className = '',
|
|
64
96
|
}: Props = $props();
|
|
@@ -83,7 +115,7 @@ let query = $state(untrack(() => labelForValue(value)));
|
|
|
83
115
|
let typed = $state(false);
|
|
84
116
|
let activeIndex = $state(0);
|
|
85
117
|
const filtered = $derived(
|
|
86
|
-
typed
|
|
118
|
+
typed && filter
|
|
87
119
|
? options.filter((option) =>
|
|
88
120
|
option.label.toLowerCase().includes(query.toLowerCase()),
|
|
89
121
|
)
|
|
@@ -136,6 +168,7 @@ function openList() {
|
|
|
136
168
|
(option) => String(option.value) === value,
|
|
137
169
|
);
|
|
138
170
|
activeIndex = selected >= 0 ? selected : 0;
|
|
171
|
+
onquery?.('');
|
|
139
172
|
}
|
|
140
173
|
function closeList() {
|
|
141
174
|
open = false;
|
|
@@ -147,6 +180,7 @@ function handleInput(event: Event & { currentTarget: HTMLInputElement }) {
|
|
|
147
180
|
typed = true;
|
|
148
181
|
open = true;
|
|
149
182
|
activeIndex = 0;
|
|
183
|
+
onquery?.(query);
|
|
150
184
|
if (allowCustom) {
|
|
151
185
|
value = query;
|
|
152
186
|
onvaluechange?.(value);
|
|
@@ -179,6 +213,10 @@ function handleKeydown(event: KeyboardEvent) {
|
|
|
179
213
|
closeList();
|
|
180
214
|
}
|
|
181
215
|
}
|
|
216
|
+
// Options that arrive late can be fewer than the highlighted row.
|
|
217
|
+
$effect(() => {
|
|
218
|
+
if (activeIndex >= filtered.length) activeIndex = 0;
|
|
219
|
+
});
|
|
182
220
|
$effect(() => {
|
|
183
221
|
const form = inputEl?.form;
|
|
184
222
|
if (!form) return;
|
|
@@ -300,15 +338,19 @@ useControlRegistration(() => {
|
|
|
300
338
|
<!-- The form posts the committed value (an option id), never the label the person sees. -->
|
|
301
339
|
{#if name}<input type="hidden" {name} {value} {disabled} />{/if}
|
|
302
340
|
<input bind:this={inputEl} id={inputId} role="combobox" autocomplete="off" {placeholder} {disabled} required={required && !value} aria-required={required ? 'true' : undefined} value={query}
|
|
303
|
-
aria-expanded={open} aria-controls={listId} aria-autocomplete="list" aria-activedescendant={open && filtered[activeIndex] ? `${listId}-${activeIndex}` : undefined}
|
|
341
|
+
aria-expanded={open} aria-controls={listId} aria-busy={busy ? 'true' : undefined} aria-invalid={invalid ? 'true' : undefined} aria-describedby={describedby} aria-autocomplete="list" aria-activedescendant={open && filtered[activeIndex] ? `${listId}-${activeIndex}` : undefined}
|
|
304
342
|
onfocus={openList} onclick={openList} oninput={handleInput} onkeydown={handleKeydown} />
|
|
305
343
|
{#if open && filtered.length}<div id={listId} class="options" role="listbox">{#each filtered as option, index (option.value)}<button id={`${listId}-${index}`} type="button" role="option"
|
|
306
|
-
aria-selected={String(option.value) === value} class:active={index === activeIndex} disabled={option.disabled} onpointerdown={(event) => event.preventDefault()} onclick={() => commit(option, true)}>{option.label}</button>{/each}</div>
|
|
344
|
+
aria-selected={String(option.value) === value} class:active={index === activeIndex} disabled={option.disabled} onpointerdown={(event) => event.preventDefault()} onclick={() => commit(option, true)}>{#if optionContent}{@render optionContent(option)}{:else}{option.label}{/if}</button>{/each}</div>
|
|
345
|
+
{:else if open && status}<div class="options empty" aria-hidden="true">{status}</div>{/if}
|
|
346
|
+
{#if status !== undefined}<div class="sr-status" role="status" aria-live="polite">{open ? status : ''}</div>{/if}
|
|
307
347
|
</div>
|
|
308
348
|
<style>
|
|
309
349
|
.combobox { position: relative; display: grid; gap: var(--smrt-spacing-1); color: var(--smrt-color-on-surface); }
|
|
310
350
|
label { font: var(--smrt-typography-label-large-font); } input { box-sizing: border-box; width: 100%; padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 1px solid var(--smrt-color-outline); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface); color: inherit; }
|
|
311
351
|
input:focus { outline: 2px solid var(--smrt-color-primary); outline-offset: 1px; } .options { position: absolute; z-index: var(--smrt-z-index-dropdown); top: 100%; left: 0; right: 0; display: grid; padding: var(--smrt-spacing-1); border: 1px solid var(--smrt-color-outline-variant); border-radius: var(--smrt-radius-small); background: var(--smrt-color-surface-container); box-shadow: var(--smrt-elevation-2); }
|
|
312
352
|
.options button { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border: 0; border-radius: var(--smrt-radius-extra-small); background: transparent; color: var(--smrt-color-on-surface); text-align: left; } .options button.active { background: var(--smrt-color-secondary-container); }
|
|
353
|
+
.options.empty { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); color: var(--smrt-color-on-surface-variant); }
|
|
354
|
+
.sr-status { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
313
355
|
:global(.combobox[data-smrt-highlighted='true']) { outline: 3px solid var(--smrt-color-tertiary); outline-offset: 4px; }
|
|
314
356
|
</style>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
1
2
|
import type { ControlInteractionOptions, ControlOption } from './control-interaction.js';
|
|
2
3
|
export interface Props {
|
|
3
4
|
/** Array of options to select from. */
|
|
@@ -24,6 +25,31 @@ export interface Props {
|
|
|
24
25
|
* field stays empty rather than flashing the raw value (an id).
|
|
25
26
|
*/
|
|
26
27
|
valueLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether typing filters `options` by label. Set `false` when the caller
|
|
30
|
+
* supplies the matching options itself (server-side search via `onquery`),
|
|
31
|
+
* so a result that matched on something other than its label stays listed.
|
|
32
|
+
*/
|
|
33
|
+
filter?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Called with the text to search for when the list opens (empty) and on
|
|
36
|
+
* every keystroke. Pair with `filter={false}` and fresh `options`.
|
|
37
|
+
*/
|
|
38
|
+
onquery?: (query: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Message announced politely to assistive technology while the list is open
|
|
41
|
+
* ("Searching…", "3 results", "No results"). It is also shown inside the
|
|
42
|
+
* list while there are no options to list.
|
|
43
|
+
*/
|
|
44
|
+
status?: string;
|
|
45
|
+
/** Marks the combobox busy while options are being fetched. */
|
|
46
|
+
busy?: boolean;
|
|
47
|
+
/** Marks the field invalid (`aria-invalid`). */
|
|
48
|
+
invalid?: boolean;
|
|
49
|
+
/** Ids of elements describing the field (error text, hints). */
|
|
50
|
+
describedby?: string;
|
|
51
|
+
/** Custom content for one option row; defaults to the label. */
|
|
52
|
+
optionContent?: Snippet<[ControlOption]>;
|
|
27
53
|
/** Callback when the selected value changes. */
|
|
28
54
|
onvaluechange?: (value: string) => void;
|
|
29
55
|
/** CSS class to apply to the combobox container. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Combobox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Combobox.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Combobox.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/Combobox.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAW,MAAM,QAAQ,CAAC;AAM/C,OAAO,KAAK,EACV,yBAAyB,EACzB,aAAa,EACd,MAAM,0BAA0B,CAAC;AAWlC,MAAM,WAAW,KAAK;IACpB,uCAAuC;IACvC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,mDAAmD;IACnD,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;IAChD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC;IACzC,gDAAgD;IAChD,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwRD,QAAA,MAAM,QAAQ,gDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-ui",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.2",
|
|
4
4
|
"smrtJsdoc": "strict",
|
|
5
5
|
"description": "Domain-agnostic Svelte 5 UI runtime for SMRT: primitives, i18n client, theme system, and module UI registry",
|
|
6
6
|
"type": "module",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
"access": "public"
|
|
140
140
|
},
|
|
141
141
|
"dependencies": {
|
|
142
|
-
"@happyvertical/smrt-types": "0.55.
|
|
142
|
+
"@happyvertical/smrt-types": "0.55.2",
|
|
143
143
|
"esm-env": "^1.2.2"
|
|
144
144
|
},
|
|
145
145
|
"peerDependencies": {
|