noph-ui 0.45.0 → 0.46.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/dist/animation.d.ts +0 -15
- package/dist/animation.js +0 -15
- package/dist/app-bar/AppBar.svelte +2 -52
- package/dist/app-bar/types.d.ts +0 -22
- package/dist/button/SplitButton.svelte +0 -17
- package/dist/button/types.d.ts +0 -6
- package/dist/checkbox/Checkbox.svelte +0 -2
- package/dist/chip/FilterChip.svelte +0 -2
- package/dist/chip/types.d.ts +0 -8
- package/dist/date-picker/DatePickerDialog.svelte +0 -10
- package/dist/date-picker/DateRangePicker.svelte +0 -5
- package/dist/date-picker/DockedDatePicker.svelte +1 -11
- package/dist/date-picker/DockedDateTimePicker.svelte +1 -8
- package/dist/date-picker/exitVisibility.svelte.d.ts +0 -5
- package/dist/date-picker/exitVisibility.svelte.js +0 -7
- package/dist/date-picker/optionList.d.ts +0 -12
- package/dist/date-picker/optionList.js +0 -12
- package/dist/dialog/Dialog.svelte +0 -22
- package/dist/dialog/types.d.ts +0 -1
- package/dist/fab/ExtendedFab.svelte +0 -26
- package/dist/fab/Fab.svelte +0 -28
- package/dist/fab/FabMenu.svelte +0 -48
- package/dist/fab/types.d.ts +0 -24
- package/dist/loading-indicator/shapes.js +0 -7
- package/dist/navigation-bar/NavigationBar.svelte +0 -7
- package/dist/navigation-bar/NavigationBarItem.svelte +0 -22
- package/dist/navigation-bar/types.d.ts +0 -10
- package/dist/navigation-drawer/NavigationDrawer.svelte +0 -21
- package/dist/navigation-drawer/types.d.ts +0 -3
- package/dist/popover.svelte.d.ts +0 -9
- package/dist/popover.svelte.js +0 -9
- package/dist/press.svelte.d.ts +0 -13
- package/dist/press.svelte.js +0 -13
- package/dist/search/Search.svelte +23 -81
- package/dist/search/Search.svelte.d.ts +4 -1
- package/dist/search/types.d.ts +0 -20
- package/dist/sheet/Sheet.svelte +0 -25
- package/dist/sheet/types.d.ts +0 -8
- package/dist/slider/Slider.svelte +0 -36
- package/dist/snackbar/Snackbar.svelte +3 -17
- package/dist/tabs/Tab.svelte +0 -4
- package/dist/tabs/Tabs.svelte +2 -2
- package/dist/text-field/TextField.svelte +0 -2
- package/dist/toolbar/Toolbar.svelte +0 -12
- package/dist/toolbar/types.d.ts +0 -7
- package/dist/tooltip/RichTooltip.svelte +0 -7
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte'
|
|
2
3
|
import '../internal/focus-ring.css'
|
|
3
4
|
import IconButton from '../button/IconButton.svelte'
|
|
4
5
|
import ArrowBackIcon from '../icons/ArrowBackIcon.svelte'
|
|
@@ -29,20 +30,23 @@
|
|
|
29
30
|
const uid = $props.id()
|
|
30
31
|
const resultsId = `np-search-results-${uid}`
|
|
31
32
|
|
|
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
33
|
const comboboxPopups = ['listbox', 'tree', 'grid', 'dialog']
|
|
38
34
|
const isCombobox = $derived(comboboxPopups.includes(String(resultsAttributes?.role)))
|
|
39
35
|
|
|
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
36
|
let pointerFocus = false
|
|
45
37
|
let focusRing = $state(false)
|
|
38
|
+
|
|
39
|
+
export const show = async () => {
|
|
40
|
+
expanded = true
|
|
41
|
+
pointerFocus = true
|
|
42
|
+
await tick()
|
|
43
|
+
inputElement?.focus()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const close = () => {
|
|
47
|
+
expanded = false
|
|
48
|
+
inputElement?.blur()
|
|
49
|
+
}
|
|
46
50
|
</script>
|
|
47
51
|
|
|
48
52
|
<div
|
|
@@ -56,19 +60,16 @@
|
|
|
56
60
|
attributes.class,
|
|
57
61
|
]}
|
|
58
62
|
onkeydown={(event) => {
|
|
59
|
-
// A key press means whatever takes focus next is being reached with the keyboard.
|
|
60
63
|
pointerFocus = false
|
|
61
64
|
attributes.onkeydown?.(event)
|
|
62
65
|
}}
|
|
63
66
|
onfocusout={(event) => {
|
|
64
|
-
// The open view floats over the page, so it closes the moment focus leaves it.
|
|
65
67
|
const next = event.relatedTarget
|
|
66
68
|
if (expanded && !(next instanceof Node && element?.contains(next))) expanded = false
|
|
67
69
|
attributes.onfocusout?.(event)
|
|
68
70
|
}}
|
|
69
71
|
>
|
|
70
72
|
<div class="np-search-container">
|
|
71
|
-
<!-- The click only forwards focus to the input below, which is the actual control. -->
|
|
72
73
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
73
74
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
74
75
|
<div
|
|
@@ -84,7 +85,6 @@
|
|
|
84
85
|
<IconButton
|
|
85
86
|
title={backLabel}
|
|
86
87
|
onclick={(event) => {
|
|
87
|
-
// Bubbling to the bar's own onclick would refocus the input and reopen the view.
|
|
88
88
|
event.stopPropagation()
|
|
89
89
|
expanded = false
|
|
90
90
|
}}
|
|
@@ -139,7 +139,6 @@
|
|
|
139
139
|
title={clearLabel}
|
|
140
140
|
onclick={() => {
|
|
141
141
|
value = ''
|
|
142
|
-
// The focus below is a pointer's doing, so it must not raise the ring.
|
|
143
142
|
pointerFocus = true
|
|
144
143
|
inputElement?.focus()
|
|
145
144
|
}}
|
|
@@ -153,11 +152,6 @@
|
|
|
153
152
|
</span>
|
|
154
153
|
</div>
|
|
155
154
|
|
|
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
155
|
<div
|
|
162
156
|
{...resultsAttributes}
|
|
163
157
|
id={resultsId}
|
|
@@ -170,13 +164,7 @@
|
|
|
170
164
|
|
|
171
165
|
<style>
|
|
172
166
|
.np-search {
|
|
173
|
-
/* SearchBarTokens: 56dp tall, fully rounded, surface-container-high. */
|
|
174
167
|
--_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
168
|
--_pane-margin: var(--np-search-pane-margin, 1.5rem);
|
|
181
169
|
display: flex;
|
|
182
170
|
flex-direction: column;
|
|
@@ -195,27 +183,18 @@
|
|
|
195
183
|
}
|
|
196
184
|
}
|
|
197
185
|
|
|
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
186
|
.np-search-docked {
|
|
203
187
|
position: relative;
|
|
204
188
|
block-size: var(--_bar-height);
|
|
205
189
|
padding-inline: 0;
|
|
206
190
|
}
|
|
207
191
|
|
|
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
192
|
.np-search-container {
|
|
213
193
|
display: flex;
|
|
214
194
|
flex-direction: column;
|
|
215
195
|
box-sizing: border-box;
|
|
216
196
|
gap: 0.125rem;
|
|
217
197
|
min-height: 0;
|
|
218
|
-
/* Container width is 360dp at the least and 720dp at the most. */
|
|
219
198
|
width: 100%;
|
|
220
199
|
min-width: min(22.5rem, 100%);
|
|
221
200
|
max-width: var(--np-search-width, 45rem);
|
|
@@ -225,18 +204,7 @@
|
|
|
225
204
|
position: absolute;
|
|
226
205
|
inset-block-start: 0;
|
|
227
206
|
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
207
|
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
208
|
min-width: min(22.5rem, calc(100% - 2 * var(--_pane-margin)));
|
|
241
209
|
z-index: var(--np-search-z-index, 3);
|
|
242
210
|
}
|
|
@@ -250,11 +218,6 @@
|
|
|
250
218
|
min-width var(--np-motion-expressive-default-spatial);
|
|
251
219
|
}
|
|
252
220
|
|
|
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
221
|
.np-search-docked.np-search-expanded .np-search-container {
|
|
259
222
|
animation: unroll var(--np-motion-expressive-default-spatial);
|
|
260
223
|
}
|
|
@@ -262,7 +225,6 @@
|
|
|
262
225
|
animation: fadeIn var(--np-motion-expressive-default-effects);
|
|
263
226
|
}
|
|
264
227
|
}
|
|
265
|
-
/* The clip is held off the edges so that it never cuts the shadow while it plays. */
|
|
266
228
|
@keyframes unroll {
|
|
267
229
|
from {
|
|
268
230
|
clip-path: inset(-1.5rem -1.5rem calc(100% - var(--_bar-height)) -1.5rem);
|
|
@@ -283,12 +245,6 @@
|
|
|
283
245
|
align-items: center;
|
|
284
246
|
box-sizing: border-box;
|
|
285
247
|
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
248
|
gap: 0.5rem;
|
|
293
249
|
padding-inline: 0.5rem;
|
|
294
250
|
height: var(--_bar-height);
|
|
@@ -308,7 +264,6 @@
|
|
|
308
264
|
}
|
|
309
265
|
}
|
|
310
266
|
|
|
311
|
-
/* The bar is a tap target at rest. Once the view is open it is a header, and takes no states. */
|
|
312
267
|
.np-search-state-layer {
|
|
313
268
|
position: absolute;
|
|
314
269
|
inset: 0;
|
|
@@ -326,7 +281,6 @@
|
|
|
326
281
|
opacity: 0.1;
|
|
327
282
|
}
|
|
328
283
|
|
|
329
|
-
/* Positioned, so the bar's content paints over the state layer rather than under it. */
|
|
330
284
|
.np-search-leading,
|
|
331
285
|
.np-search-trailing,
|
|
332
286
|
.np-search-input {
|
|
@@ -339,18 +293,15 @@
|
|
|
339
293
|
align-items: center;
|
|
340
294
|
flex: none;
|
|
341
295
|
}
|
|
342
|
-
/* Trailing actions sit flush against one another. */
|
|
343
296
|
.np-search-trailing {
|
|
344
297
|
gap: 0;
|
|
345
298
|
}
|
|
346
|
-
/* LeadingIconColor is on-surface; trailing is on-surface-variant. */
|
|
347
299
|
.np-search-leading {
|
|
348
300
|
--np-icon-button-icon-color: var(--np-color-on-surface);
|
|
349
301
|
}
|
|
350
302
|
.np-search-trailing {
|
|
351
303
|
--np-icon-button-icon-color: var(--np-color-on-surface-variant);
|
|
352
304
|
}
|
|
353
|
-
/* Sized like the icon button that replaces it, so the glyph holds still as the view opens. */
|
|
354
305
|
.np-search-leading-icon {
|
|
355
306
|
display: flex;
|
|
356
307
|
align-items: center;
|
|
@@ -368,7 +319,6 @@
|
|
|
368
319
|
outline: none;
|
|
369
320
|
background: none;
|
|
370
321
|
font: inherit;
|
|
371
|
-
/* InputTextFont is body-large. */
|
|
372
322
|
font-size: 1rem;
|
|
373
323
|
line-height: 1.5rem;
|
|
374
324
|
letter-spacing: 0.03125rem;
|
|
@@ -380,7 +330,6 @@
|
|
|
380
330
|
color: var(--np-color-on-surface-variant);
|
|
381
331
|
opacity: 1;
|
|
382
332
|
}
|
|
383
|
-
/* The type="search" clear affordance would duplicate the component's own. */
|
|
384
333
|
.np-search-input::-webkit-search-cancel-button {
|
|
385
334
|
appearance: none;
|
|
386
335
|
}
|
|
@@ -397,7 +346,6 @@
|
|
|
397
346
|
}
|
|
398
347
|
}
|
|
399
348
|
|
|
400
|
-
/* Results are only in the layout, and only in the accessibility tree, once expanded. */
|
|
401
349
|
.np-search-results {
|
|
402
350
|
display: none;
|
|
403
351
|
box-sizing: border-box;
|
|
@@ -408,20 +356,14 @@
|
|
|
408
356
|
display: block;
|
|
409
357
|
}
|
|
410
358
|
|
|
411
|
-
/* Docked: the results sit under the field, growing to two thirds of the screen at the most. */
|
|
412
359
|
.np-search-docked .np-search-results {
|
|
413
360
|
max-height: var(--np-search-results-max-height, 66.6667dvh);
|
|
414
361
|
}
|
|
415
|
-
/* Contained docks them as their own 12dp pane. */
|
|
416
362
|
.np-search-contained.np-search-docked .np-search-results {
|
|
417
363
|
border-radius: var(--np-search-results-shape, var(--np-shape-corner-medium));
|
|
418
364
|
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
419
365
|
}
|
|
420
366
|
|
|
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
367
|
.np-search-divided.np-search-expanded .np-search-bar {
|
|
426
368
|
border-radius: 0;
|
|
427
369
|
background-color: transparent;
|
|
@@ -432,21 +374,15 @@
|
|
|
432
374
|
border-radius: var(--np-search-results-shape, var(--np-shape-corner-extra-large));
|
|
433
375
|
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
434
376
|
}
|
|
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
377
|
.np-search-divided.np-search-docked.np-search-expanded .np-search-results {
|
|
440
378
|
border-end-start-radius: inherit;
|
|
441
379
|
border-end-end-radius: inherit;
|
|
442
380
|
}
|
|
443
381
|
|
|
444
|
-
/* Full screen: the field and its results take over the viewport, and the view has no shape. */
|
|
445
382
|
.np-search-full-screen.np-search-expanded {
|
|
446
383
|
position: fixed;
|
|
447
384
|
inset: 0;
|
|
448
385
|
z-index: var(--np-search-z-index, 24);
|
|
449
|
-
/* The pill needs the same room above it that it keeps at its sides. */
|
|
450
386
|
padding-block-start: var(--_pane-margin);
|
|
451
387
|
background-color: var(--np-search-view-background-color, var(--np-color-surface-container-low));
|
|
452
388
|
}
|
|
@@ -458,10 +394,6 @@
|
|
|
458
394
|
flex: 1;
|
|
459
395
|
max-height: none;
|
|
460
396
|
}
|
|
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
397
|
.np-search-divided.np-search-full-screen.np-search-expanded {
|
|
466
398
|
padding: 0;
|
|
467
399
|
background-color: var(--np-search-container-color, var(--np-color-surface-container-high));
|
|
@@ -469,4 +401,14 @@
|
|
|
469
401
|
.np-search-divided.np-search-full-screen.np-search-expanded .np-search-bar {
|
|
470
402
|
height: 4.5rem;
|
|
471
403
|
}
|
|
404
|
+
|
|
405
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
406
|
+
@starting-style {
|
|
407
|
+
.np-search-full-screen.np-search-expanded,
|
|
408
|
+
.np-search-divided.np-search-full-screen.np-search-expanded {
|
|
409
|
+
padding-block-start: 0;
|
|
410
|
+
padding-inline: var(--np-search-pane-margin, 1.5rem);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
472
414
|
</style>
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import '../internal/focus-ring.css';
|
|
2
2
|
import type { SearchProps } from './types.ts';
|
|
3
|
-
declare const Search: import("svelte").Component<SearchProps, {
|
|
3
|
+
declare const Search: import("svelte").Component<SearchProps, {
|
|
4
|
+
show: () => Promise<void>;
|
|
5
|
+
close: () => void;
|
|
6
|
+
}, "element" | "value" | "inputElement" | "expanded">;
|
|
4
7
|
type Search = ReturnType<typeof Search>;
|
|
5
8
|
export default Search;
|
package/dist/search/types.d.ts
CHANGED
|
@@ -1,38 +1,18 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes, HTMLInputAttributes } from 'svelte/elements';
|
|
3
3
|
export interface SearchProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onsearch'> {
|
|
4
|
-
/** Bindable. The query. */
|
|
5
4
|
value?: string;
|
|
6
|
-
/** Hinted search text, shown while the field is empty. */
|
|
7
5
|
placeholder?: string;
|
|
8
|
-
/** Bindable. Whether the results are showing. */
|
|
9
6
|
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
7
|
variant?: 'contained' | 'divided';
|
|
15
|
-
/** `'docked'` drops the results under the field. `'full-screen'` covers the viewport. */
|
|
16
8
|
view?: 'docked' | 'full-screen';
|
|
17
|
-
/** Leading slot. Defaults to a search icon, or a back arrow once expanded. */
|
|
18
9
|
leading?: Snippet;
|
|
19
|
-
/** Trailing slot, for an avatar or actions. A clear button is shown alongside it. */
|
|
20
10
|
trailing?: Snippet;
|
|
21
|
-
/** Accessible name for the field. */
|
|
22
11
|
label?: string;
|
|
23
|
-
/** Accessible name for the clear button. */
|
|
24
12
|
clearLabel?: string;
|
|
25
|
-
/** Accessible name for the back action shown while expanded. */
|
|
26
13
|
backLabel?: string;
|
|
27
|
-
/** Fired on the Enter key, with the current query. */
|
|
28
14
|
onsearch?: (value: string) => void;
|
|
29
|
-
/** Extra attributes for the underlying `input`. */
|
|
30
15
|
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
16
|
resultsAttributes?: HTMLAttributes<HTMLDivElement>;
|
|
37
17
|
element?: HTMLDivElement;
|
|
38
18
|
inputElement?: HTMLInputElement;
|
package/dist/sheet/Sheet.svelte
CHANGED
|
@@ -37,14 +37,6 @@
|
|
|
37
37
|
)
|
|
38
38
|
</script>
|
|
39
39
|
|
|
40
|
-
<!--
|
|
41
|
-
A `dialog` rather than a popover. `showModal()` keeps everything outside the sheet inert for as
|
|
42
|
-
long as it is open, including anything added while it is open, and restores focus on close. A
|
|
43
|
-
hand-rolled trap can only inert what exists at the moment it runs.
|
|
44
|
-
|
|
45
|
-
`closedby="any"` is the light dismiss a popover gets for free. Where it is unsupported the click
|
|
46
|
-
handler below covers it: a click on the backdrop is reported against the dialog itself.
|
|
47
|
-
-->
|
|
48
40
|
<dialog
|
|
49
41
|
{...attributes}
|
|
50
42
|
bind:this={element}
|
|
@@ -58,11 +50,6 @@
|
|
|
58
50
|
}}
|
|
59
51
|
ontoggle={(event) => {
|
|
60
52
|
open = event.newState === 'open'
|
|
61
|
-
/*
|
|
62
|
-
* Without this the browser autofocuses the first focusable descendant, which is the close
|
|
63
|
-
* button when `action` puts one in the header, firing its tooltip on open. Focus the sheet
|
|
64
|
-
* itself instead, so nothing is focused that the user didn't choose to interact with.
|
|
65
|
-
*/
|
|
66
53
|
if (event.newState === 'open') element?.focus()
|
|
67
54
|
ontoggle?.(event)
|
|
68
55
|
}}
|
|
@@ -91,13 +78,7 @@
|
|
|
91
78
|
<style>
|
|
92
79
|
.np-sheet {
|
|
93
80
|
position: fixed;
|
|
94
|
-
/* Focus lands here on open only to stay off the header action, not as something to show. */
|
|
95
81
|
outline: none;
|
|
96
|
-
/*
|
|
97
|
-
* `showModal()` puts a modal sheet in the top layer, above normal stacking order regardless
|
|
98
|
-
* of z-index. A non-modal sheet (`.show()`) stays in normal flow, so it needs a z-index of
|
|
99
|
-
* its own to clear app bars and other overlays.
|
|
100
|
-
*/
|
|
101
82
|
z-index: var(--np-sheet-z-index, 24);
|
|
102
83
|
box-sizing: border-box;
|
|
103
84
|
margin: 0;
|
|
@@ -113,7 +94,6 @@
|
|
|
113
94
|
box-shadow: var(--np-sheet-elevation, var(--np-elevation-1));
|
|
114
95
|
}
|
|
115
96
|
|
|
116
|
-
/* A closed dialog is `display: none` from the user agent, which the enter transition needs. */
|
|
117
97
|
.np-sheet:not([open]) {
|
|
118
98
|
display: none;
|
|
119
99
|
}
|
|
@@ -193,16 +173,11 @@
|
|
|
193
173
|
padding: 1rem 1.5rem 1.5rem;
|
|
194
174
|
}
|
|
195
175
|
|
|
196
|
-
/* `::backdrop` is the scrim, so a modal sheet needs no backdrop element of its own. */
|
|
197
176
|
.np-sheet-modal::backdrop {
|
|
198
177
|
background-color: var(--np-color-scrim);
|
|
199
178
|
opacity: 0.32;
|
|
200
179
|
}
|
|
201
180
|
|
|
202
|
-
/*
|
|
203
|
-
* `display` and `overlay` are discrete properties, so the sheet would appear and vanish without
|
|
204
|
-
* `allow-discrete`. `@starting-style` gives the entry a value to move from.
|
|
205
|
-
*/
|
|
206
181
|
@media (prefers-reduced-motion: no-preference) {
|
|
207
182
|
.np-sheet {
|
|
208
183
|
transition:
|
package/dist/sheet/types.d.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
3
3
|
export interface SheetProps extends Omit<HTMLDialogAttributes, 'open'> {
|
|
4
|
-
/** Bindable. Whether the sheet is showing. */
|
|
5
4
|
open?: boolean;
|
|
6
|
-
/**
|
|
7
|
-
* A modal sheet takes focus and blocks the page behind it; a standard one sits alongside the
|
|
8
|
-
* content and leaves it usable.
|
|
9
|
-
*/
|
|
10
5
|
modal?: boolean;
|
|
11
|
-
/** Which edge the sheet is docked to. `'bottom'` is M3's bottom sheet, the sides are side sheets. */
|
|
12
6
|
placement?: 'bottom' | 'top' | 'start' | 'end';
|
|
13
|
-
/** The drag handle M3 puts on a bottom sheet. Ignored on the other edges. */
|
|
14
7
|
handle?: boolean;
|
|
15
8
|
headline?: string;
|
|
16
|
-
/** Trailing action in the header, usually a close button. */
|
|
17
9
|
action?: Snippet;
|
|
18
10
|
element?: HTMLDialogElement;
|
|
19
11
|
}
|
|
@@ -115,13 +115,6 @@
|
|
|
115
115
|
|
|
116
116
|
const DRAG_THRESHOLD = 3
|
|
117
117
|
|
|
118
|
-
/*
|
|
119
|
-
* The value label is a `manual` popover rather than a plain absolutely positioned element, so it
|
|
120
|
-
* renders in the top layer and isn't clipped by an ancestor that scrolls or clips overflow, the
|
|
121
|
-
* way a `DemoContainer` or any card with `overflow: hidden` would otherwise cut it off. `manual`
|
|
122
|
-
* rather than `hint`, whose open and close the browser drives on its own schedule; this one has
|
|
123
|
-
* to follow the drag exactly.
|
|
124
|
-
*/
|
|
125
118
|
let startLabelElement = $state<HTMLDivElement>()
|
|
126
119
|
let endLabelElement = $state<HTMLDivElement>()
|
|
127
120
|
let startHandleElement = $state<HTMLDivElement>()
|
|
@@ -129,13 +122,6 @@
|
|
|
129
122
|
let hovered = $state<'start' | 'end' | null>(null)
|
|
130
123
|
let focused = $state<'start' | 'end' | null>(null)
|
|
131
124
|
|
|
132
|
-
/*
|
|
133
|
-
* One handle carries the label at a time, the one being interacted with: a drag wins, then the
|
|
134
|
-
* pointer, then the keyboard. Focus only counts when it did not come from a pointer, since a
|
|
135
|
-
* drag focuses the input and `:focus-visible` does match a range input focused that way, which
|
|
136
|
-
* is why the focus ring is suppressed by `np-pointer-focused` too. Without that gate the label
|
|
137
|
-
* would stay up after a drag for as long as the input keeps focus, however far the pointer went.
|
|
138
|
-
*/
|
|
139
125
|
let labelled = $derived(
|
|
140
126
|
disabled ? null : (dragging ?? hovered ?? (pointerFocused ? null : focused)),
|
|
141
127
|
)
|
|
@@ -193,16 +179,8 @@
|
|
|
193
179
|
}
|
|
194
180
|
}
|
|
195
181
|
|
|
196
|
-
/*
|
|
197
|
-
* Hover is derived from where the pointer actually is rather than from the handle's own
|
|
198
|
-
* mouseenter/mouseleave. A handle is only a few pixels wide, so those events are easy to miss
|
|
199
|
-
* between frames, and while a drag holds pointer capture they are routed to the capturing
|
|
200
|
-
* element and never reach the handle at all, which left the label stuck open. The tolerance
|
|
201
|
-
* gives the thin handle a hit area worth aiming at.
|
|
202
|
-
*/
|
|
203
182
|
const HOVER_TOLERANCE = 12
|
|
204
183
|
|
|
205
|
-
/** Distance from the pointer to a handle's box, zero while inside it. */
|
|
206
184
|
const distanceTo = (handle: HTMLDivElement | undefined, e: PointerEvent) => {
|
|
207
185
|
if (!handle) return Infinity
|
|
208
186
|
const r = handle.getBoundingClientRect()
|
|
@@ -597,15 +575,6 @@
|
|
|
597
575
|
block-size var(--np-motion-expressive-fast-effects);
|
|
598
576
|
}
|
|
599
577
|
|
|
600
|
-
/*
|
|
601
|
-
* A `manual` popover rather than a plain absolutely positioned element: it renders in the top
|
|
602
|
-
* layer, anchored to the handle by `position-anchor`/`position-area` rather than by an inset
|
|
603
|
-
* computed from the handle's own box, so it isn't clipped by an ancestor that scrolls or hides
|
|
604
|
-
* overflow. It stays a wrapper around the visible label rather than the popover itself carrying
|
|
605
|
-
* the styling, so it can keep resolving `position-area` in the slider's own writing mode while
|
|
606
|
-
* the label inside always renders horizontally, the same split the previous absolute-positioned
|
|
607
|
-
* version needed for a vertical slider's rotated writing mode.
|
|
608
|
-
*/
|
|
609
578
|
.np-slider-label-anchor[popover] {
|
|
610
579
|
position-area: block-start;
|
|
611
580
|
justify-self: anchor-center;
|
|
@@ -617,11 +586,6 @@
|
|
|
617
586
|
pointer-events: none;
|
|
618
587
|
}
|
|
619
588
|
|
|
620
|
-
/*
|
|
621
|
-
* `display` only while open. Setting it on the base rule would beat the user agent's
|
|
622
|
-
* `display: none` for a closed popover, because author styles win over user agent styles
|
|
623
|
-
* whatever the specificity, leaving the label on screen the whole time.
|
|
624
|
-
*/
|
|
625
589
|
.np-slider-label-anchor:popover-open {
|
|
626
590
|
display: flex;
|
|
627
591
|
opacity: 1;
|
|
@@ -23,16 +23,12 @@
|
|
|
23
23
|
let hovered = $state(false)
|
|
24
24
|
let focused = $state(false)
|
|
25
25
|
|
|
26
|
+
const uid = $props.id()
|
|
26
27
|
const controller = popoverController(() => element)
|
|
27
28
|
|
|
28
29
|
export const show = () => controller.show()
|
|
29
30
|
export const close = () => controller.close()
|
|
30
31
|
|
|
31
|
-
/*
|
|
32
|
-
* `toggle` fires once the popover has settled, so `open` coming back from the DOM already
|
|
33
|
-
* matches it and this does nothing. Syncing on `beforetoggle` instead would land in the middle
|
|
34
|
-
* of the browser's show operation, where `showPopover()` throws.
|
|
35
|
-
*/
|
|
36
32
|
syncOpenEffect(
|
|
37
33
|
() => element,
|
|
38
34
|
() => open,
|
|
@@ -40,11 +36,6 @@
|
|
|
40
36
|
close,
|
|
41
37
|
)
|
|
42
38
|
|
|
43
|
-
/*
|
|
44
|
-
* The timeout lives and dies with the snackbar being open, so there is no timer to clear by
|
|
45
|
-
* hand: closing early or unmounting runs the cleanup. It holds while the snackbar is hovered or
|
|
46
|
-
* keeps focus, and starts over once the snackbar is left alone, so an action stays reachable.
|
|
47
|
-
*/
|
|
48
39
|
$effect(() => {
|
|
49
40
|
if (!open || hovered || focused || timeout <= 0) return
|
|
50
41
|
const timeoutId = setTimeout(close, timeout)
|
|
@@ -52,18 +43,13 @@
|
|
|
52
43
|
})
|
|
53
44
|
</script>
|
|
54
45
|
|
|
55
|
-
<!--
|
|
56
|
-
`alert` is an atomic live region, so a screen reader reads the whole snackbar out when it appears,
|
|
57
|
-
label and supporting text together. It needs no name of its own for that, and the ARIA alert
|
|
58
|
-
pattern gives it none: naming the region after an element inside it only risks that text being
|
|
59
|
-
announced twice. A caller's own `aria-label` reaches the element through `attributes`.
|
|
60
|
-
-->
|
|
61
46
|
<div
|
|
62
47
|
{...attributes}
|
|
63
48
|
{popover}
|
|
64
49
|
class={['np-snackbar', attributes.class]}
|
|
65
50
|
bind:this={element}
|
|
66
51
|
role="alert"
|
|
52
|
+
aria-labelledby="np-snackbar-label-{uid}"
|
|
67
53
|
onpointerenter={() => (hovered = true)}
|
|
68
54
|
onpointerleave={() => (hovered = false)}
|
|
69
55
|
onfocusin={() => (focused = true)}
|
|
@@ -75,7 +61,7 @@
|
|
|
75
61
|
>
|
|
76
62
|
<div class="np-snackbar-inner">
|
|
77
63
|
<div class="np-snackbar-label-container">
|
|
78
|
-
<div class="np-snackbar-label">{label}</div>
|
|
64
|
+
<div id="np-snackbar-label-{uid}" class="np-snackbar-label">{label}</div>
|
|
79
65
|
{#if supportingText}
|
|
80
66
|
<div class="np-snackbar-supporting-text">{supportingText}</div>
|
|
81
67
|
{/if}
|
package/dist/tabs/Tab.svelte
CHANGED
|
@@ -23,10 +23,6 @@
|
|
|
23
23
|
|
|
24
24
|
const tabsContext = getTabsContext()
|
|
25
25
|
|
|
26
|
-
/*
|
|
27
|
-
* The indicator follows the selection one flush behind, so the anchor it is tied to has moved
|
|
28
|
-
* to the tab that is already marked selected rather than to one still being rendered.
|
|
29
|
-
*/
|
|
30
26
|
$effect(() => {
|
|
31
27
|
if (tabsContext.value === value) {
|
|
32
28
|
tabsContext.indicatorValue = value
|
package/dist/tabs/Tabs.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte'
|
|
2
3
|
import Divider from '../divider/Divider.svelte'
|
|
3
4
|
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
4
5
|
import { setTabsContext } from './context.js'
|
|
@@ -12,11 +13,10 @@
|
|
|
12
13
|
...attributes
|
|
13
14
|
}: TabsProps = $props()
|
|
14
15
|
|
|
15
|
-
// svelte-ignore state_referenced_locally
|
|
16
16
|
let tabsContext = $state<TabsContext>({
|
|
17
17
|
value,
|
|
18
18
|
indicatorValue: value,
|
|
19
|
-
variant,
|
|
19
|
+
variant: untrack(() => variant),
|
|
20
20
|
})
|
|
21
21
|
$effect(() => {
|
|
22
22
|
value = tabsContext.value
|
|
@@ -33,8 +33,6 @@
|
|
|
33
33
|
const uid = $props.id()
|
|
34
34
|
let errorText = $derived(issues?.map((issue) => issue.message).join(', '))
|
|
35
35
|
|
|
36
|
-
// Merged rather than set before the spread, so a consumer's own aria wiring adds to the
|
|
37
|
-
// supporting-text wiring instead of silently replacing it.
|
|
38
36
|
const supportingTextId = `supporting-text-${uid}`
|
|
39
37
|
const ids = (...values: (string | undefined | null | false)[]) =>
|
|
40
38
|
values.filter(Boolean).join(' ') || undefined
|
|
@@ -54,12 +54,6 @@
|
|
|
54
54
|
|
|
55
55
|
.np-toolbar-vertical {
|
|
56
56
|
flex-direction: column;
|
|
57
|
-
/*
|
|
58
|
-
* Stacked actions sit right above one another, so a tooltip in its default place covers the
|
|
59
|
-
* button before it. Moving it to the trailing side keeps every action readable, and
|
|
60
|
-
* flip-inline sends it the other way when the window edge really is in the way. Material's
|
|
61
|
-
* Compose API exposes the same idea as TooltipAnchorPosition.End.
|
|
62
|
-
*/
|
|
63
57
|
--np-tooltip-position-area: inline-end;
|
|
64
58
|
--np-tooltip-justify-self: auto;
|
|
65
59
|
--np-tooltip-align-self: anchor-center;
|
|
@@ -72,14 +66,12 @@
|
|
|
72
66
|
--np-rich-tooltip-position-try-fallbacks: flip-inline;
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
/* Docked: full width, flush to the edge, no corner rounding. */
|
|
76
69
|
.np-toolbar-docked {
|
|
77
70
|
width: 100%;
|
|
78
71
|
min-height: 4rem;
|
|
79
72
|
justify-content: center;
|
|
80
73
|
padding-inline: 0.5rem;
|
|
81
74
|
background-color: var(--np-toolbar-container-color, var(--np-color-surface-container));
|
|
82
|
-
/* Clears the home inset when docked to the bottom of a phone screen. */
|
|
83
75
|
padding-block-end: max(0px, env(safe-area-inset-bottom));
|
|
84
76
|
}
|
|
85
77
|
.np-toolbar-docked.np-toolbar-vertical {
|
|
@@ -91,10 +83,6 @@
|
|
|
91
83
|
padding-inline: 0;
|
|
92
84
|
}
|
|
93
85
|
|
|
94
|
-
/*
|
|
95
|
-
* Floating: an island above the content. `position: sticky` keeps it in flow so it never
|
|
96
|
-
* covers the end of the page, which `fixed` would.
|
|
97
|
-
*/
|
|
98
86
|
.np-toolbar-floating {
|
|
99
87
|
position: sticky;
|
|
100
88
|
z-index: 8;
|
package/dist/toolbar/types.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
2
|
export interface ToolbarProps extends HTMLAttributes<HTMLElement> {
|
|
3
|
-
/**
|
|
4
|
-
* `'docked'` spans the full width of its container and sits flush against an edge, for actions
|
|
5
|
-
* that stay the same across pages. It replaces M3's deprecated bottom app bar. `'floating'`
|
|
6
|
-
* hovers above the content as a rounded island, for actions specific to the page.
|
|
7
|
-
*/
|
|
8
3
|
variant?: 'docked' | 'floating';
|
|
9
|
-
/** Which edge a floating toolbar hovers against. Ignored when docked. */
|
|
10
4
|
placement?: 'bottom' | 'top' | 'start' | 'end';
|
|
11
5
|
orientation?: 'horizontal' | 'vertical';
|
|
12
|
-
/** Colour role of a floating toolbar's container. */
|
|
13
6
|
color?: 'standard' | 'vibrant';
|
|
14
7
|
element?: HTMLElement;
|
|
15
8
|
}
|
|
@@ -13,13 +13,6 @@
|
|
|
13
13
|
...attributes
|
|
14
14
|
}: RichTooltipProps = $props()
|
|
15
15
|
|
|
16
|
-
/*
|
|
17
|
-
* The control the tooltip is opened from, passed to `showPopover()` as its source so a
|
|
18
|
-
* programmatic open gets the invoker relationship the declarative one gets for free: the
|
|
19
|
-
* tooltip counts as nested inside any popover its control sits in, rather than dismissing it.
|
|
20
|
-
* Either attribute names a control, since `commandfor` and `popovertarget` are separate
|
|
21
|
-
* properties that set the same relationship.
|
|
22
|
-
*/
|
|
23
16
|
const findControl = () =>
|
|
24
17
|
id
|
|
25
18
|
? (document.querySelector<HTMLElement>(`[commandfor="${id}"], [popovertarget="${id}"]`) ??
|