noph-ui 0.44.0 → 0.45.0
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 +10 -1
- package/dist/animation.d.ts +17 -0
- package/dist/animation.js +48 -0
- package/dist/app-bar/AppBar.svelte +280 -0
- package/dist/app-bar/AppBar.svelte.d.ts +4 -0
- package/dist/app-bar/index.d.ts +1 -0
- package/dist/app-bar/index.js +1 -0
- package/dist/app-bar/types.d.ts +35 -0
- package/dist/app-bar/types.js +1 -0
- package/dist/autocomplete/AutoComplete.svelte +10 -11
- package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
- package/dist/autocomplete/types.d.ts +1 -1
- package/dist/badge/Badge.svelte +6 -1
- package/dist/badge/types.d.ts +0 -1
- package/dist/button/Button.svelte +5 -15
- package/dist/button/IconButton.svelte +5 -15
- package/dist/button/SegmentedButton.svelte +15 -1
- package/dist/button/SplitButton.svelte +92 -0
- package/dist/button/SplitButton.svelte.d.ts +4 -0
- package/dist/button/index.d.ts +1 -0
- package/dist/button/index.js +1 -0
- package/dist/button/types.d.ts +23 -4
- package/dist/card/types.d.ts +1 -1
- package/dist/checkbox/Checkbox.svelte +49 -58
- package/dist/chip/AssistChip.svelte +5 -5
- package/dist/chip/ChipSet.svelte +3 -11
- package/dist/chip/FilterChip.svelte +35 -27
- package/dist/chip/InputChip.svelte +13 -2
- package/dist/chip/SuggestionChip.svelte +29 -0
- package/dist/chip/SuggestionChip.svelte.d.ts +4 -0
- package/dist/chip/index.d.ts +1 -0
- package/dist/chip/index.js +1 -0
- package/dist/chip/types.d.ts +24 -9
- package/dist/date-picker/Calendar.svelte +1 -1
- package/dist/date-picker/DatePickerDialog.svelte +30 -8
- package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -1
- package/dist/date-picker/DateRangePicker.svelte +45 -10
- package/dist/date-picker/DateRangePicker.svelte.d.ts +4 -1
- package/dist/date-picker/DockedDatePicker.svelte +18 -4
- package/dist/date-picker/DockedDatePicker.svelte.d.ts +4 -1
- package/dist/date-picker/DockedDateTimePicker.svelte +18 -4
- package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +4 -1
- package/dist/date-picker/SelectionList.svelte +6 -32
- package/dist/date-picker/SelectionList.svelte.d.ts +2 -12
- package/dist/date-picker/TimeColumn.svelte +4 -29
- package/dist/date-picker/exitVisibility.svelte.d.ts +10 -0
- package/dist/date-picker/exitVisibility.svelte.js +30 -0
- package/dist/date-picker/optionList.d.ts +16 -0
- package/dist/date-picker/optionList.js +43 -0
- package/dist/date-picker/types.d.ts +11 -4
- package/dist/dialog/Dialog.svelte +77 -68
- package/dist/dialog/Dialog.svelte.d.ts +3 -3
- package/dist/dialog/types.d.ts +5 -3
- package/dist/fab/ExtendedFab.svelte +281 -0
- package/dist/fab/ExtendedFab.svelte.d.ts +5 -0
- package/dist/fab/Fab.svelte +239 -0
- package/dist/fab/Fab.svelte.d.ts +5 -0
- package/dist/fab/FabMenu.svelte +255 -0
- package/dist/fab/FabMenu.svelte.d.ts +7 -0
- package/dist/fab/index.d.ts +3 -0
- package/dist/fab/index.js +3 -0
- package/dist/fab/types.d.ts +58 -0
- package/dist/fab/types.js +1 -0
- package/dist/icons/ArrowBackIcon.svelte +13 -0
- package/dist/icons/ArrowBackIcon.svelte.d.ts +7 -0
- package/dist/icons/ChevronDownIcon.svelte +3 -0
- package/dist/icons/ChevronDownIcon.svelte.d.ts +26 -0
- package/dist/icons/SearchIcon.svelte +15 -0
- package/dist/icons/SearchIcon.svelte.d.ts +7 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons/index.js +3 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/internal/focus-ring.css +12 -0
- package/dist/menu/Menu.svelte +12 -7
- package/dist/menu/Menu.svelte.d.ts +2 -2
- package/dist/menu/types.d.ts +0 -2
- package/dist/navigation-bar/NavigationBar.svelte +66 -0
- package/dist/navigation-bar/NavigationBar.svelte.d.ts +4 -0
- package/dist/navigation-bar/NavigationBarItem.svelte +261 -0
- package/dist/navigation-bar/NavigationBarItem.svelte.d.ts +5 -0
- package/dist/navigation-bar/index.d.ts +2 -0
- package/dist/navigation-bar/index.js +2 -0
- package/dist/navigation-bar/types.d.ts +29 -0
- package/dist/navigation-bar/types.js +1 -0
- package/dist/navigation-drawer/NavigationDrawer.svelte +128 -76
- package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +4 -1
- package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -3
- package/dist/navigation-drawer/types.d.ts +6 -1
- package/dist/navigation-rail/NavigationRailItem.svelte +16 -0
- package/dist/navigation-rail/types.d.ts +3 -0
- package/dist/popover.svelte.d.ts +14 -0
- package/dist/popover.svelte.js +31 -0
- package/dist/press.svelte.d.ts +19 -0
- package/dist/press.svelte.js +45 -0
- package/dist/radio/Radio.svelte +26 -7
- package/dist/radio/types.d.ts +3 -0
- package/dist/search/Search.svelte +472 -0
- package/dist/search/Search.svelte.d.ts +5 -0
- package/dist/search/index.d.ts +1 -0
- package/dist/search/index.js +1 -0
- package/dist/search/types.d.ts +39 -0
- package/dist/search/types.js +1 -0
- package/dist/select/Select.svelte +48 -87
- package/dist/sheet/Sheet.svelte +260 -0
- package/dist/sheet/Sheet.svelte.d.ts +7 -0
- package/dist/sheet/index.d.ts +1 -0
- package/dist/sheet/index.js +1 -0
- package/dist/sheet/types.d.ts +19 -0
- package/dist/sheet/types.js +1 -0
- package/dist/slider/Slider.svelte +154 -30
- package/dist/slider/types.d.ts +3 -0
- package/dist/snackbar/Snackbar.svelte +48 -37
- package/dist/snackbar/Snackbar.svelte.d.ts +2 -2
- package/dist/snackbar/types.d.ts +3 -2
- package/dist/switch/Switch.svelte +51 -20
- package/dist/switch/types.d.ts +3 -0
- package/dist/tabs/Tab.svelte +8 -12
- package/dist/tabs/Tabs.svelte +3 -3
- package/dist/text-field/TextField.svelte +2 -2
- package/dist/toolbar/Toolbar.svelte +133 -0
- package/dist/toolbar/Toolbar.svelte.d.ts +4 -0
- package/dist/toolbar/index.d.ts +1 -0
- package/dist/toolbar/index.js +1 -0
- package/dist/toolbar/types.d.ts +15 -0
- package/dist/toolbar/types.js +1 -0
- package/dist/tooltip/RichTooltip.svelte +25 -12
- package/dist/tooltip/RichTooltip.svelte.d.ts +2 -2
- package/dist/tooltip/Tooltip.svelte +22 -11
- package/dist/tooltip/Tooltip.svelte.d.ts +4 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.js +6 -0
- package/package.json +13 -13
- package/dist/date-picker/enterAnimation.d.ts +0 -1
- package/dist/date-picker/enterAnimation.js +0 -10
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import '../internal/focus-ring.css'
|
|
3
|
+
import IconButton from '../button/IconButton.svelte'
|
|
4
|
+
import ArrowBackIcon from '../icons/ArrowBackIcon.svelte'
|
|
5
|
+
import CloseIcon from '../icons/CloseIcon.svelte'
|
|
6
|
+
import SearchIcon from '../icons/SearchIcon.svelte'
|
|
7
|
+
import type { SearchProps } from './types.js'
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
value = $bindable(''),
|
|
11
|
+
placeholder = '',
|
|
12
|
+
expanded = $bindable(false),
|
|
13
|
+
variant = 'contained',
|
|
14
|
+
view = 'docked',
|
|
15
|
+
leading,
|
|
16
|
+
trailing,
|
|
17
|
+
resultsAttributes,
|
|
18
|
+
label = 'Search',
|
|
19
|
+
clearLabel = 'Clear search',
|
|
20
|
+
backLabel = 'Close search',
|
|
21
|
+
onsearch,
|
|
22
|
+
inputAttributes,
|
|
23
|
+
children,
|
|
24
|
+
element = $bindable(),
|
|
25
|
+
inputElement = $bindable(),
|
|
26
|
+
...attributes
|
|
27
|
+
}: SearchProps = $props()
|
|
28
|
+
|
|
29
|
+
const uid = $props.id()
|
|
30
|
+
const resultsId = `np-search-results-${uid}`
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* A combobox has to own a popup ARIA recognises, so the field claims to be one only when the
|
|
34
|
+
* results say what they are. Left alone they are ordinary content that appears below the
|
|
35
|
+
* field, which is what a list, a set of categories or a row of chips actually is.
|
|
36
|
+
*/
|
|
37
|
+
const comboboxPopups = ['listbox', 'tree', 'grid', 'dialog']
|
|
38
|
+
const isCombobox = $derived(comboboxPopups.includes(String(resultsAttributes?.role)))
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* A focused text input always matches `:focus-visible`, pointer or not, so the ring is tracked
|
|
42
|
+
* by hand: it shows only when focus did not arrive under a finger or a mouse.
|
|
43
|
+
*/
|
|
44
|
+
let pointerFocus = false
|
|
45
|
+
let focusRing = $state(false)
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div
|
|
49
|
+
{...attributes}
|
|
50
|
+
bind:this={element}
|
|
51
|
+
class={[
|
|
52
|
+
'np-search',
|
|
53
|
+
`np-search-${variant}`,
|
|
54
|
+
`np-search-${view}`,
|
|
55
|
+
expanded && 'np-search-expanded',
|
|
56
|
+
attributes.class,
|
|
57
|
+
]}
|
|
58
|
+
onkeydown={(event) => {
|
|
59
|
+
// A key press means whatever takes focus next is being reached with the keyboard.
|
|
60
|
+
pointerFocus = false
|
|
61
|
+
attributes.onkeydown?.(event)
|
|
62
|
+
}}
|
|
63
|
+
onfocusout={(event) => {
|
|
64
|
+
// The open view floats over the page, so it closes the moment focus leaves it.
|
|
65
|
+
const next = event.relatedTarget
|
|
66
|
+
if (expanded && !(next instanceof Node && element?.contains(next))) expanded = false
|
|
67
|
+
attributes.onfocusout?.(event)
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<div class="np-search-container">
|
|
71
|
+
<!-- The click only forwards focus to the input below, which is the actual control. -->
|
|
72
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
73
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
74
|
+
<div
|
|
75
|
+
class={['np-search-bar', focusRing && 'np-search-focus-ring']}
|
|
76
|
+
onpointerdown={() => (pointerFocus = true)}
|
|
77
|
+
onclick={() => inputElement?.focus()}
|
|
78
|
+
>
|
|
79
|
+
<div class="np-search-state-layer"></div>
|
|
80
|
+
<span class="np-search-leading">
|
|
81
|
+
{#if leading}
|
|
82
|
+
{@render leading()}
|
|
83
|
+
{:else if expanded}
|
|
84
|
+
<IconButton
|
|
85
|
+
title={backLabel}
|
|
86
|
+
onclick={(event) => {
|
|
87
|
+
// Bubbling to the bar's own onclick would refocus the input and reopen the view.
|
|
88
|
+
event.stopPropagation()
|
|
89
|
+
expanded = false
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<ArrowBackIcon />
|
|
93
|
+
</IconButton>
|
|
94
|
+
{:else}
|
|
95
|
+
<span class="np-search-leading-icon"><SearchIcon /></span>
|
|
96
|
+
{/if}
|
|
97
|
+
</span>
|
|
98
|
+
|
|
99
|
+
<input
|
|
100
|
+
{...inputAttributes}
|
|
101
|
+
bind:this={inputElement}
|
|
102
|
+
bind:value
|
|
103
|
+
type="search"
|
|
104
|
+
class="np-search-input"
|
|
105
|
+
{placeholder}
|
|
106
|
+
aria-label={label}
|
|
107
|
+
role={isCombobox ? 'combobox' : undefined}
|
|
108
|
+
aria-expanded={isCombobox ? expanded : undefined}
|
|
109
|
+
aria-autocomplete={isCombobox ? 'list' : undefined}
|
|
110
|
+
aria-controls={resultsId}
|
|
111
|
+
autocomplete="off"
|
|
112
|
+
onpointerdown={(event) => {
|
|
113
|
+
pointerFocus = true
|
|
114
|
+
inputAttributes?.onpointerdown?.(event)
|
|
115
|
+
}}
|
|
116
|
+
onfocus={(event) => {
|
|
117
|
+
expanded = true
|
|
118
|
+
focusRing = !pointerFocus
|
|
119
|
+
pointerFocus = false
|
|
120
|
+
inputAttributes?.onfocus?.(event)
|
|
121
|
+
}}
|
|
122
|
+
onblur={(event) => {
|
|
123
|
+
focusRing = false
|
|
124
|
+
inputAttributes?.onblur?.(event)
|
|
125
|
+
}}
|
|
126
|
+
onkeydown={(event) => {
|
|
127
|
+
if (event.key === 'Enter') onsearch?.(value)
|
|
128
|
+
if (event.key === 'Escape' && expanded) {
|
|
129
|
+
expanded = false
|
|
130
|
+
inputElement?.blur()
|
|
131
|
+
}
|
|
132
|
+
inputAttributes?.onkeydown?.(event)
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
<span class="np-search-trailing">
|
|
137
|
+
{#if value}
|
|
138
|
+
<IconButton
|
|
139
|
+
title={clearLabel}
|
|
140
|
+
onclick={() => {
|
|
141
|
+
value = ''
|
|
142
|
+
// The focus below is a pointer's doing, so it must not raise the ring.
|
|
143
|
+
pointerFocus = true
|
|
144
|
+
inputElement?.focus()
|
|
145
|
+
}}
|
|
146
|
+
>
|
|
147
|
+
<CloseIcon />
|
|
148
|
+
</IconButton>
|
|
149
|
+
{/if}
|
|
150
|
+
{#if trailing}
|
|
151
|
+
{@render trailing()}
|
|
152
|
+
{/if}
|
|
153
|
+
</span>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<!--
|
|
157
|
+
The container brings no semantics of its own: what goes in it is a list one time and
|
|
158
|
+
categories, avatars and chips the next. `resultsAttributes` is where a role belongs when
|
|
159
|
+
the content has earned one.
|
|
160
|
+
-->
|
|
161
|
+
<div
|
|
162
|
+
{...resultsAttributes}
|
|
163
|
+
id={resultsId}
|
|
164
|
+
class={['np-search-results', resultsAttributes?.class]}
|
|
165
|
+
>
|
|
166
|
+
{@render children?.()}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<style>
|
|
172
|
+
.np-search {
|
|
173
|
+
/* SearchBarTokens: 56dp tall, fully rounded, surface-container-high. */
|
|
174
|
+
--_bar-height: 3.5rem;
|
|
175
|
+
/*
|
|
176
|
+
* M3 Expressive grows the bar by shrinking the margins it keeps to its pane, from 24dp at
|
|
177
|
+
* rest to 12dp once the view opens. Owning the margin is what makes that growth work
|
|
178
|
+
* without the consumer having to name two widths.
|
|
179
|
+
*/
|
|
180
|
+
--_pane-margin: var(--np-search-pane-margin, 1.5rem);
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
width: 100%;
|
|
185
|
+
min-width: 0;
|
|
186
|
+
padding-inline: var(--_pane-margin);
|
|
187
|
+
}
|
|
188
|
+
.np-search-contained.np-search-expanded {
|
|
189
|
+
--_pane-margin: var(--np-search-view-margin, 0.75rem);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
193
|
+
.np-search {
|
|
194
|
+
transition: padding var(--np-motion-expressive-default-spatial);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/*
|
|
199
|
+
* Docked, the bar is all the component takes up in the layout: the view it opens floats over
|
|
200
|
+
* the page rather than pushing it down, so the margins are the container's own insets.
|
|
201
|
+
*/
|
|
202
|
+
.np-search-docked {
|
|
203
|
+
position: relative;
|
|
204
|
+
block-size: var(--_bar-height);
|
|
205
|
+
padding-inline: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* The container is only a surface in the divided style, where the bar and the results share
|
|
210
|
+
* one. Contained keeps them apart, 2dp of gap between the two.
|
|
211
|
+
*/
|
|
212
|
+
.np-search-container {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: column;
|
|
215
|
+
box-sizing: border-box;
|
|
216
|
+
gap: 0.125rem;
|
|
217
|
+
min-height: 0;
|
|
218
|
+
/* Container width is 360dp at the least and 720dp at the most. */
|
|
219
|
+
width: 100%;
|
|
220
|
+
min-width: min(22.5rem, 100%);
|
|
221
|
+
max-width: var(--np-search-width, 45rem);
|
|
222
|
+
margin-inline: auto;
|
|
223
|
+
}
|
|
224
|
+
.np-search-docked .np-search-container {
|
|
225
|
+
position: absolute;
|
|
226
|
+
inset-block-start: 0;
|
|
227
|
+
inset-inline: var(--_pane-margin);
|
|
228
|
+
/*
|
|
229
|
+
* A definite `width` rather than `auto`, which resolves outside the transition engine and
|
|
230
|
+
* would snap the pane to its grown width the instant the class changes instead of easing
|
|
231
|
+
* it in with the insets.
|
|
232
|
+
*/
|
|
233
|
+
width: calc(100% - 2 * var(--_pane-margin));
|
|
234
|
+
/*
|
|
235
|
+
* An absolutely positioned box measures a percentage against the whole component, margins
|
|
236
|
+
* included, so the floor has to leave room for them or a narrow host overflows. Also
|
|
237
|
+
* transitioned below, in step with `width`, or it would snap to its own end value early
|
|
238
|
+
* and clamp the still-transitioning `width` to it for the whole animation.
|
|
239
|
+
*/
|
|
240
|
+
min-width: min(22.5rem, calc(100% - 2 * var(--_pane-margin)));
|
|
241
|
+
z-index: var(--np-search-z-index, 3);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
245
|
+
.np-search-docked .np-search-container {
|
|
246
|
+
transition:
|
|
247
|
+
inset-inline-start var(--np-motion-expressive-default-spatial),
|
|
248
|
+
inset-inline-end var(--np-motion-expressive-default-spatial),
|
|
249
|
+
width var(--np-motion-expressive-default-spatial),
|
|
250
|
+
min-width var(--np-motion-expressive-default-spatial);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/*
|
|
254
|
+
* The view unrolls from under the bar rather than arriving whole. Clipping is what does it,
|
|
255
|
+
* so nothing about the bar moves and the pane's shape, rule and shadow are all uncovered
|
|
256
|
+
* together, however tall the list turns out to be.
|
|
257
|
+
*/
|
|
258
|
+
.np-search-docked.np-search-expanded .np-search-container {
|
|
259
|
+
animation: unroll var(--np-motion-expressive-default-spatial);
|
|
260
|
+
}
|
|
261
|
+
.np-search-docked.np-search-expanded .np-search-results {
|
|
262
|
+
animation: fadeIn var(--np-motion-expressive-default-effects);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/* The clip is held off the edges so that it never cuts the shadow while it plays. */
|
|
266
|
+
@keyframes unroll {
|
|
267
|
+
from {
|
|
268
|
+
clip-path: inset(-1.5rem -1.5rem calc(100% - var(--_bar-height)) -1.5rem);
|
|
269
|
+
}
|
|
270
|
+
to {
|
|
271
|
+
clip-path: inset(-1.5rem);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
@keyframes fadeIn {
|
|
275
|
+
from {
|
|
276
|
+
opacity: 0;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.np-search-bar {
|
|
281
|
+
position: relative;
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
box-sizing: border-box;
|
|
285
|
+
flex: none;
|
|
286
|
+
/*
|
|
287
|
+
* 8dp of padding and 8dp of gap put the glyphs 16dp inside the bar and the input text 16dp
|
|
288
|
+
* past them, which is what the spec measures. The spec reaches those numbers with 48dp tap
|
|
289
|
+
* targets and 4dp of space; the library's icon buttons are 40dp, so the space absorbs the
|
|
290
|
+
* difference.
|
|
291
|
+
*/
|
|
292
|
+
gap: 0.5rem;
|
|
293
|
+
padding-inline: 0.5rem;
|
|
294
|
+
height: var(--_bar-height);
|
|
295
|
+
border-radius: var(--np-search-shape, var(--np-shape-corner-full));
|
|
296
|
+
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
297
|
+
color: var(--np-color-on-surface);
|
|
298
|
+
}
|
|
299
|
+
.np-search:not(.np-search-expanded) .np-search-bar {
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
304
|
+
.np-search-bar {
|
|
305
|
+
transition:
|
|
306
|
+
height var(--np-motion-expressive-default-spatial),
|
|
307
|
+
background-color var(--np-motion-expressive-fast-effects);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* The bar is a tap target at rest. Once the view is open it is a header, and takes no states. */
|
|
312
|
+
.np-search-state-layer {
|
|
313
|
+
position: absolute;
|
|
314
|
+
inset: 0;
|
|
315
|
+
border-radius: inherit;
|
|
316
|
+
pointer-events: none;
|
|
317
|
+
background-color: var(--np-color-on-surface);
|
|
318
|
+
opacity: 0;
|
|
319
|
+
}
|
|
320
|
+
@media (hover: hover) {
|
|
321
|
+
.np-search:not(.np-search-expanded) .np-search-bar:hover .np-search-state-layer {
|
|
322
|
+
opacity: 0.08;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
.np-search:not(.np-search-expanded) .np-search-bar:active .np-search-state-layer {
|
|
326
|
+
opacity: 0.1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Positioned, so the bar's content paints over the state layer rather than under it. */
|
|
330
|
+
.np-search-leading,
|
|
331
|
+
.np-search-trailing,
|
|
332
|
+
.np-search-input {
|
|
333
|
+
position: relative;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.np-search-leading,
|
|
337
|
+
.np-search-trailing {
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
flex: none;
|
|
341
|
+
}
|
|
342
|
+
/* Trailing actions sit flush against one another. */
|
|
343
|
+
.np-search-trailing {
|
|
344
|
+
gap: 0;
|
|
345
|
+
}
|
|
346
|
+
/* LeadingIconColor is on-surface; trailing is on-surface-variant. */
|
|
347
|
+
.np-search-leading {
|
|
348
|
+
--np-icon-button-icon-color: var(--np-color-on-surface);
|
|
349
|
+
}
|
|
350
|
+
.np-search-trailing {
|
|
351
|
+
--np-icon-button-icon-color: var(--np-color-on-surface-variant);
|
|
352
|
+
}
|
|
353
|
+
/* Sized like the icon button that replaces it, so the glyph holds still as the view opens. */
|
|
354
|
+
.np-search-leading-icon {
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: center;
|
|
358
|
+
block-size: 2.5rem;
|
|
359
|
+
inline-size: 2.5rem;
|
|
360
|
+
fill: var(--np-color-on-surface);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.np-search-input {
|
|
364
|
+
flex: 1;
|
|
365
|
+
min-width: 0;
|
|
366
|
+
appearance: none;
|
|
367
|
+
border: none;
|
|
368
|
+
outline: none;
|
|
369
|
+
background: none;
|
|
370
|
+
font: inherit;
|
|
371
|
+
/* InputTextFont is body-large. */
|
|
372
|
+
font-size: 1rem;
|
|
373
|
+
line-height: 1.5rem;
|
|
374
|
+
letter-spacing: 0.03125rem;
|
|
375
|
+
color: var(--np-color-on-surface);
|
|
376
|
+
padding: 0;
|
|
377
|
+
cursor: inherit;
|
|
378
|
+
}
|
|
379
|
+
.np-search-input::placeholder {
|
|
380
|
+
color: var(--np-color-on-surface-variant);
|
|
381
|
+
opacity: 1;
|
|
382
|
+
}
|
|
383
|
+
/* The type="search" clear affordance would duplicate the component's own. */
|
|
384
|
+
.np-search-input::-webkit-search-cancel-button {
|
|
385
|
+
appearance: none;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.np-search-focus-ring {
|
|
389
|
+
outline-style: solid;
|
|
390
|
+
outline-color: var(--np-search-focus-indicator-color, var(--np-color-secondary));
|
|
391
|
+
outline-width: 3px;
|
|
392
|
+
outline-offset: 2px;
|
|
393
|
+
}
|
|
394
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
395
|
+
.np-search-focus-ring {
|
|
396
|
+
animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Results are only in the layout, and only in the accessibility tree, once expanded. */
|
|
401
|
+
.np-search-results {
|
|
402
|
+
display: none;
|
|
403
|
+
box-sizing: border-box;
|
|
404
|
+
overflow-y: auto;
|
|
405
|
+
overscroll-behavior: contain;
|
|
406
|
+
}
|
|
407
|
+
.np-search-expanded .np-search-results {
|
|
408
|
+
display: block;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Docked: the results sit under the field, growing to two thirds of the screen at the most. */
|
|
412
|
+
.np-search-docked .np-search-results {
|
|
413
|
+
max-height: var(--np-search-results-max-height, 66.6667dvh);
|
|
414
|
+
}
|
|
415
|
+
/* Contained docks them as their own 12dp pane. */
|
|
416
|
+
.np-search-contained.np-search-docked .np-search-results {
|
|
417
|
+
border-radius: var(--np-search-results-shape, var(--np-shape-corner-medium));
|
|
418
|
+
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/*
|
|
422
|
+
* Divided (baseline) folds the field and the results into one 28dp container, separated by a
|
|
423
|
+
* rule. Contained keeps the pill, which is what M3 recommends.
|
|
424
|
+
*/
|
|
425
|
+
.np-search-divided.np-search-expanded .np-search-bar {
|
|
426
|
+
border-radius: 0;
|
|
427
|
+
background-color: transparent;
|
|
428
|
+
border-block-end: 1px solid var(--np-search-divider-color, var(--np-color-outline));
|
|
429
|
+
}
|
|
430
|
+
.np-search-divided.np-search-docked.np-search-expanded .np-search-container {
|
|
431
|
+
gap: 0;
|
|
432
|
+
border-radius: var(--np-search-results-shape, var(--np-shape-corner-extra-large));
|
|
433
|
+
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
434
|
+
}
|
|
435
|
+
/*
|
|
436
|
+
* The results round off the container's bottom corners themselves, rather than the container
|
|
437
|
+
* clipping them, which would take the header's focus ring with it.
|
|
438
|
+
*/
|
|
439
|
+
.np-search-divided.np-search-docked.np-search-expanded .np-search-results {
|
|
440
|
+
border-end-start-radius: inherit;
|
|
441
|
+
border-end-end-radius: inherit;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Full screen: the field and its results take over the viewport, and the view has no shape. */
|
|
445
|
+
.np-search-full-screen.np-search-expanded {
|
|
446
|
+
position: fixed;
|
|
447
|
+
inset: 0;
|
|
448
|
+
z-index: var(--np-search-z-index, 24);
|
|
449
|
+
/* The pill needs the same room above it that it keeps at its sides. */
|
|
450
|
+
padding-block-start: var(--_pane-margin);
|
|
451
|
+
background-color: var(--np-search-view-background-color, var(--np-color-surface-container-low));
|
|
452
|
+
}
|
|
453
|
+
.np-search-full-screen.np-search-expanded .np-search-container {
|
|
454
|
+
flex: 1;
|
|
455
|
+
max-width: none;
|
|
456
|
+
}
|
|
457
|
+
.np-search-full-screen.np-search-expanded .np-search-results {
|
|
458
|
+
flex: 1;
|
|
459
|
+
max-height: none;
|
|
460
|
+
}
|
|
461
|
+
/*
|
|
462
|
+
* Contained holds the 56dp bar in full screen too. Only the divided header grows to 72dp, and
|
|
463
|
+
* it runs edge to edge on the view's own surface.
|
|
464
|
+
*/
|
|
465
|
+
.np-search-divided.np-search-full-screen.np-search-expanded {
|
|
466
|
+
padding: 0;
|
|
467
|
+
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
468
|
+
}
|
|
469
|
+
.np-search-divided.np-search-full-screen.np-search-expanded .np-search-bar {
|
|
470
|
+
height: 4.5rem;
|
|
471
|
+
}
|
|
472
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import '../internal/focus-ring.css';
|
|
2
|
+
import type { SearchProps } from './types.ts';
|
|
3
|
+
declare const Search: import("svelte").Component<SearchProps, {}, "element" | "value" | "inputElement" | "expanded">;
|
|
4
|
+
type Search = ReturnType<typeof Search>;
|
|
5
|
+
export default Search;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Search } from './Search.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Search } from './Search.svelte';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
export interface SearchProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onsearch'> {
|
|
4
|
+
/** Bindable. The query. */
|
|
5
|
+
value?: string;
|
|
6
|
+
/** Hinted search text, shown while the field is empty. */
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Bindable. Whether the results are showing. */
|
|
9
|
+
expanded?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* `'contained'` keeps the field's pill shape and colour while expanded, which M3 recommends.
|
|
12
|
+
* `'divided'` squares the field off against the results and separates them with a divider.
|
|
13
|
+
*/
|
|
14
|
+
variant?: 'contained' | 'divided';
|
|
15
|
+
/** `'docked'` drops the results under the field. `'full-screen'` covers the viewport. */
|
|
16
|
+
view?: 'docked' | 'full-screen';
|
|
17
|
+
/** Leading slot. Defaults to a search icon, or a back arrow once expanded. */
|
|
18
|
+
leading?: Snippet;
|
|
19
|
+
/** Trailing slot, for an avatar or actions. A clear button is shown alongside it. */
|
|
20
|
+
trailing?: Snippet;
|
|
21
|
+
/** Accessible name for the field. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Accessible name for the clear button. */
|
|
24
|
+
clearLabel?: string;
|
|
25
|
+
/** Accessible name for the back action shown while expanded. */
|
|
26
|
+
backLabel?: string;
|
|
27
|
+
/** Fired on the Enter key, with the current query. */
|
|
28
|
+
onsearch?: (value: string) => void;
|
|
29
|
+
/** Extra attributes for the underlying `input`. */
|
|
30
|
+
inputAttributes?: HTMLInputAttributes;
|
|
31
|
+
/**
|
|
32
|
+
* Extra attributes for the container holding the suggestions or results. It carries no role of
|
|
33
|
+
* its own, so this is where the content's semantics go: `role="listbox"` when the children
|
|
34
|
+
* really are options, nothing when they are a list, categories or chips.
|
|
35
|
+
*/
|
|
36
|
+
resultsAttributes?: HTMLAttributes<HTMLDivElement>;
|
|
37
|
+
element?: HTMLDivElement;
|
|
38
|
+
inputElement?: HTMLInputElement;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -74,7 +74,6 @@
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
let field = $state<HTMLDivElement>()
|
|
77
|
-
let clientWidth = $state(0)
|
|
78
77
|
let menuOpen = $state(false)
|
|
79
78
|
let focusIndex = $state(-1)
|
|
80
79
|
let pendingFocus = false
|
|
@@ -230,6 +229,30 @@
|
|
|
230
229
|
}
|
|
231
230
|
}
|
|
232
231
|
|
|
232
|
+
const optionKeydown = (event: KeyboardEvent, option: SelectOption) => {
|
|
233
|
+
const key = event.key
|
|
234
|
+
if (key === 'ArrowDown') {
|
|
235
|
+
event.preventDefault()
|
|
236
|
+
moveFocus(1)
|
|
237
|
+
} else if (key === 'ArrowUp') {
|
|
238
|
+
event.preventDefault()
|
|
239
|
+
moveFocus(-1)
|
|
240
|
+
} else if (key === 'Home') {
|
|
241
|
+
event.preventDefault()
|
|
242
|
+
focusEdge(true)
|
|
243
|
+
} else if (key === 'End') {
|
|
244
|
+
event.preventDefault()
|
|
245
|
+
focusEdge(false)
|
|
246
|
+
} else if (key === 'Enter' || key === ' ') {
|
|
247
|
+
event.preventDefault()
|
|
248
|
+
handleOptionSelect(event, option)
|
|
249
|
+
} else if (key === 'Tab') {
|
|
250
|
+
menuElement?.hidePopover()
|
|
251
|
+
} else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
252
|
+
performTypeahead(key)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
233
256
|
const performTypeahead = (char: string) => {
|
|
234
257
|
const now = performance.now()
|
|
235
258
|
if (now - lastTypeTime > 700) typeBuffer = ''
|
|
@@ -268,7 +291,7 @@
|
|
|
268
291
|
: !label?.length
|
|
269
292
|
? '--top-space:1rem;--bottom-space:1rem;'
|
|
270
293
|
: '') + style}
|
|
271
|
-
class={['text-field', attributes.class]}
|
|
294
|
+
class={['np-text-field', attributes.class]}
|
|
272
295
|
bind:this={element}
|
|
273
296
|
>
|
|
274
297
|
<!-- svelte-ignore a11y_autofocus -->
|
|
@@ -291,7 +314,6 @@
|
|
|
291
314
|
aria-activedescendant={activeDescendantId}
|
|
292
315
|
data-testid={attributes['data-testid']}
|
|
293
316
|
bind:this={field}
|
|
294
|
-
bind:clientWidth
|
|
295
317
|
autofocus={disabled ? false : autofocus}
|
|
296
318
|
onclick={(event) => {
|
|
297
319
|
const target = event.target as HTMLElement
|
|
@@ -453,93 +475,32 @@
|
|
|
453
475
|
</div>
|
|
454
476
|
|
|
455
477
|
{#snippet item(option: SelectOption, index: number)}
|
|
456
|
-
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
focusEdge(true)
|
|
478
|
-
} else if (key === 'End') {
|
|
479
|
-
event.preventDefault()
|
|
480
|
-
focusEdge(false)
|
|
481
|
-
} else if (key === 'Enter' || key === ' ') {
|
|
482
|
-
event.preventDefault()
|
|
483
|
-
handleOptionSelect(event, option)
|
|
484
|
-
} else if (key === 'Tab') {
|
|
485
|
-
menuElement?.hidePopover()
|
|
486
|
-
} else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
487
|
-
performTypeahead(key)
|
|
488
|
-
}
|
|
489
|
-
}}
|
|
490
|
-
variant="button"
|
|
491
|
-
selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
492
|
-
aria-selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
493
|
-
>{option.label}
|
|
494
|
-
{#snippet start()}
|
|
495
|
-
<Check disabled={option.disabled} checked={value.includes(option.value)} />
|
|
496
|
-
{/snippet}
|
|
497
|
-
</Item>
|
|
498
|
-
{:else}
|
|
499
|
-
<Item
|
|
500
|
-
id="{uid}-opt-{index}"
|
|
501
|
-
onclick={(event) => {
|
|
502
|
-
handleOptionSelect(event, option)
|
|
503
|
-
field?.focus()
|
|
504
|
-
}}
|
|
505
|
-
tabindex={-1}
|
|
506
|
-
disabled={option.disabled}
|
|
507
|
-
aria-disabled={option.disabled}
|
|
508
|
-
role="option"
|
|
509
|
-
onkeydown={(event) => {
|
|
510
|
-
const key = event.key
|
|
511
|
-
if (key === 'ArrowDown') {
|
|
512
|
-
event.preventDefault()
|
|
513
|
-
moveFocus(1)
|
|
514
|
-
} else if (key === 'ArrowUp') {
|
|
515
|
-
event.preventDefault()
|
|
516
|
-
moveFocus(-1)
|
|
517
|
-
} else if (key === 'Home') {
|
|
518
|
-
event.preventDefault()
|
|
519
|
-
focusEdge(true)
|
|
520
|
-
} else if (key === 'End') {
|
|
521
|
-
event.preventDefault()
|
|
522
|
-
focusEdge(false)
|
|
523
|
-
} else if (key === 'Enter' || key === ' ') {
|
|
524
|
-
event.preventDefault()
|
|
525
|
-
handleOptionSelect(event, option)
|
|
526
|
-
} else if (key === 'Tab') {
|
|
527
|
-
menuElement?.hidePopover()
|
|
528
|
-
} else if (key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
529
|
-
performTypeahead(key)
|
|
530
|
-
}
|
|
531
|
-
}}
|
|
532
|
-
variant="button"
|
|
533
|
-
selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
534
|
-
aria-selected={Array.isArray(value) ? value.includes(option.value) : value === option.value}
|
|
535
|
-
>{option.label}
|
|
536
|
-
</Item>
|
|
537
|
-
{/if}
|
|
478
|
+
{@const isSelected = selectedSet.has(option.value)}
|
|
479
|
+
{#snippet check()}
|
|
480
|
+
<Check disabled={option.disabled} checked={isSelected} />
|
|
481
|
+
{/snippet}
|
|
482
|
+
<Item
|
|
483
|
+
id="{uid}-opt-{index}"
|
|
484
|
+
onclick={(event) => {
|
|
485
|
+
handleOptionSelect(event, option)
|
|
486
|
+
field?.focus()
|
|
487
|
+
}}
|
|
488
|
+
tabindex={-1}
|
|
489
|
+
disabled={option.disabled}
|
|
490
|
+
aria-disabled={option.disabled}
|
|
491
|
+
role="option"
|
|
492
|
+
onkeydown={(event) => optionKeydown(event, option)}
|
|
493
|
+
variant="button"
|
|
494
|
+
selected={isSelected}
|
|
495
|
+
aria-selected={isSelected}
|
|
496
|
+
start={multiple ? check : undefined}
|
|
497
|
+
>{option.label}
|
|
498
|
+
</Item>
|
|
538
499
|
{/snippet}
|
|
539
500
|
|
|
540
501
|
<Menu
|
|
541
502
|
id="listbox-{uid}"
|
|
542
|
-
style={`position-anchor:--${uid};${widthProp}
|
|
503
|
+
style={`position-anchor:--${uid};${widthProp}:anchor-size(width)`}
|
|
543
504
|
role="listbox"
|
|
544
505
|
aria-multiselectable={multiple}
|
|
545
506
|
--np-menu-justify-self="none"
|
|
@@ -689,7 +650,7 @@
|
|
|
689
650
|
height: 100%;
|
|
690
651
|
position: relative;
|
|
691
652
|
}
|
|
692
|
-
.text-field {
|
|
653
|
+
.np-text-field {
|
|
693
654
|
display: inline-flex;
|
|
694
655
|
resize: both;
|
|
695
656
|
text-align: start;
|