noph-ui 0.24.10 → 0.24.11
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 +1 -0
- package/dist/autocomplete/AutoComplete.svelte +90 -68
- package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
- package/dist/autocomplete/types.d.ts +0 -2
- package/dist/list/Item.svelte +2 -1
- package/dist/list/types.d.ts +3 -0
- package/dist/select/Select.svelte +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,9 +26,6 @@
|
|
|
26
26
|
onkeydown,
|
|
27
27
|
onclick,
|
|
28
28
|
oninput,
|
|
29
|
-
onblur,
|
|
30
|
-
onfocusout,
|
|
31
|
-
focused = $bindable(false),
|
|
32
29
|
...attributes
|
|
33
30
|
}: AutoCompleteProps = $props()
|
|
34
31
|
|
|
@@ -40,61 +37,81 @@
|
|
|
40
37
|
menuElement?.hidePopover()
|
|
41
38
|
}
|
|
42
39
|
const uid = $props.id()
|
|
43
|
-
|
|
40
|
+
const defaultOptionsFilter = (option: AutoCompleteOption) => {
|
|
44
41
|
return !value || option.label.toLocaleLowerCase().includes(value.toLocaleLowerCase())
|
|
45
42
|
}
|
|
46
43
|
let displayOptions = $derived(options.filter(optionsFilter || defaultOptionsFilter))
|
|
47
44
|
let useVirtualList = $derived(displayOptions.length > 4000)
|
|
48
45
|
let clientWidth = $state(0)
|
|
49
|
-
let menuElement
|
|
46
|
+
let menuElement = $state<HTMLDivElement>()
|
|
47
|
+
let menuOpen = $state(false)
|
|
50
48
|
let finalPopulated = $state(populated)
|
|
51
|
-
let
|
|
49
|
+
let activeIndex = $state(-1)
|
|
50
|
+
|
|
51
|
+
function setActive(index: number) {
|
|
52
|
+
if (index < 0 || index >= displayOptions.length) {
|
|
53
|
+
activeIndex = -1
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
activeIndex = index
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function moveActive(delta: number) {
|
|
60
|
+
if (!displayOptions.length) {
|
|
61
|
+
activeIndex = -1
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const next =
|
|
65
|
+
activeIndex === -1
|
|
66
|
+
? delta > 0
|
|
67
|
+
? 0
|
|
68
|
+
: displayOptions.length - 1
|
|
69
|
+
: (activeIndex + delta + displayOptions.length) % displayOptions.length
|
|
70
|
+
setActive(next)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (activeIndex >= displayOptions.length) {
|
|
75
|
+
activeIndex = -1
|
|
76
|
+
}
|
|
77
|
+
})
|
|
52
78
|
</script>
|
|
53
79
|
|
|
54
|
-
{#snippet item(option: AutoCompleteOption)}
|
|
80
|
+
{#snippet item(option: AutoCompleteOption, index: number)}
|
|
55
81
|
<Item
|
|
82
|
+
id="{uid}-opt-{index}"
|
|
83
|
+
softFocus={index === activeIndex}
|
|
84
|
+
aria-selected={index === activeIndex}
|
|
85
|
+
role="option"
|
|
86
|
+
onmousedown={(e) => {
|
|
87
|
+
e.preventDefault()
|
|
88
|
+
}}
|
|
89
|
+
onmouseenter={() => setActive(index)}
|
|
56
90
|
onclick={(event) => {
|
|
57
91
|
event.preventDefault()
|
|
58
|
-
|
|
92
|
+
setActive(index)
|
|
59
93
|
onoptionselect(option)
|
|
60
94
|
}}
|
|
61
|
-
role="option"
|
|
62
|
-
disabled={option.disabled}
|
|
63
|
-
onkeydown={(event) => {
|
|
64
|
-
if (event.key === 'ArrowDown') {
|
|
65
|
-
blockEvent = true
|
|
66
|
-
;(event.currentTarget?.nextElementSibling as HTMLElement)?.focus()
|
|
67
|
-
event.preventDefault()
|
|
68
|
-
}
|
|
69
|
-
if (event.key === 'ArrowUp') {
|
|
70
|
-
blockEvent = true
|
|
71
|
-
;(event.currentTarget?.previousElementSibling as HTMLElement)?.focus()
|
|
72
|
-
event.preventDefault()
|
|
73
|
-
}
|
|
74
|
-
if (event.key === 'Enter') {
|
|
75
|
-
onoptionselect(option)
|
|
76
|
-
}
|
|
77
|
-
if (event.key === 'Tab') {
|
|
78
|
-
finalPopulated = populated
|
|
79
|
-
blockEvent = false
|
|
80
|
-
hidePopover?.()
|
|
81
|
-
}
|
|
82
|
-
}}
|
|
83
95
|
variant="button"
|
|
84
96
|
>{option.label}
|
|
85
97
|
</Item>
|
|
86
98
|
{/snippet}
|
|
87
99
|
|
|
88
100
|
<TextField
|
|
89
|
-
autocomplete="off"
|
|
90
101
|
{...attributes}
|
|
102
|
+
autocomplete="off"
|
|
91
103
|
{variant}
|
|
92
104
|
type="text"
|
|
93
105
|
populated={finalPopulated}
|
|
94
106
|
bind:clientWidth
|
|
95
107
|
bind:value
|
|
96
|
-
bind:focused
|
|
97
108
|
style="anchor-name:--{uid};"
|
|
109
|
+
role="combobox"
|
|
110
|
+
aria-controls="listbox-{uid}"
|
|
111
|
+
aria-expanded={menuOpen}
|
|
112
|
+
aria-autocomplete="list"
|
|
113
|
+
aria-activedescendant={activeIndex >= 0 ? `${uid}-opt-${activeIndex}` : undefined}
|
|
114
|
+
aria-haspopup="listbox"
|
|
98
115
|
onclick={(event) => {
|
|
99
116
|
finalPopulated = true
|
|
100
117
|
showPopover()
|
|
@@ -102,32 +119,42 @@
|
|
|
102
119
|
}}
|
|
103
120
|
oninput={(event) => {
|
|
104
121
|
showPopover()
|
|
122
|
+
activeIndex = -1
|
|
105
123
|
oninput?.(event)
|
|
106
124
|
}}
|
|
107
125
|
onkeydown={(event) => {
|
|
108
|
-
if (event.key === 'Tab'
|
|
109
|
-
|
|
126
|
+
if (event.key === 'Tab') {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
if (event.key === 'Escape') {
|
|
110
130
|
hidePopover()
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
finalPopulated = true
|
|
115
|
-
blockEvent = true
|
|
116
|
-
showPopover()
|
|
117
|
-
;(menuElement?.firstElementChild?.firstElementChild as HTMLElement)?.focus()
|
|
118
|
-
}
|
|
131
|
+
activeIndex = -1
|
|
132
|
+
event.preventDefault()
|
|
133
|
+
return
|
|
119
134
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
if (event.key === 'ArrowDown') {
|
|
136
|
+
finalPopulated = true
|
|
137
|
+
showPopover()
|
|
138
|
+
moveActive(1)
|
|
139
|
+
event.preventDefault()
|
|
140
|
+
return
|
|
125
141
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
if (event.key === 'ArrowUp') {
|
|
143
|
+
finalPopulated = true
|
|
144
|
+
showPopover()
|
|
145
|
+
moveActive(-1)
|
|
146
|
+
event.preventDefault()
|
|
147
|
+
return
|
|
130
148
|
}
|
|
149
|
+
if (event.key === 'Enter' && activeIndex >= 0) {
|
|
150
|
+
const opt = displayOptions[activeIndex]
|
|
151
|
+
if (opt) {
|
|
152
|
+
onoptionselect(opt)
|
|
153
|
+
}
|
|
154
|
+
event.preventDefault()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
onkeydown?.(event)
|
|
131
158
|
}}
|
|
132
159
|
bind:reportValidity
|
|
133
160
|
bind:checkValidity
|
|
@@ -135,6 +162,7 @@
|
|
|
135
162
|
>{@render children?.()}
|
|
136
163
|
</TextField>
|
|
137
164
|
<Menu
|
|
165
|
+
id="listbox-{uid}"
|
|
138
166
|
style="position-anchor:--{uid};{clampMenuWidth || useVirtualList
|
|
139
167
|
? 'width'
|
|
140
168
|
: 'min-width'}:{clientWidth}px"
|
|
@@ -147,38 +175,32 @@
|
|
|
147
175
|
? 'var(--np-outlined-select-text-field-container-shape)'
|
|
148
176
|
: 'var(--np-filled-select-text-field-container-shape)'}
|
|
149
177
|
anchor={element}
|
|
150
|
-
onbeforetoggle={(e) => {
|
|
151
|
-
if (e.newState !== 'closed') {
|
|
152
|
-
blockEvent = true
|
|
153
|
-
}
|
|
154
|
-
}}
|
|
155
178
|
ontoggle={(e) => {
|
|
156
179
|
if (e.newState === 'closed') {
|
|
157
|
-
|
|
180
|
+
menuOpen = false
|
|
181
|
+
activeIndex = -1
|
|
158
182
|
if (!populated && finalPopulated && !value) {
|
|
159
183
|
finalPopulated = false
|
|
160
184
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
onblur?.(event)
|
|
168
|
-
onfocusout?.(event)
|
|
185
|
+
} else {
|
|
186
|
+
menuOpen = true
|
|
187
|
+
// Ensure activeIndex valid when opening
|
|
188
|
+
if (activeIndex >= displayOptions.length) {
|
|
189
|
+
activeIndex = -1
|
|
190
|
+
}
|
|
169
191
|
}
|
|
170
192
|
}}
|
|
171
193
|
bind:element={menuElement}
|
|
172
194
|
>
|
|
173
195
|
{#if useVirtualList}
|
|
174
196
|
<VirtualList height="250px" itemHeight={48} items={displayOptions}>
|
|
175
|
-
{#snippet row(option)}
|
|
176
|
-
{@render item(option)}
|
|
197
|
+
{#snippet row(option, index)}
|
|
198
|
+
{@render item(option, index)}
|
|
177
199
|
{/snippet}
|
|
178
200
|
</VirtualList>
|
|
179
201
|
{:else}
|
|
180
202
|
{#each displayOptions as option, index (index)}
|
|
181
|
-
{@render item(option)}
|
|
203
|
+
{@render item(option, index)}
|
|
182
204
|
{/each}
|
|
183
205
|
{/if}
|
|
184
206
|
</Menu>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { AutoCompleteProps } from './types.ts';
|
|
2
|
-
declare const AutoComplete: import("svelte").Component<AutoCompleteProps, {}, "element" | "value" | "showPopover" | "hidePopover" | "reportValidity" | "checkValidity"
|
|
2
|
+
declare const AutoComplete: import("svelte").Component<AutoCompleteProps, {}, "element" | "value" | "showPopover" | "hidePopover" | "reportValidity" | "checkValidity">;
|
|
3
3
|
type AutoComplete = ReturnType<typeof AutoComplete>;
|
|
4
4
|
export default AutoComplete;
|
|
@@ -2,8 +2,6 @@ import type { InputFieldProps } from '../types.ts';
|
|
|
2
2
|
export interface AutoCompleteOption {
|
|
3
3
|
value?: string | number;
|
|
4
4
|
label: string;
|
|
5
|
-
disabled?: boolean;
|
|
6
|
-
selected?: boolean | undefined | null;
|
|
7
5
|
}
|
|
8
6
|
export interface AutoCompleteProps extends Omit<InputFieldProps, 'clientWidth' | 'clientHeight'> {
|
|
9
7
|
options: AutoCompleteOption[];
|
package/dist/list/Item.svelte
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
disabled = false,
|
|
14
14
|
onfocus,
|
|
15
15
|
onblur,
|
|
16
|
+
softFocus = false,
|
|
16
17
|
...attributes
|
|
17
18
|
}: ItemProps = $props()
|
|
18
19
|
|
|
19
|
-
let focused = $
|
|
20
|
+
let focused = $derived(softFocus)
|
|
20
21
|
let visible = $state(false)
|
|
21
22
|
let element: HTMLButtonElement | HTMLAnchorElement | HTMLDivElement | undefined = $state()
|
|
22
23
|
let observer: IntersectionObserver | undefined
|
package/dist/list/types.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ interface ButtonProps extends HTMLButtonAttributes {
|
|
|
6
6
|
end?: Snippet;
|
|
7
7
|
variant: 'button';
|
|
8
8
|
supportingText?: Snippet;
|
|
9
|
+
softFocus?: boolean;
|
|
9
10
|
}
|
|
10
11
|
interface AnchorProps extends HTMLAnchorAttributes {
|
|
11
12
|
selected?: boolean;
|
|
@@ -14,6 +15,7 @@ interface AnchorProps extends HTMLAnchorAttributes {
|
|
|
14
15
|
disabled?: boolean;
|
|
15
16
|
variant: 'link';
|
|
16
17
|
supportingText?: Snippet;
|
|
18
|
+
softFocus?: boolean;
|
|
17
19
|
}
|
|
18
20
|
interface TextProps extends HTMLAttributes<HTMLDivElement> {
|
|
19
21
|
selected?: boolean;
|
|
@@ -22,6 +24,7 @@ interface TextProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
22
24
|
disabled?: boolean;
|
|
23
25
|
variant?: 'text';
|
|
24
26
|
supportingText?: Snippet;
|
|
27
|
+
softFocus?: boolean;
|
|
25
28
|
}
|
|
26
29
|
export type ItemProps = ButtonProps | AnchorProps | TextProps;
|
|
27
30
|
export type ListItemProps = ButtonProps | AnchorProps | TextProps;
|
|
@@ -457,14 +457,15 @@
|
|
|
457
457
|
</div>
|
|
458
458
|
</div>
|
|
459
459
|
|
|
460
|
-
{#snippet item(option: SelectOption, index
|
|
460
|
+
{#snippet item(option: SelectOption, index: number)}
|
|
461
461
|
{#if Array.isArray(value) && multiple}
|
|
462
462
|
<Item
|
|
463
|
-
id={
|
|
463
|
+
id="{uid}-opt-{index}"
|
|
464
464
|
onclick={(event) => {
|
|
465
465
|
handleOptionSelect(event, option)
|
|
466
466
|
field?.focus()
|
|
467
467
|
}}
|
|
468
|
+
tabindex={-1}
|
|
468
469
|
disabled={option.disabled}
|
|
469
470
|
aria-disabled={option.disabled}
|
|
470
471
|
role="option"
|
|
@@ -501,11 +502,12 @@
|
|
|
501
502
|
</Item>
|
|
502
503
|
{:else}
|
|
503
504
|
<Item
|
|
504
|
-
id={
|
|
505
|
+
id="{uid}-opt-{index}"
|
|
505
506
|
onclick={(event) => {
|
|
506
507
|
handleOptionSelect(event, option)
|
|
507
508
|
field?.focus()
|
|
508
509
|
}}
|
|
510
|
+
tabindex={-1}
|
|
509
511
|
disabled={option.disabled}
|
|
510
512
|
aria-disabled={option.disabled}
|
|
511
513
|
role="option"
|
|
@@ -554,11 +556,28 @@
|
|
|
554
556
|
? 'var(--np-outlined-select-text-field-container-shape)'
|
|
555
557
|
: 'var(--np-filled-select-text-field-container-shape)'}
|
|
556
558
|
anchor={anchorElement}
|
|
557
|
-
ontoggle={({ newState }) => {
|
|
559
|
+
ontoggle={async ({ newState }) => {
|
|
558
560
|
if (newState === 'open') {
|
|
559
561
|
menuOpen = true
|
|
562
|
+
let idx = -1
|
|
563
|
+
if (multiple) {
|
|
564
|
+
if (Array.isArray(value) && value.length) {
|
|
565
|
+
idx = options.findIndex((o) => value.includes(o.value) && !o.disabled)
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
idx = options.findIndex((o) => o.value === value && !o.disabled)
|
|
569
|
+
}
|
|
570
|
+
if (idx < 0) {
|
|
571
|
+
idx = options.findIndex((o) => !o.disabled)
|
|
572
|
+
}
|
|
573
|
+
if (idx < 0) idx = 0
|
|
574
|
+
focusIndex = idx
|
|
575
|
+
await tick()
|
|
576
|
+
const el = document.getElementById(`${uid}-opt-${focusIndex}`)
|
|
577
|
+
;(el as HTMLElement | null)?.focus?.()
|
|
560
578
|
} else {
|
|
561
579
|
menuOpen = false
|
|
580
|
+
focusIndex = -1
|
|
562
581
|
}
|
|
563
582
|
}}
|
|
564
583
|
bind:element={menuElement}
|