noph-ui 0.24.8 → 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/button/Button.svelte +46 -48
- package/dist/button/IconButton.svelte +47 -46
- package/dist/list/Item.svelte +2 -1
- package/dist/list/types.d.ts +3 -0
- package/dist/navigation-drawer/NavigationDrawerItem.svelte +9 -16
- package/dist/navigation-rail/NavigationRailItem.svelte +9 -16
- package/dist/radio/Radio.svelte +4 -3
- package/dist/select/Select.svelte +210 -52
- package/dist/select/VirtualList.svelte +26 -17
- package/dist/select/VirtualList.svelte.d.ts +2 -1
- package/dist/snackbar/Snackbar.svelte +3 -2
- 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[];
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Ripple from '../ripple/Ripple.svelte'
|
|
3
3
|
import Tooltip from '../tooltip/Tooltip.svelte'
|
|
4
|
-
import type {
|
|
5
|
-
HTMLAnchorAttributes,
|
|
6
|
-
HTMLButtonAttributes,
|
|
7
|
-
MouseEventHandler,
|
|
8
|
-
} from 'svelte/elements'
|
|
4
|
+
import type { HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements'
|
|
9
5
|
import type { ButtonProps } from './types.ts'
|
|
10
6
|
import CircularProgress from '../progress/CircularProgress.svelte'
|
|
11
7
|
|
|
@@ -29,13 +25,6 @@
|
|
|
29
25
|
}: ButtonProps = $props()
|
|
30
26
|
|
|
31
27
|
const uid = $props.id()
|
|
32
|
-
|
|
33
|
-
const isButton = (obj: unknown): obj is HTMLButtonAttributes => {
|
|
34
|
-
return (obj as HTMLAnchorAttributes).href === undefined
|
|
35
|
-
}
|
|
36
|
-
const isLink = (obj: unknown): obj is HTMLAnchorAttributes => {
|
|
37
|
-
return (obj as HTMLAnchorAttributes).href !== undefined
|
|
38
|
-
}
|
|
39
28
|
</script>
|
|
40
29
|
|
|
41
30
|
{#snippet content()}
|
|
@@ -64,13 +53,36 @@
|
|
|
64
53
|
{/if}
|
|
65
54
|
{/snippet}
|
|
66
55
|
|
|
67
|
-
{#if
|
|
56
|
+
{#if 'href' in attributes && !disabled && !loading}
|
|
57
|
+
<a
|
|
58
|
+
{...attributes}
|
|
59
|
+
onclick={(event) => {
|
|
60
|
+
;(onclick as MouseEventHandler<HTMLAnchorElement>)?.(event)
|
|
61
|
+
}}
|
|
62
|
+
aria-describedby={title ? uid : attributes['aria-describedby']}
|
|
63
|
+
aria-label={title || attributes['aria-label']}
|
|
64
|
+
bind:this={element}
|
|
65
|
+
class={[
|
|
66
|
+
'np-button',
|
|
67
|
+
size,
|
|
68
|
+
selected ? 'square' : shape,
|
|
69
|
+
toggle && 'toggle',
|
|
70
|
+
'enabled',
|
|
71
|
+
variant,
|
|
72
|
+
attributes.class,
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
{@render content()}
|
|
76
|
+
</a>
|
|
77
|
+
{:else}
|
|
68
78
|
<button
|
|
69
79
|
{...attributes as HTMLButtonAttributes}
|
|
70
80
|
aria-describedby={title ? uid : attributes['aria-describedby']}
|
|
71
81
|
aria-label={title || attributes['aria-label']}
|
|
72
82
|
disabled={disabled || loading}
|
|
73
|
-
aria-pressed={selected}
|
|
83
|
+
aria-pressed={toggle ? selected : undefined}
|
|
84
|
+
aria-busy={loading}
|
|
85
|
+
type={(attributes['type'] as 'button' | 'submit' | 'reset' | 'button') ?? undefined}
|
|
74
86
|
bind:this={element}
|
|
75
87
|
onclick={(event) => {
|
|
76
88
|
if (toggle) {
|
|
@@ -82,39 +94,18 @@
|
|
|
82
94
|
'np-button',
|
|
83
95
|
size,
|
|
84
96
|
selected || loading ? 'square' : shape,
|
|
85
|
-
toggle
|
|
86
|
-
selected
|
|
87
|
-
loading
|
|
97
|
+
toggle && 'toggle',
|
|
98
|
+
selected && 'selected',
|
|
99
|
+
loading && 'np-loading',
|
|
88
100
|
disabled || loading ? `${variant}-disabled disabled` : `${variant} enabled`,
|
|
89
101
|
attributes.class,
|
|
90
102
|
]}
|
|
91
103
|
>
|
|
92
104
|
{@render content()}
|
|
93
105
|
</button>
|
|
94
|
-
{:else if isLink(attributes)}
|
|
95
|
-
<a
|
|
96
|
-
{...attributes}
|
|
97
|
-
onclick={(event) => {
|
|
98
|
-
;(onclick as MouseEventHandler<HTMLAnchorElement>)?.(event)
|
|
99
|
-
}}
|
|
100
|
-
aria-describedby={title ? uid : attributes['aria-describedby']}
|
|
101
|
-
aria-label={title || attributes['aria-label']}
|
|
102
|
-
bind:this={element}
|
|
103
|
-
class={[
|
|
104
|
-
'np-button',
|
|
105
|
-
size,
|
|
106
|
-
selected || loading ? 'square' : shape,
|
|
107
|
-
toggle ? 'toggle' : '',
|
|
108
|
-
'enabled',
|
|
109
|
-
variant,
|
|
110
|
-
attributes.class,
|
|
111
|
-
]}
|
|
112
|
-
>
|
|
113
|
-
{@render content()}
|
|
114
|
-
</a>
|
|
115
106
|
{/if}
|
|
116
107
|
|
|
117
|
-
{#if title}
|
|
108
|
+
{#if title && !disabled && !loading}
|
|
118
109
|
<Tooltip {keepTooltipOnClick} id={uid}>{title}</Tooltip>
|
|
119
110
|
{/if}
|
|
120
111
|
|
|
@@ -258,10 +249,8 @@
|
|
|
258
249
|
background-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
|
|
259
250
|
}
|
|
260
251
|
.outlined-disabled {
|
|
261
|
-
outline-
|
|
262
|
-
|
|
263
|
-
outline-width: 1px;
|
|
264
|
-
outline-offset: -1px;
|
|
252
|
+
/* Variant outline now rendered via pseudo-element; keep token for color */
|
|
253
|
+
--_outlined-border-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
|
|
265
254
|
}
|
|
266
255
|
.enabled:focus-visible {
|
|
267
256
|
outline-style: solid;
|
|
@@ -363,19 +352,28 @@
|
|
|
363
352
|
}
|
|
364
353
|
.outlined {
|
|
365
354
|
background-color: var(--np-outlined-button-container-color, transparent);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
355
|
+
--_outlined-border-color: var(
|
|
356
|
+
--np-outlined-button-outline-color,
|
|
357
|
+
var(--np-color-outline-variant)
|
|
358
|
+
);
|
|
370
359
|
--np-ripple-hover-color: var(--np-outlined-button-label-text-color, var(--np-color-primary));
|
|
371
360
|
--np-ripple-pressed-color: var(--np-outlined-button-label-text-color, var(--np-color-primary));
|
|
372
361
|
color: var(--np-outlined-button-label-text-color, var(--np-color-primary));
|
|
373
362
|
}
|
|
374
363
|
|
|
364
|
+
.outlined:not(.selected)::after,
|
|
365
|
+
.outlined-disabled::after {
|
|
366
|
+
content: '';
|
|
367
|
+
position: absolute;
|
|
368
|
+
inset: 0;
|
|
369
|
+
border: 1px solid var(--_outlined-border-color);
|
|
370
|
+
border-radius: inherit;
|
|
371
|
+
pointer-events: none;
|
|
372
|
+
}
|
|
373
|
+
|
|
375
374
|
.outlined.selected {
|
|
376
375
|
background-color: var(--np-color-inverse-surface);
|
|
377
376
|
color: var(--np-color-inverse-on-surface);
|
|
378
|
-
outline-style: none;
|
|
379
377
|
}
|
|
380
378
|
.button-icon {
|
|
381
379
|
display: inline-flex;
|
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
import Ripple from '../ripple/Ripple.svelte'
|
|
4
4
|
import Tooltip from '../tooltip/Tooltip.svelte'
|
|
5
5
|
import type { IconButtonProps } from './types.ts'
|
|
6
|
-
import type {
|
|
7
|
-
HTMLAnchorAttributes,
|
|
8
|
-
HTMLButtonAttributes,
|
|
9
|
-
MouseEventHandler,
|
|
10
|
-
} from 'svelte/elements'
|
|
6
|
+
import type { HTMLButtonAttributes, MouseEventHandler } from 'svelte/elements'
|
|
11
7
|
|
|
12
8
|
let {
|
|
13
9
|
variant = 'text',
|
|
@@ -15,7 +11,7 @@
|
|
|
15
11
|
children,
|
|
16
12
|
title,
|
|
17
13
|
element = $bindable(),
|
|
18
|
-
disabled,
|
|
14
|
+
disabled = false,
|
|
19
15
|
loading = false,
|
|
20
16
|
loadingAriaLabel,
|
|
21
17
|
selected = $bindable(false),
|
|
@@ -30,13 +26,6 @@
|
|
|
30
26
|
|
|
31
27
|
const uid = $props.id()
|
|
32
28
|
let touchEl: HTMLSpanElement | undefined = $state()
|
|
33
|
-
|
|
34
|
-
const isButton = (obj: unknown): obj is HTMLButtonAttributes => {
|
|
35
|
-
return (obj as HTMLAnchorAttributes).href === undefined
|
|
36
|
-
}
|
|
37
|
-
const isLink = (obj: unknown): obj is HTMLAnchorAttributes => {
|
|
38
|
-
return (obj as HTMLAnchorAttributes).href !== undefined
|
|
39
|
-
}
|
|
40
29
|
</script>
|
|
41
30
|
|
|
42
31
|
{#snippet content()}
|
|
@@ -56,12 +45,37 @@
|
|
|
56
45
|
{/if}
|
|
57
46
|
{/snippet}
|
|
58
47
|
|
|
59
|
-
{#if
|
|
48
|
+
{#if 'href' in attributes && !disabled && !loading}
|
|
49
|
+
<a
|
|
50
|
+
{...attributes}
|
|
51
|
+
onclick={(event) => {
|
|
52
|
+
;(onclick as MouseEventHandler<HTMLAnchorElement>)?.(event)
|
|
53
|
+
}}
|
|
54
|
+
aria-describedby={title ? uid : undefined}
|
|
55
|
+
aria-label={title}
|
|
56
|
+
bind:this={element}
|
|
57
|
+
class={[
|
|
58
|
+
'np-icon-button',
|
|
59
|
+
size,
|
|
60
|
+
width,
|
|
61
|
+
variant,
|
|
62
|
+
selected ? 'square' : shape,
|
|
63
|
+
'enabled',
|
|
64
|
+
toggle && 'toggle',
|
|
65
|
+
selected && 'selected',
|
|
66
|
+
attributes.class,
|
|
67
|
+
].filter(Boolean)}
|
|
68
|
+
>
|
|
69
|
+
{@render content()}
|
|
70
|
+
</a>
|
|
71
|
+
{:else}
|
|
60
72
|
<button
|
|
73
|
+
{...attributes as HTMLButtonAttributes}
|
|
61
74
|
aria-describedby={title ? uid : attributes['aria-describedby']}
|
|
62
75
|
aria-label={title || attributes['aria-label']}
|
|
63
|
-
aria-pressed={selected}
|
|
64
|
-
{
|
|
76
|
+
aria-pressed={toggle ? selected : undefined}
|
|
77
|
+
aria-busy={loading}
|
|
78
|
+
type={(attributes['type'] as 'button' | 'submit' | 'reset' | 'button') ?? undefined}
|
|
65
79
|
disabled={disabled || loading}
|
|
66
80
|
bind:this={element}
|
|
67
81
|
onclick={(event) => {
|
|
@@ -77,38 +91,15 @@
|
|
|
77
91
|
selected || loading ? 'square' : shape,
|
|
78
92
|
disabled || loading ? `${variant}-disabled disabled` : `${variant} enabled`,
|
|
79
93
|
toggle && 'toggle',
|
|
80
|
-
selected
|
|
94
|
+
selected && 'selected',
|
|
81
95
|
attributes.class,
|
|
82
96
|
]}
|
|
83
97
|
>
|
|
84
98
|
{@render content()}
|
|
85
99
|
</button>
|
|
86
|
-
{:else if isLink(attributes)}
|
|
87
|
-
<a
|
|
88
|
-
{...attributes}
|
|
89
|
-
onclick={(event) => {
|
|
90
|
-
;(onclick as MouseEventHandler<HTMLAnchorElement>)?.(event)
|
|
91
|
-
}}
|
|
92
|
-
aria-describedby={title ? uid : undefined}
|
|
93
|
-
aria-label={title}
|
|
94
|
-
bind:this={element}
|
|
95
|
-
class={[
|
|
96
|
-
'np-icon-button',
|
|
97
|
-
size,
|
|
98
|
-
width,
|
|
99
|
-
variant,
|
|
100
|
-
selected || loading ? 'square' : shape,
|
|
101
|
-
'enabled',
|
|
102
|
-
toggle && 'toggle',
|
|
103
|
-
selected ? 'selected' : '',
|
|
104
|
-
attributes.class,
|
|
105
|
-
]}
|
|
106
|
-
>
|
|
107
|
-
{@render content()}
|
|
108
|
-
</a>
|
|
109
100
|
{/if}
|
|
110
101
|
|
|
111
|
-
{#if title}
|
|
102
|
+
{#if title && !disabled && !loading}
|
|
112
103
|
<Tooltip {keepTooltipOnClick} id={uid}>{title}</Tooltip>
|
|
113
104
|
{/if}
|
|
114
105
|
|
|
@@ -343,16 +334,26 @@
|
|
|
343
334
|
}
|
|
344
335
|
|
|
345
336
|
.outlined {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
337
|
+
--_outlined-border-color: var(
|
|
338
|
+
--np-outlined-icon-button-outline-color,
|
|
339
|
+
var(--np-color-outline-variant)
|
|
340
|
+
);
|
|
350
341
|
--np-ripple-hover-color: var(--np-color-on-surface-variant);
|
|
351
342
|
--np-ripple-pressed-color: var(--np-color-on-surface-variant);
|
|
352
343
|
color: var(--np-color-on-surface-variant);
|
|
353
344
|
}
|
|
345
|
+
|
|
346
|
+
.outlined:not(.selected)::after,
|
|
347
|
+
.outlined-disabled::after {
|
|
348
|
+
content: '';
|
|
349
|
+
position: absolute;
|
|
350
|
+
inset: 0;
|
|
351
|
+
border: 1px solid var(--_outlined-border-color);
|
|
352
|
+
border-radius: inherit;
|
|
353
|
+
pointer-events: none;
|
|
354
|
+
}
|
|
355
|
+
|
|
354
356
|
.outlined.selected {
|
|
355
|
-
outline-style: none;
|
|
356
357
|
--np-ripple-hover-color: var(--np-color-on-surface-variant);
|
|
357
358
|
--np-ripple-pressed-color: var(--np-color-on-surface-variant);
|
|
358
359
|
color: var(--np-color-inverse-on-surface);
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
3
3
|
import type { NavigationDrawerItemProps } from './types.ts'
|
|
4
4
|
import Ripple from '../ripple/Ripple.svelte'
|
|
5
5
|
|
|
@@ -10,13 +10,6 @@
|
|
|
10
10
|
icon,
|
|
11
11
|
...attributes
|
|
12
12
|
}: NavigationDrawerItemProps = $props()
|
|
13
|
-
|
|
14
|
-
const isButton = (obj: unknown): obj is HTMLButtonAttributes => {
|
|
15
|
-
return (obj as HTMLAnchorAttributes).href === undefined
|
|
16
|
-
}
|
|
17
|
-
const isLink = (obj: unknown): obj is HTMLAnchorAttributes => {
|
|
18
|
-
return (obj as HTMLAnchorAttributes).href !== undefined
|
|
19
|
-
}
|
|
20
13
|
</script>
|
|
21
14
|
|
|
22
15
|
{#snippet content()}
|
|
@@ -30,9 +23,9 @@
|
|
|
30
23
|
<div class="np-navigation-drawer-item-badge">{badgeLabelText}</div>
|
|
31
24
|
{/snippet}
|
|
32
25
|
|
|
33
|
-
{#if
|
|
34
|
-
<
|
|
35
|
-
{...attributes
|
|
26
|
+
{#if 'href' in attributes}
|
|
27
|
+
<a
|
|
28
|
+
{...attributes}
|
|
36
29
|
class={[
|
|
37
30
|
'np-navigation-drawer-item',
|
|
38
31
|
selected && 'np-navigation-drawer-item-selected',
|
|
@@ -40,10 +33,10 @@
|
|
|
40
33
|
]}
|
|
41
34
|
>
|
|
42
35
|
{@render content()}
|
|
43
|
-
</
|
|
44
|
-
{:else
|
|
45
|
-
<
|
|
46
|
-
{...attributes}
|
|
36
|
+
</a>
|
|
37
|
+
{:else}
|
|
38
|
+
<button
|
|
39
|
+
{...attributes as HTMLButtonAttributes}
|
|
47
40
|
class={[
|
|
48
41
|
'np-navigation-drawer-item',
|
|
49
42
|
selected && 'np-navigation-drawer-item-selected',
|
|
@@ -51,7 +44,7 @@
|
|
|
51
44
|
]}
|
|
52
45
|
>
|
|
53
46
|
{@render content()}
|
|
54
|
-
</
|
|
47
|
+
</button>
|
|
55
48
|
{/if}
|
|
56
49
|
|
|
57
50
|
<style>
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
3
3
|
import type { NavigationRailItemProps } from './types.ts'
|
|
4
4
|
import Ripple from '../ripple/Ripple.svelte'
|
|
5
5
|
|
|
6
6
|
let { selected, icon, label, ...attributes }: NavigationRailItemProps = $props()
|
|
7
7
|
let touchEl: HTMLSpanElement | undefined = $state()
|
|
8
|
-
|
|
9
|
-
const isButton = (obj: unknown): obj is HTMLButtonAttributes => {
|
|
10
|
-
return (obj as HTMLAnchorAttributes).href === undefined
|
|
11
|
-
}
|
|
12
|
-
const isLink = (obj: unknown): obj is HTMLAnchorAttributes => {
|
|
13
|
-
return (obj as HTMLAnchorAttributes).href !== undefined
|
|
14
|
-
}
|
|
15
8
|
</script>
|
|
16
9
|
|
|
17
10
|
{#snippet content()}
|
|
@@ -23,20 +16,20 @@
|
|
|
23
16
|
<span class="np-touch" bind:this={touchEl}></span>
|
|
24
17
|
{/snippet}
|
|
25
18
|
|
|
26
|
-
{#if
|
|
27
|
-
<button
|
|
28
|
-
{...attributes as HTMLButtonAttributes}
|
|
29
|
-
class={['np-navigation-action', selected && 'np-navigation-action-selected', attributes.class]}
|
|
30
|
-
>
|
|
31
|
-
{@render content()}
|
|
32
|
-
</button>
|
|
33
|
-
{:else if isLink(attributes)}
|
|
19
|
+
{#if 'href' in attributes}
|
|
34
20
|
<a
|
|
35
21
|
{...attributes}
|
|
36
22
|
class={['np-navigation-action', selected && 'np-navigation-action-selected', attributes.class]}
|
|
37
23
|
>
|
|
38
24
|
{@render content()}
|
|
39
25
|
</a>
|
|
26
|
+
{:else}
|
|
27
|
+
<button
|
|
28
|
+
{...attributes as HTMLButtonAttributes}
|
|
29
|
+
class={['np-navigation-action', selected && 'np-navigation-action-selected', attributes.class]}
|
|
30
|
+
>
|
|
31
|
+
{@render content()}
|
|
32
|
+
</button>
|
|
40
33
|
{/if}
|
|
41
34
|
|
|
42
35
|
<style>
|
package/dist/radio/Radio.svelte
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
...attributes
|
|
12
12
|
}: RadioProps = $props()
|
|
13
13
|
|
|
14
|
-
let inputEl
|
|
14
|
+
let inputEl = $state<HTMLInputElement>()
|
|
15
|
+
const uid = $props.id()
|
|
15
16
|
</script>
|
|
16
17
|
|
|
17
18
|
<label {style} class={['np-host', attributes.class]} bind:this={element}>
|
|
@@ -20,11 +21,11 @@
|
|
|
20
21
|
<Ripple forElement={inputEl} class="np-radio-ripple" />
|
|
21
22
|
{/if}
|
|
22
23
|
<svg class="np-radio-icon" viewBox="0 0 20 20">
|
|
23
|
-
<mask id="
|
|
24
|
+
<mask id="{uid}-mask">
|
|
24
25
|
<rect width="100%" height="100%" fill="white" />
|
|
25
26
|
<circle cx="10" cy="10" r="8" fill="black" />
|
|
26
27
|
</mask>
|
|
27
|
-
<circle class="outer circle" cx="10" cy="10" r="10" mask="url(#
|
|
28
|
+
<circle class="outer circle" cx="10" cy="10" r="10" mask="url(#{uid}-mask)" />
|
|
28
29
|
<circle class="inner circle" cx="10" cy="10" r="5" />
|
|
29
30
|
</svg>
|
|
30
31
|
{#if group !== undefined}
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
value = options.find((option) => option.selected)?.value
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
let selectedOption: SelectOption[] = $
|
|
48
|
+
let selectedOption: SelectOption[] = $derived(
|
|
49
49
|
options
|
|
50
|
-
.filter(
|
|
51
|
-
option
|
|
52
|
-
|
|
53
|
-
: value === option.value
|
|
50
|
+
.filter(
|
|
51
|
+
(option) =>
|
|
52
|
+
option.selected ||
|
|
53
|
+
(Array.isArray(value) ? value.includes(option.value) : value === option.value),
|
|
54
54
|
)
|
|
55
55
|
.map((option) => ({ ...option, selected: true })),
|
|
56
56
|
)
|
|
@@ -59,13 +59,27 @@
|
|
|
59
59
|
|
|
60
60
|
let errorTextRaw: string = $state(errorText)
|
|
61
61
|
let errorRaw = $state(error)
|
|
62
|
-
let selectElement
|
|
63
|
-
let menuElement
|
|
64
|
-
let anchorElement
|
|
65
|
-
let field
|
|
62
|
+
let selectElement = $state<HTMLSelectElement>()
|
|
63
|
+
let menuElement = $state<HTMLDivElement>()
|
|
64
|
+
let anchorElement = $state<HTMLDivElement>()
|
|
65
|
+
let field = $state<HTMLDivElement>()
|
|
66
66
|
let clientWidth = $state(0)
|
|
67
67
|
let menuOpen = $state(false)
|
|
68
|
-
let
|
|
68
|
+
let focusIndex = $state(-1)
|
|
69
|
+
let typeBuffer = ''
|
|
70
|
+
let lastTypeTime = 0
|
|
71
|
+
|
|
72
|
+
let activeDescendantId = $derived.by<string | undefined>(() => {
|
|
73
|
+
if (!menuOpen) return undefined
|
|
74
|
+
if (focusIndex >= 0 && focusIndex < options.length) return `${uid}-opt-${focusIndex}`
|
|
75
|
+
const fallbackIdx = multiple
|
|
76
|
+
? Array.isArray(value) && value.length
|
|
77
|
+
? options.findIndex((o) => o.value === value[0])
|
|
78
|
+
: -1
|
|
79
|
+
: options.findIndex((o) => o.value === value)
|
|
80
|
+
return fallbackIdx >= 0 ? `${uid}-opt-${fallbackIdx}` : undefined
|
|
81
|
+
})
|
|
82
|
+
let selectedLabel = $derived.by<string>(() => {
|
|
69
83
|
if (multiple) {
|
|
70
84
|
if (value && Array.isArray(value)) {
|
|
71
85
|
return value
|
|
@@ -143,6 +157,82 @@
|
|
|
143
157
|
selectElement?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
144
158
|
})
|
|
145
159
|
}
|
|
160
|
+
|
|
161
|
+
const openMenuAndFocus = async (index: number) => {
|
|
162
|
+
if (!menuOpen) {
|
|
163
|
+
menuElement?.showPopover()
|
|
164
|
+
}
|
|
165
|
+
focusIndex = Math.min(Math.max(index, 0), options.length - 1)
|
|
166
|
+
await tick()
|
|
167
|
+
const el = document.getElementById(`${uid}-opt-${focusIndex}`)
|
|
168
|
+
;(el as HTMLElement | null)?.focus?.()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const moveFocus = (delta: number) => {
|
|
172
|
+
if (!options.length) return
|
|
173
|
+
let next = focusIndex
|
|
174
|
+
if (next < 0) {
|
|
175
|
+
const selIdx = Array.isArray(value)
|
|
176
|
+
? options.findIndex((o) => value.includes(o.value) && !o.disabled)
|
|
177
|
+
: options.findIndex((o) => o.value === value && !o.disabled)
|
|
178
|
+
next = selIdx >= 0 ? selIdx : 0
|
|
179
|
+
}
|
|
180
|
+
let attempts = 0
|
|
181
|
+
while (attempts < options.length) {
|
|
182
|
+
next = (next + delta + options.length) % options.length
|
|
183
|
+
if (!options[next].disabled) {
|
|
184
|
+
openMenuAndFocus(next)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
attempts++
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const focusEdge = (start: boolean) => {
|
|
192
|
+
if (!options.length) {
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
if (start) {
|
|
196
|
+
for (let i = 0; i < options.length; i++) {
|
|
197
|
+
if (!options[i].disabled) {
|
|
198
|
+
openMenuAndFocus(i)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
for (let i = options.length - 1; i >= 0; i--) {
|
|
204
|
+
if (!options[i].disabled) {
|
|
205
|
+
openMenuAndFocus(i)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const performTypeahead = (char: string) => {
|
|
213
|
+
const now = performance.now()
|
|
214
|
+
if (now - lastTypeTime > 700) typeBuffer = ''
|
|
215
|
+
lastTypeTime = now
|
|
216
|
+
typeBuffer += char.toLowerCase()
|
|
217
|
+
if (!options.length) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
const startIdx = focusIndex >= 0 ? (focusIndex + 1) % options.length : 0
|
|
221
|
+
for (let i = 0; i < options.length; i++) {
|
|
222
|
+
const idx = (startIdx + i) % options.length
|
|
223
|
+
const label = options[idx].label?.toLowerCase?.() || ''
|
|
224
|
+
if (label.startsWith(typeBuffer) && !options[idx].disabled) {
|
|
225
|
+
openMenuAndFocus(idx)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeBuffer.length > 1) {
|
|
231
|
+
const last = typeBuffer[typeBuffer.length - 1]
|
|
232
|
+
typeBuffer = last
|
|
233
|
+
performTypeahead('')
|
|
234
|
+
}
|
|
235
|
+
}
|
|
146
236
|
</script>
|
|
147
237
|
|
|
148
238
|
{#snippet arrows()}
|
|
@@ -175,10 +265,11 @@
|
|
|
175
265
|
role="combobox"
|
|
176
266
|
aria-haspopup="listbox"
|
|
177
267
|
tabindex={disabled ? -1 : tabindex}
|
|
178
|
-
aria-controls="listbox"
|
|
268
|
+
aria-controls="listbox-{uid}"
|
|
179
269
|
aria-expanded={menuOpen}
|
|
180
270
|
aria-label={attributes['aria-label'] || label}
|
|
181
271
|
aria-disabled={disabled}
|
|
272
|
+
aria-activedescendant={activeDescendantId}
|
|
182
273
|
data-testid={attributes['data-testid']}
|
|
183
274
|
bind:this={field}
|
|
184
275
|
bind:clientWidth
|
|
@@ -192,19 +283,51 @@
|
|
|
192
283
|
}
|
|
193
284
|
}}
|
|
194
285
|
onkeydown={(event) => {
|
|
195
|
-
|
|
286
|
+
const key = event.key
|
|
287
|
+
if (key === 'Tab') {
|
|
196
288
|
menuElement?.hidePopover()
|
|
197
|
-
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
if (key === 'Escape') {
|
|
292
|
+
menuElement?.hidePopover()
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
if (key === 'ArrowDown') {
|
|
198
296
|
event.preventDefault()
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
|
|
297
|
+
moveFocus(1)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
if (key === 'ArrowUp') {
|
|
301
|
+
event.preventDefault()
|
|
302
|
+
moveFocus(-1)
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
if (key === 'Home') {
|
|
306
|
+
event.preventDefault()
|
|
307
|
+
focusEdge(true)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
if (key === 'End') {
|
|
311
|
+
event.preventDefault()
|
|
312
|
+
focusEdge(false)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (key === 'Enter' || key === ' ') {
|
|
316
|
+
event.preventDefault()
|
|
317
|
+
if (!menuOpen) {
|
|
318
|
+
openMenuAndFocus(focusIndex >= 0 ? focusIndex : 0)
|
|
319
|
+
} else if (focusIndex >= 0) {
|
|
320
|
+
const opt = options[focusIndex]
|
|
321
|
+
if (opt && !opt.disabled) {
|
|
322
|
+
handleOptionSelect(event, opt)
|
|
323
|
+
}
|
|
207
324
|
}
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
// Printable character for typeahead
|
|
328
|
+
if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
329
|
+
performTypeahead(key)
|
|
330
|
+
return
|
|
208
331
|
}
|
|
209
332
|
}}
|
|
210
333
|
>
|
|
@@ -222,7 +345,7 @@
|
|
|
222
345
|
<span class={['label', !noAsterisk && required && 'required']}>{label}</span>
|
|
223
346
|
</div>
|
|
224
347
|
<div class="outline-notch">
|
|
225
|
-
<span class="notch
|
|
348
|
+
<span class="notch" aria-hidden="true"
|
|
226
349
|
>{label}{noAsterisk || !required ? '' : '*'}</span
|
|
227
350
|
>
|
|
228
351
|
</div>
|
|
@@ -334,33 +457,44 @@
|
|
|
334
457
|
</div>
|
|
335
458
|
</div>
|
|
336
459
|
|
|
337
|
-
{#snippet item(option: SelectOption)}
|
|
460
|
+
{#snippet item(option: SelectOption, index: number)}
|
|
338
461
|
{#if Array.isArray(value) && multiple}
|
|
339
462
|
<Item
|
|
463
|
+
id="{uid}-opt-{index}"
|
|
340
464
|
onclick={(event) => {
|
|
341
465
|
handleOptionSelect(event, option)
|
|
342
466
|
field?.focus()
|
|
343
467
|
}}
|
|
468
|
+
tabindex={-1}
|
|
344
469
|
disabled={option.disabled}
|
|
470
|
+
aria-disabled={option.disabled}
|
|
345
471
|
role="option"
|
|
346
472
|
onkeydown={(event) => {
|
|
347
|
-
|
|
348
|
-
|
|
473
|
+
const key = event.key
|
|
474
|
+
if (key === 'ArrowDown') {
|
|
349
475
|
event.preventDefault()
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
476
|
+
moveFocus(1)
|
|
477
|
+
} else if (key === 'ArrowUp') {
|
|
478
|
+
event.preventDefault()
|
|
479
|
+
moveFocus(-1)
|
|
480
|
+
} else if (key === 'Home') {
|
|
481
|
+
event.preventDefault()
|
|
482
|
+
focusEdge(true)
|
|
483
|
+
} else if (key === 'End') {
|
|
484
|
+
event.preventDefault()
|
|
485
|
+
focusEdge(false)
|
|
486
|
+
} else if (key === 'Enter' || key === ' ') {
|
|
353
487
|
event.preventDefault()
|
|
354
|
-
}
|
|
355
|
-
if (event.key === 'Enter') {
|
|
356
488
|
handleOptionSelect(event, option)
|
|
357
|
-
}
|
|
358
|
-
if (event.key === 'Tab') {
|
|
489
|
+
} else if (key === 'Tab') {
|
|
359
490
|
menuElement?.hidePopover()
|
|
491
|
+
} else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
492
|
+
performTypeahead(key)
|
|
360
493
|
}
|
|
361
494
|
}}
|
|
362
495
|
variant="button"
|
|
363
496
|
selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
497
|
+
aria-selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
364
498
|
>{option.label}
|
|
365
499
|
{#snippet start()}
|
|
366
500
|
<Check disabled={option.disabled} checked={value.includes(option.value)} />
|
|
@@ -368,40 +502,53 @@
|
|
|
368
502
|
</Item>
|
|
369
503
|
{:else}
|
|
370
504
|
<Item
|
|
505
|
+
id="{uid}-opt-{index}"
|
|
371
506
|
onclick={(event) => {
|
|
372
507
|
handleOptionSelect(event, option)
|
|
373
508
|
field?.focus()
|
|
374
509
|
}}
|
|
510
|
+
tabindex={-1}
|
|
375
511
|
disabled={option.disabled}
|
|
512
|
+
aria-disabled={option.disabled}
|
|
376
513
|
role="option"
|
|
377
514
|
onkeydown={(event) => {
|
|
378
|
-
|
|
379
|
-
|
|
515
|
+
const key = event.key
|
|
516
|
+
if (key === 'ArrowDown') {
|
|
380
517
|
event.preventDefault()
|
|
381
|
-
|
|
382
|
-
if (
|
|
383
|
-
|
|
518
|
+
moveFocus(1)
|
|
519
|
+
} else if (key === 'ArrowUp') {
|
|
520
|
+
event.preventDefault()
|
|
521
|
+
moveFocus(-1)
|
|
522
|
+
} else if (key === 'Home') {
|
|
523
|
+
event.preventDefault()
|
|
524
|
+
focusEdge(true)
|
|
525
|
+
} else if (key === 'End') {
|
|
526
|
+
event.preventDefault()
|
|
527
|
+
focusEdge(false)
|
|
528
|
+
} else if (key === 'Enter' || key === ' ') {
|
|
384
529
|
event.preventDefault()
|
|
385
|
-
}
|
|
386
|
-
if (event.key === 'Enter') {
|
|
387
530
|
handleOptionSelect(event, option)
|
|
388
|
-
}
|
|
389
|
-
if (event.key === 'Tab') {
|
|
531
|
+
} else if (key === 'Tab') {
|
|
390
532
|
menuElement?.hidePopover()
|
|
533
|
+
} else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
534
|
+
performTypeahead(key)
|
|
391
535
|
}
|
|
392
536
|
}}
|
|
393
537
|
variant="button"
|
|
394
538
|
selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
539
|
+
aria-selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
395
540
|
>{option.label}
|
|
396
541
|
</Item>
|
|
397
542
|
{/if}
|
|
398
543
|
{/snippet}
|
|
399
544
|
|
|
400
545
|
<Menu
|
|
546
|
+
id="listbox-{uid}"
|
|
401
547
|
style="position-anchor:--{uid};{clampMenuWidth || useVirtualList
|
|
402
548
|
? 'width'
|
|
403
549
|
: 'min-width'}:{clientWidth}px"
|
|
404
550
|
role="listbox"
|
|
551
|
+
aria-multiselectable={multiple}
|
|
405
552
|
--np-menu-justify-self="none"
|
|
406
553
|
--np-menu-position-area="bottom span-right"
|
|
407
554
|
--np-menu-margin="2px 0"
|
|
@@ -409,24 +556,41 @@
|
|
|
409
556
|
? 'var(--np-outlined-select-text-field-container-shape)'
|
|
410
557
|
: 'var(--np-filled-select-text-field-container-shape)'}
|
|
411
558
|
anchor={anchorElement}
|
|
412
|
-
ontoggle={({ newState }) => {
|
|
559
|
+
ontoggle={async ({ newState }) => {
|
|
413
560
|
if (newState === 'open') {
|
|
414
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?.()
|
|
415
578
|
} else {
|
|
416
579
|
menuOpen = false
|
|
580
|
+
focusIndex = -1
|
|
417
581
|
}
|
|
418
582
|
}}
|
|
419
583
|
bind:element={menuElement}
|
|
420
584
|
>
|
|
421
585
|
{#if useVirtualList}
|
|
422
586
|
<VirtualList height="250px" itemHeight={48} items={options}>
|
|
423
|
-
{#snippet row(option)}
|
|
424
|
-
{@render item(option)}
|
|
587
|
+
{#snippet row(option, index)}
|
|
588
|
+
{@render item(option, index)}
|
|
425
589
|
{/snippet}
|
|
426
590
|
</VirtualList>
|
|
427
591
|
{:else}
|
|
428
592
|
{#each options as option, index (index)}
|
|
429
|
-
{@render item(option)}
|
|
593
|
+
{@render item(option, index)}
|
|
430
594
|
{/each}
|
|
431
595
|
{/if}
|
|
432
596
|
</Menu>
|
|
@@ -763,12 +927,6 @@
|
|
|
763
927
|
.notch {
|
|
764
928
|
font-size: 0.75rem;
|
|
765
929
|
line-height: 1rem;
|
|
766
|
-
}
|
|
767
|
-
.notch.np-hidden {
|
|
768
|
-
opacity: 0;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
.label.np-hidden {
|
|
772
930
|
opacity: 0;
|
|
773
931
|
}
|
|
774
932
|
|
|
@@ -828,7 +986,7 @@
|
|
|
828
986
|
.disabled .label {
|
|
829
987
|
color: var(--np-color-on-surface);
|
|
830
988
|
}
|
|
831
|
-
.disabled
|
|
989
|
+
.disabled {
|
|
832
990
|
opacity: 0.38;
|
|
833
991
|
}
|
|
834
992
|
.resizable:not(.disabled) .np-container {
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { onMount, tick, type Snippet } from 'svelte'
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements'
|
|
4
4
|
|
|
5
|
-
interface
|
|
5
|
+
interface VirtualListProps extends HTMLAttributes<HTMLDivElement> {
|
|
6
6
|
items: T[]
|
|
7
7
|
height?: string
|
|
8
8
|
itemHeight?: number
|
|
9
9
|
start?: number
|
|
10
10
|
end?: number
|
|
11
|
-
row: Snippet<[T]>
|
|
11
|
+
row: Snippet<[T, number]>
|
|
12
|
+
overscan?: number
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
let {
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
start = $bindable(0),
|
|
19
20
|
end = $bindable(0),
|
|
20
21
|
row,
|
|
21
|
-
|
|
22
|
+
overscan = 4,
|
|
23
|
+
}: VirtualListProps = $props()
|
|
22
24
|
|
|
23
25
|
let height_map: number[] = []
|
|
24
26
|
// eslint-disable-next-line no-undef
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
|
|
31
33
|
let top = $state(0)
|
|
32
34
|
let bottom = $state(0)
|
|
33
|
-
let average_height: number = $state(0)
|
|
35
|
+
let average_height: number = $state(itemHeight || 0)
|
|
34
36
|
|
|
35
37
|
$effect(() => {
|
|
36
38
|
if (mounted) {
|
|
@@ -38,9 +40,10 @@
|
|
|
38
40
|
}
|
|
39
41
|
})
|
|
40
42
|
let visible = $derived(
|
|
41
|
-
items.slice(start, end).map((data, i) => {
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
items.slice(Math.max(0, start - overscan), end).map((data, i) => ({
|
|
44
|
+
index: i + Math.max(0, start - overscan),
|
|
45
|
+
data,
|
|
46
|
+
})),
|
|
44
47
|
)
|
|
45
48
|
|
|
46
49
|
async function refresh(items: T[], viewport_height: number, itemHeight?: number) {
|
|
@@ -68,10 +71,11 @@
|
|
|
68
71
|
i += 1
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
i = Math.min(i + overscan, items.length)
|
|
71
75
|
end = i
|
|
72
76
|
|
|
73
77
|
const remaining = items.length - end
|
|
74
|
-
average_height = (top + content_height) / end
|
|
78
|
+
average_height = end ? (top + content_height) / end : average_height || itemHeight || 0
|
|
75
79
|
|
|
76
80
|
bottom = remaining * average_height
|
|
77
81
|
height_map.length = items.length
|
|
@@ -98,14 +102,24 @@
|
|
|
98
102
|
if (y + row_height > scrollTop) {
|
|
99
103
|
start = i
|
|
100
104
|
top = y
|
|
101
|
-
|
|
102
105
|
break
|
|
103
106
|
}
|
|
104
|
-
|
|
105
107
|
y += row_height
|
|
106
108
|
i += 1
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
if (start > 0 && overscan > 0) {
|
|
112
|
+
let back = 0
|
|
113
|
+
let s = start
|
|
114
|
+
while (s > 0 && back < overscan) {
|
|
115
|
+
s -= 1
|
|
116
|
+
const h = height_map[s] || average_height
|
|
117
|
+
back += 1
|
|
118
|
+
top -= h
|
|
119
|
+
}
|
|
120
|
+
start = s
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
while (i < items.length) {
|
|
110
124
|
y += height_map[i] || average_height
|
|
111
125
|
i += 1
|
|
@@ -116,7 +130,7 @@
|
|
|
116
130
|
end = i
|
|
117
131
|
|
|
118
132
|
const remaining = items.length - end
|
|
119
|
-
average_height = y / end
|
|
133
|
+
average_height = end ? y / end : average_height || itemHeight || 0
|
|
120
134
|
|
|
121
135
|
height_map.fill(average_height, i, items.length)
|
|
122
136
|
bottom = remaining * average_height
|
|
@@ -137,13 +151,8 @@
|
|
|
137
151
|
const d = actual_height - expected_height
|
|
138
152
|
viewport.scrollTo(0, scrollTop + d)
|
|
139
153
|
}
|
|
140
|
-
|
|
141
|
-
// TODO if we overestimated the space these
|
|
142
|
-
// rows would occupy we may need to add some
|
|
143
|
-
// more. maybe we can just call handle_scroll again?
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
// trigger initial refresh
|
|
147
156
|
onMount(() => {
|
|
148
157
|
// eslint-disable-next-line no-undef
|
|
149
158
|
rows = contents?.children as HTMLCollectionOf<HTMLElement>
|
|
@@ -159,7 +168,7 @@
|
|
|
159
168
|
>
|
|
160
169
|
<div bind:this={contents} style="padding-top: {top}px; padding-bottom: {bottom}px;">
|
|
161
170
|
{#each visible as entry (entry.index)}
|
|
162
|
-
{@render row(entry.data)}
|
|
171
|
+
{@render row(entry.data, entry.index)}
|
|
163
172
|
{/each}
|
|
164
173
|
</div>
|
|
165
174
|
</svelte-virtual-list-viewport>
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
}: SnackbarProps = $props()
|
|
23
23
|
|
|
24
24
|
let timeoutId: number | undefined = $state()
|
|
25
|
+
const uid = $props.id()
|
|
25
26
|
|
|
26
27
|
showPopover = () => {
|
|
27
28
|
element?.showPopover()
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
class={['np-snackbar', attributes.class]}
|
|
39
40
|
bind:this={element}
|
|
40
41
|
role="alert"
|
|
41
|
-
aria-label
|
|
42
|
+
aria-labelledby="np-snackbar-label-{uid}"
|
|
42
43
|
onbeforetoggle={(event) => {
|
|
43
44
|
let { newState } = event
|
|
44
45
|
if (newState === 'closed') {
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
>
|
|
55
56
|
<div class="np-snackbar-inner">
|
|
56
57
|
<div class="np-snackbar-label-container">
|
|
57
|
-
<div class="np-snackbar-label">{label}</div>
|
|
58
|
+
<div id="np-snackbar-label-{uid}" class="np-snackbar-label">{label}</div>
|
|
58
59
|
{#if supportingText}
|
|
59
60
|
<div class="np-snackbar-supporting-text">{supportingText}</div>
|
|
60
61
|
{/if}
|