noph-ui 0.24.10 → 0.24.12
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 +118 -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 +53 -6
- package/dist/select/VirtualList.svelte +7 -1
- package/dist/select/VirtualList.svelte.d.ts +4 -0
- 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,109 @@
|
|
|
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
|
+
|
|
78
|
+
if (menuOpen && activeIndex >= 0) {
|
|
79
|
+
const id = `${uid}-opt-${activeIndex}`
|
|
80
|
+
const optEl = document.getElementById(id)
|
|
81
|
+
if (optEl) {
|
|
82
|
+
optEl.scrollIntoView({ block: 'nearest' })
|
|
83
|
+
} else if (useVirtualList && menuElement) {
|
|
84
|
+
const viewport = menuElement.querySelector(
|
|
85
|
+
'svelte-virtual-list-viewport',
|
|
86
|
+
) as HTMLElement | null
|
|
87
|
+
if (viewport) {
|
|
88
|
+
let rowHeight = 48
|
|
89
|
+
const firstRow = viewport.querySelector('[id^="' + uid + '-opt-"]') as HTMLElement | null
|
|
90
|
+
if (firstRow) {
|
|
91
|
+
rowHeight = firstRow.offsetHeight || rowHeight
|
|
92
|
+
}
|
|
93
|
+
const top = activeIndex * rowHeight
|
|
94
|
+
const bottom = top + rowHeight
|
|
95
|
+
const { scrollTop, clientHeight } = viewport
|
|
96
|
+
if (top < scrollTop) {
|
|
97
|
+
viewport.scrollTop = top
|
|
98
|
+
} else if (bottom > scrollTop + clientHeight) {
|
|
99
|
+
viewport.scrollTop = bottom - clientHeight
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
52
105
|
</script>
|
|
53
106
|
|
|
54
|
-
{#snippet item(option: AutoCompleteOption)}
|
|
107
|
+
{#snippet item(option: AutoCompleteOption, index: number)}
|
|
55
108
|
<Item
|
|
109
|
+
id="{uid}-opt-{index}"
|
|
110
|
+
softFocus={index === activeIndex}
|
|
111
|
+
aria-selected={index === activeIndex}
|
|
112
|
+
role="option"
|
|
113
|
+
tabindex={-1}
|
|
114
|
+
onmousedown={(e) => {
|
|
115
|
+
e.preventDefault()
|
|
116
|
+
}}
|
|
117
|
+
onmouseenter={() => setActive(index)}
|
|
56
118
|
onclick={(event) => {
|
|
57
119
|
event.preventDefault()
|
|
58
|
-
|
|
120
|
+
setActive(index)
|
|
59
121
|
onoptionselect(option)
|
|
60
122
|
}}
|
|
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
123
|
variant="button"
|
|
84
124
|
>{option.label}
|
|
85
125
|
</Item>
|
|
86
126
|
{/snippet}
|
|
87
127
|
|
|
88
128
|
<TextField
|
|
89
|
-
autocomplete="off"
|
|
90
129
|
{...attributes}
|
|
130
|
+
autocomplete="off"
|
|
91
131
|
{variant}
|
|
92
132
|
type="text"
|
|
93
133
|
populated={finalPopulated}
|
|
94
134
|
bind:clientWidth
|
|
95
135
|
bind:value
|
|
96
|
-
bind:focused
|
|
97
136
|
style="anchor-name:--{uid};"
|
|
137
|
+
role="combobox"
|
|
138
|
+
aria-controls="listbox-{uid}"
|
|
139
|
+
aria-expanded={menuOpen}
|
|
140
|
+
aria-autocomplete="list"
|
|
141
|
+
aria-activedescendant={activeIndex >= 0 ? `${uid}-opt-${activeIndex}` : undefined}
|
|
142
|
+
aria-haspopup="listbox"
|
|
98
143
|
onclick={(event) => {
|
|
99
144
|
finalPopulated = true
|
|
100
145
|
showPopover()
|
|
@@ -102,32 +147,42 @@
|
|
|
102
147
|
}}
|
|
103
148
|
oninput={(event) => {
|
|
104
149
|
showPopover()
|
|
150
|
+
activeIndex = -1
|
|
105
151
|
oninput?.(event)
|
|
106
152
|
}}
|
|
107
153
|
onkeydown={(event) => {
|
|
108
|
-
if (event.key === 'Tab'
|
|
109
|
-
|
|
154
|
+
if (event.key === 'Tab') {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (event.key === 'Escape') {
|
|
110
158
|
hidePopover()
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
finalPopulated = true
|
|
115
|
-
blockEvent = true
|
|
116
|
-
showPopover()
|
|
117
|
-
;(menuElement?.firstElementChild?.firstElementChild as HTMLElement)?.focus()
|
|
118
|
-
}
|
|
159
|
+
activeIndex = -1
|
|
160
|
+
event.preventDefault()
|
|
161
|
+
return
|
|
119
162
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
163
|
+
if (event.key === 'ArrowDown') {
|
|
164
|
+
finalPopulated = true
|
|
165
|
+
showPopover()
|
|
166
|
+
moveActive(1)
|
|
167
|
+
event.preventDefault()
|
|
168
|
+
return
|
|
125
169
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
if (event.key === 'ArrowUp') {
|
|
171
|
+
finalPopulated = true
|
|
172
|
+
showPopover()
|
|
173
|
+
moveActive(-1)
|
|
174
|
+
event.preventDefault()
|
|
175
|
+
return
|
|
130
176
|
}
|
|
177
|
+
if (event.key === 'Enter' && activeIndex >= 0) {
|
|
178
|
+
const opt = displayOptions[activeIndex]
|
|
179
|
+
if (opt) {
|
|
180
|
+
onoptionselect(opt)
|
|
181
|
+
}
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
onkeydown?.(event)
|
|
131
186
|
}}
|
|
132
187
|
bind:reportValidity
|
|
133
188
|
bind:checkValidity
|
|
@@ -135,6 +190,7 @@
|
|
|
135
190
|
>{@render children?.()}
|
|
136
191
|
</TextField>
|
|
137
192
|
<Menu
|
|
193
|
+
id="listbox-{uid}"
|
|
138
194
|
style="position-anchor:--{uid};{clampMenuWidth || useVirtualList
|
|
139
195
|
? 'width'
|
|
140
196
|
: 'min-width'}:{clientWidth}px"
|
|
@@ -147,38 +203,32 @@
|
|
|
147
203
|
? 'var(--np-outlined-select-text-field-container-shape)'
|
|
148
204
|
: 'var(--np-filled-select-text-field-container-shape)'}
|
|
149
205
|
anchor={element}
|
|
150
|
-
onbeforetoggle={(e) => {
|
|
151
|
-
if (e.newState !== 'closed') {
|
|
152
|
-
blockEvent = true
|
|
153
|
-
}
|
|
154
|
-
}}
|
|
155
206
|
ontoggle={(e) => {
|
|
156
207
|
if (e.newState === 'closed') {
|
|
157
|
-
|
|
208
|
+
menuOpen = false
|
|
209
|
+
activeIndex = -1
|
|
158
210
|
if (!populated && finalPopulated && !value) {
|
|
159
211
|
finalPopulated = false
|
|
160
212
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
onblur?.(event)
|
|
168
|
-
onfocusout?.(event)
|
|
213
|
+
} else {
|
|
214
|
+
menuOpen = true
|
|
215
|
+
// Ensure activeIndex valid when opening
|
|
216
|
+
if (activeIndex >= displayOptions.length) {
|
|
217
|
+
activeIndex = -1
|
|
218
|
+
}
|
|
169
219
|
}
|
|
170
220
|
}}
|
|
171
221
|
bind:element={menuElement}
|
|
172
222
|
>
|
|
173
223
|
{#if useVirtualList}
|
|
174
224
|
<VirtualList height="250px" itemHeight={48} items={displayOptions}>
|
|
175
|
-
{#snippet row(option)}
|
|
176
|
-
{@render item(option)}
|
|
225
|
+
{#snippet row(option, index)}
|
|
226
|
+
{@render item(option, index)}
|
|
177
227
|
{/snippet}
|
|
178
228
|
</VirtualList>
|
|
179
229
|
{:else}
|
|
180
230
|
{#each displayOptions as option, index (index)}
|
|
181
|
-
{@render item(option)}
|
|
231
|
+
{@render item(option, index)}
|
|
182
232
|
{/each}
|
|
183
233
|
{/if}
|
|
184
234
|
</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;
|
|
@@ -165,7 +165,28 @@
|
|
|
165
165
|
focusIndex = Math.min(Math.max(index, 0), options.length - 1)
|
|
166
166
|
await tick()
|
|
167
167
|
const el = document.getElementById(`${uid}-opt-${focusIndex}`)
|
|
168
|
-
|
|
168
|
+
if (el) {
|
|
169
|
+
el.focus()
|
|
170
|
+
} else if (useVirtualList && menuElement) {
|
|
171
|
+
const viewport = menuElement.querySelector(
|
|
172
|
+
'svelte-virtual-list-viewport',
|
|
173
|
+
) as HTMLElement | null
|
|
174
|
+
if (viewport) {
|
|
175
|
+
let rowHeight = 48
|
|
176
|
+
const firstRow = viewport.querySelector('[id^="' + uid + '-opt-"]') as HTMLElement | null
|
|
177
|
+
if (firstRow) {
|
|
178
|
+
rowHeight = firstRow.offsetHeight || rowHeight
|
|
179
|
+
}
|
|
180
|
+
const top = focusIndex * rowHeight
|
|
181
|
+
const bottom = top + rowHeight
|
|
182
|
+
const { scrollTop, clientHeight } = viewport
|
|
183
|
+
if (top < scrollTop) {
|
|
184
|
+
viewport.scrollTop = top
|
|
185
|
+
} else if (bottom > scrollTop + clientHeight) {
|
|
186
|
+
viewport.scrollTop = bottom - clientHeight
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
169
190
|
}
|
|
170
191
|
|
|
171
192
|
const moveFocus = (delta: number) => {
|
|
@@ -457,14 +478,15 @@
|
|
|
457
478
|
</div>
|
|
458
479
|
</div>
|
|
459
480
|
|
|
460
|
-
{#snippet item(option: SelectOption, index
|
|
481
|
+
{#snippet item(option: SelectOption, index: number)}
|
|
461
482
|
{#if Array.isArray(value) && multiple}
|
|
462
483
|
<Item
|
|
463
|
-
id={
|
|
484
|
+
id="{uid}-opt-{index}"
|
|
464
485
|
onclick={(event) => {
|
|
465
486
|
handleOptionSelect(event, option)
|
|
466
487
|
field?.focus()
|
|
467
488
|
}}
|
|
489
|
+
tabindex={-1}
|
|
468
490
|
disabled={option.disabled}
|
|
469
491
|
aria-disabled={option.disabled}
|
|
470
492
|
role="option"
|
|
@@ -501,11 +523,12 @@
|
|
|
501
523
|
</Item>
|
|
502
524
|
{:else}
|
|
503
525
|
<Item
|
|
504
|
-
id={
|
|
526
|
+
id="{uid}-opt-{index}"
|
|
505
527
|
onclick={(event) => {
|
|
506
528
|
handleOptionSelect(event, option)
|
|
507
529
|
field?.focus()
|
|
508
530
|
}}
|
|
531
|
+
tabindex={-1}
|
|
509
532
|
disabled={option.disabled}
|
|
510
533
|
aria-disabled={option.disabled}
|
|
511
534
|
role="option"
|
|
@@ -554,17 +577,41 @@
|
|
|
554
577
|
? 'var(--np-outlined-select-text-field-container-shape)'
|
|
555
578
|
: 'var(--np-filled-select-text-field-container-shape)'}
|
|
556
579
|
anchor={anchorElement}
|
|
557
|
-
ontoggle={({ newState }) => {
|
|
580
|
+
ontoggle={async ({ newState }) => {
|
|
558
581
|
if (newState === 'open') {
|
|
559
582
|
menuOpen = true
|
|
583
|
+
let idx = -1
|
|
584
|
+
if (multiple) {
|
|
585
|
+
if (Array.isArray(value) && value.length) {
|
|
586
|
+
idx = options.findIndex((o) => value.includes(o.value) && !o.disabled)
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
idx = options.findIndex((o) => o.value === value && !o.disabled)
|
|
590
|
+
}
|
|
591
|
+
if (idx < 0) {
|
|
592
|
+
idx = options.findIndex((o) => !o.disabled)
|
|
593
|
+
}
|
|
594
|
+
if (idx < 0) idx = 0
|
|
595
|
+
focusIndex = idx
|
|
560
596
|
} else {
|
|
561
597
|
menuOpen = false
|
|
598
|
+
focusIndex = -1
|
|
562
599
|
}
|
|
563
600
|
}}
|
|
564
601
|
bind:element={menuElement}
|
|
565
602
|
>
|
|
566
603
|
{#if useVirtualList}
|
|
567
|
-
<VirtualList
|
|
604
|
+
<VirtualList
|
|
605
|
+
height="250px"
|
|
606
|
+
itemHeight={48}
|
|
607
|
+
items={options}
|
|
608
|
+
rendered={({ start, end }) => {
|
|
609
|
+
if (focusIndex >= start && focusIndex < end) {
|
|
610
|
+
const el = document.getElementById(`${uid}-opt-${focusIndex}`)
|
|
611
|
+
el?.focus()
|
|
612
|
+
}
|
|
613
|
+
}}
|
|
614
|
+
>
|
|
568
615
|
{#snippet row(option, index)}
|
|
569
616
|
{@render item(option, index)}
|
|
570
617
|
{/snippet}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
end?: number
|
|
11
11
|
row: Snippet<[T, number]>
|
|
12
12
|
overscan?: number
|
|
13
|
+
rendered?: (event: { start: number; end: number }) => void
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
let {
|
|
@@ -19,7 +20,8 @@
|
|
|
19
20
|
start = $bindable(0),
|
|
20
21
|
end = $bindable(0),
|
|
21
22
|
row,
|
|
22
|
-
overscan =
|
|
23
|
+
overscan = 0,
|
|
24
|
+
rendered,
|
|
23
25
|
}: VirtualListProps = $props()
|
|
24
26
|
|
|
25
27
|
let height_map: number[] = []
|
|
@@ -79,6 +81,8 @@
|
|
|
79
81
|
|
|
80
82
|
bottom = remaining * average_height
|
|
81
83
|
height_map.length = items.length
|
|
84
|
+
|
|
85
|
+
rendered?.({ start, end })
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
async function handle_scroll() {
|
|
@@ -151,6 +155,8 @@
|
|
|
151
155
|
const d = actual_height - expected_height
|
|
152
156
|
viewport.scrollTo(0, scrollTop + d)
|
|
153
157
|
}
|
|
158
|
+
await tick()
|
|
159
|
+
rendered?.({ start, end })
|
|
154
160
|
}
|
|
155
161
|
|
|
156
162
|
onMount(() => {
|