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,260 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { syncOpenEffect } from '../popover.svelte.js'
|
|
3
|
+
import type { SheetProps } from './types.js'
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
open = $bindable(false),
|
|
7
|
+
modal = true,
|
|
8
|
+
placement = 'bottom',
|
|
9
|
+
handle = true,
|
|
10
|
+
headline,
|
|
11
|
+
action,
|
|
12
|
+
children,
|
|
13
|
+
element = $bindable(),
|
|
14
|
+
onclose,
|
|
15
|
+
ontoggle,
|
|
16
|
+
...attributes
|
|
17
|
+
}: SheetProps = $props()
|
|
18
|
+
|
|
19
|
+
const uid = $props.id()
|
|
20
|
+
const headlineId = `np-sheet-headline-${uid}`
|
|
21
|
+
|
|
22
|
+
export const show = () => {
|
|
23
|
+
if (!element || element.open) return
|
|
24
|
+
if (modal) element.showModal()
|
|
25
|
+
else element.show()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const close = () => {
|
|
29
|
+
element?.close()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
syncOpenEffect(
|
|
33
|
+
() => element,
|
|
34
|
+
() => open,
|
|
35
|
+
show,
|
|
36
|
+
close,
|
|
37
|
+
)
|
|
38
|
+
</script>
|
|
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
|
+
<dialog
|
|
49
|
+
{...attributes}
|
|
50
|
+
bind:this={element}
|
|
51
|
+
tabindex="-1"
|
|
52
|
+
closedby={modal ? 'any' : 'none'}
|
|
53
|
+
aria-labelledby={headline ? headlineId : attributes['aria-labelledby']}
|
|
54
|
+
class={['np-sheet', `np-sheet-${placement}`, modal && 'np-sheet-modal', attributes.class]}
|
|
55
|
+
onclose={(event) => {
|
|
56
|
+
open = false
|
|
57
|
+
onclose?.(event)
|
|
58
|
+
}}
|
|
59
|
+
ontoggle={(event) => {
|
|
60
|
+
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
|
+
if (event.newState === 'open') element?.focus()
|
|
67
|
+
ontoggle?.(event)
|
|
68
|
+
}}
|
|
69
|
+
onclick={(event) => {
|
|
70
|
+
if (modal && event.target === element) close()
|
|
71
|
+
}}
|
|
72
|
+
>
|
|
73
|
+
{#if handle && placement === 'bottom'}
|
|
74
|
+
<div class="np-sheet-handle" aria-hidden="true"></div>
|
|
75
|
+
{/if}
|
|
76
|
+
{#if headline || action}
|
|
77
|
+
<div class="np-sheet-header">
|
|
78
|
+
{#if headline}
|
|
79
|
+
<h2 id={headlineId} class="np-sheet-headline">{headline}</h2>
|
|
80
|
+
{/if}
|
|
81
|
+
{#if action}
|
|
82
|
+
<div class="np-sheet-action">{@render action()}</div>
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
86
|
+
<div class="np-sheet-content">
|
|
87
|
+
{@render children?.()}
|
|
88
|
+
</div>
|
|
89
|
+
</dialog>
|
|
90
|
+
|
|
91
|
+
<style>
|
|
92
|
+
.np-sheet {
|
|
93
|
+
position: fixed;
|
|
94
|
+
/* Focus lands here on open only to stay off the header action, not as something to show. */
|
|
95
|
+
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
|
+
z-index: var(--np-sheet-z-index, 24);
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
margin: 0;
|
|
104
|
+
padding: 0;
|
|
105
|
+
border: none;
|
|
106
|
+
max-width: none;
|
|
107
|
+
max-height: none;
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
background-color: var(--np-sheet-container-color, var(--np-color-surface-container-low));
|
|
112
|
+
color: var(--np-color-on-surface);
|
|
113
|
+
box-shadow: var(--np-sheet-elevation, var(--np-elevation-1));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* A closed dialog is `display: none` from the user agent, which the enter transition needs. */
|
|
117
|
+
.np-sheet:not([open]) {
|
|
118
|
+
display: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.np-sheet-bottom,
|
|
122
|
+
.np-sheet-top {
|
|
123
|
+
inset-inline: 0;
|
|
124
|
+
width: 100%;
|
|
125
|
+
max-height: var(--np-sheet-size, 50dvh);
|
|
126
|
+
}
|
|
127
|
+
.np-sheet-bottom {
|
|
128
|
+
inset-block-end: 0;
|
|
129
|
+
inset-block-start: auto;
|
|
130
|
+
border-start-start-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
|
|
131
|
+
border-start-end-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
|
|
132
|
+
}
|
|
133
|
+
.np-sheet-top {
|
|
134
|
+
inset-block-start: 0;
|
|
135
|
+
inset-block-end: auto;
|
|
136
|
+
border-end-start-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
|
|
137
|
+
border-end-end-radius: var(--np-sheet-shape, var(--np-shape-corner-extra-large));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.np-sheet-start,
|
|
141
|
+
.np-sheet-end {
|
|
142
|
+
inset-block: 0;
|
|
143
|
+
height: 100dvh;
|
|
144
|
+
width: var(--np-sheet-size, 20rem);
|
|
145
|
+
max-width: 100%;
|
|
146
|
+
}
|
|
147
|
+
.np-sheet-start {
|
|
148
|
+
inset-inline-start: 0;
|
|
149
|
+
inset-inline-end: auto;
|
|
150
|
+
border-start-end-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
|
|
151
|
+
border-end-end-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
|
|
152
|
+
}
|
|
153
|
+
.np-sheet-end {
|
|
154
|
+
inset-inline-end: 0;
|
|
155
|
+
inset-inline-start: auto;
|
|
156
|
+
border-start-start-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
|
|
157
|
+
border-end-start-radius: var(--np-sheet-shape, var(--np-shape-corner-large));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.np-sheet-handle {
|
|
161
|
+
flex: none;
|
|
162
|
+
width: 2rem;
|
|
163
|
+
height: 0.25rem;
|
|
164
|
+
margin: 1rem auto 0;
|
|
165
|
+
border-radius: var(--np-shape-corner-full);
|
|
166
|
+
background-color: var(--np-sheet-handle-color, var(--np-color-outline-variant));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.np-sheet-header {
|
|
170
|
+
flex: none;
|
|
171
|
+
display: flex;
|
|
172
|
+
align-items: center;
|
|
173
|
+
gap: 0.5rem;
|
|
174
|
+
padding: 1rem 1.5rem 0;
|
|
175
|
+
}
|
|
176
|
+
.np-sheet-headline {
|
|
177
|
+
flex: 1;
|
|
178
|
+
margin: 0;
|
|
179
|
+
font-size: 1.5rem;
|
|
180
|
+
line-height: 2rem;
|
|
181
|
+
font-weight: 400;
|
|
182
|
+
}
|
|
183
|
+
.np-sheet-action {
|
|
184
|
+
flex: none;
|
|
185
|
+
display: flex;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.np-sheet-content {
|
|
189
|
+
flex: 1;
|
|
190
|
+
min-height: 0;
|
|
191
|
+
overflow-y: auto;
|
|
192
|
+
overscroll-behavior: contain;
|
|
193
|
+
padding: 1rem 1.5rem 1.5rem;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* `::backdrop` is the scrim, so a modal sheet needs no backdrop element of its own. */
|
|
197
|
+
.np-sheet-modal::backdrop {
|
|
198
|
+
background-color: var(--np-color-scrim);
|
|
199
|
+
opacity: 0.32;
|
|
200
|
+
}
|
|
201
|
+
|
|
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
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
207
|
+
.np-sheet {
|
|
208
|
+
transition:
|
|
209
|
+
translate var(--np-motion-expressive-default-spatial),
|
|
210
|
+
display var(--np-motion-expressive-default-spatial) allow-discrete,
|
|
211
|
+
overlay var(--np-motion-expressive-default-spatial) allow-discrete;
|
|
212
|
+
}
|
|
213
|
+
.np-sheet::backdrop {
|
|
214
|
+
transition:
|
|
215
|
+
opacity var(--np-motion-expressive-fast-effects),
|
|
216
|
+
display var(--np-motion-expressive-fast-effects) allow-discrete,
|
|
217
|
+
overlay var(--np-motion-expressive-fast-effects) allow-discrete;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.np-sheet-bottom {
|
|
221
|
+
translate: 0 100%;
|
|
222
|
+
}
|
|
223
|
+
.np-sheet-top {
|
|
224
|
+
translate: 0 -100%;
|
|
225
|
+
}
|
|
226
|
+
.np-sheet-start {
|
|
227
|
+
translate: -100% 0;
|
|
228
|
+
}
|
|
229
|
+
.np-sheet-end {
|
|
230
|
+
translate: 100% 0;
|
|
231
|
+
}
|
|
232
|
+
.np-sheet[open] {
|
|
233
|
+
translate: 0 0;
|
|
234
|
+
}
|
|
235
|
+
@starting-style {
|
|
236
|
+
.np-sheet-bottom[open] {
|
|
237
|
+
translate: 0 100%;
|
|
238
|
+
}
|
|
239
|
+
.np-sheet-top[open] {
|
|
240
|
+
translate: 0 -100%;
|
|
241
|
+
}
|
|
242
|
+
.np-sheet-start[open] {
|
|
243
|
+
translate: -100% 0;
|
|
244
|
+
}
|
|
245
|
+
.np-sheet-end[open] {
|
|
246
|
+
translate: 100% 0;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.np-sheet-modal::backdrop {
|
|
251
|
+
opacity: 0;
|
|
252
|
+
}
|
|
253
|
+
.np-sheet-modal[open]::backdrop {
|
|
254
|
+
opacity: 0.32;
|
|
255
|
+
@starting-style {
|
|
256
|
+
opacity: 0;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Sheet } from './Sheet.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Sheet } from './Sheet.svelte';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
3
|
+
export interface SheetProps extends Omit<HTMLDialogAttributes, 'open'> {
|
|
4
|
+
/** Bindable. Whether the sheet is showing. */
|
|
5
|
+
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
|
+
modal?: boolean;
|
|
11
|
+
/** Which edge the sheet is docked to. `'bottom'` is M3's bottom sheet, the sides are side sheets. */
|
|
12
|
+
placement?: 'bottom' | 'top' | 'start' | 'end';
|
|
13
|
+
/** The drag handle M3 puts on a bottom sheet. Ignored on the other edges. */
|
|
14
|
+
handle?: boolean;
|
|
15
|
+
headline?: string;
|
|
16
|
+
/** Trailing action in the header, usually a close button. */
|
|
17
|
+
action?: Snippet;
|
|
18
|
+
element?: HTMLDialogElement;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { popoverController, syncOpenEffect } from '../popover.svelte.js'
|
|
2
3
|
import type { SliderProps } from './types.js'
|
|
3
4
|
|
|
4
5
|
let {
|
|
@@ -21,9 +22,14 @@
|
|
|
21
22
|
element = $bindable(),
|
|
22
23
|
inputElement = $bindable(),
|
|
23
24
|
endInputElement = $bindable(),
|
|
25
|
+
issues,
|
|
26
|
+
'aria-invalid': ariaInvalid,
|
|
24
27
|
...attributes
|
|
25
28
|
}: SliderProps = $props()
|
|
26
29
|
|
|
30
|
+
let hasError = $derived(!!issues?.length)
|
|
31
|
+
const uid = $props.id()
|
|
32
|
+
|
|
27
33
|
const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v))
|
|
28
34
|
|
|
29
35
|
let span = $derived(max - min || 1)
|
|
@@ -109,6 +115,38 @@
|
|
|
109
115
|
|
|
110
116
|
const DRAG_THRESHOLD = 3
|
|
111
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
|
+
let startLabelElement = $state<HTMLDivElement>()
|
|
126
|
+
let endLabelElement = $state<HTMLDivElement>()
|
|
127
|
+
let startHandleElement = $state<HTMLDivElement>()
|
|
128
|
+
let endHandleElement = $state<HTMLDivElement>()
|
|
129
|
+
let hovered = $state<'start' | 'end' | null>(null)
|
|
130
|
+
let focused = $state<'start' | 'end' | null>(null)
|
|
131
|
+
|
|
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
|
+
let labelled = $derived(
|
|
140
|
+
disabled ? null : (dragging ?? hovered ?? (pointerFocused ? null : focused)),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const bindLabel = (getElement: () => HTMLDivElement | undefined, side: 'start' | 'end') => {
|
|
144
|
+
const controller = popoverController(getElement)
|
|
145
|
+
syncOpenEffect(getElement, () => labelled === side, controller.show, controller.close)
|
|
146
|
+
}
|
|
147
|
+
bindLabel(() => startLabelElement, 'start')
|
|
148
|
+
bindLabel(() => endLabelElement, 'end')
|
|
149
|
+
|
|
112
150
|
$effect(() => {
|
|
113
151
|
if (!icon || !activeLaneElement) return
|
|
114
152
|
const lane = activeLaneElement
|
|
@@ -155,7 +193,32 @@
|
|
|
155
193
|
}
|
|
156
194
|
}
|
|
157
195
|
|
|
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
|
+
const HOVER_TOLERANCE = 12
|
|
204
|
+
|
|
205
|
+
/** Distance from the pointer to a handle's box, zero while inside it. */
|
|
206
|
+
const distanceTo = (handle: HTMLDivElement | undefined, e: PointerEvent) => {
|
|
207
|
+
if (!handle) return Infinity
|
|
208
|
+
const r = handle.getBoundingClientRect()
|
|
209
|
+
const dx = Math.max(r.left - e.clientX, 0, e.clientX - r.right)
|
|
210
|
+
const dy = Math.max(r.top - e.clientY, 0, e.clientY - r.bottom)
|
|
211
|
+
return Math.hypot(dx, dy)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const updateHover = (e: PointerEvent) => {
|
|
215
|
+
const toStart = distanceTo(startHandleElement, e)
|
|
216
|
+
const toEnd = distanceTo(endHandleElement, e)
|
|
217
|
+
hovered = Math.min(toStart, toEnd) > HOVER_TOLERANCE ? null : toStart <= toEnd ? 'start' : 'end'
|
|
218
|
+
}
|
|
219
|
+
|
|
158
220
|
const onpointermove = (e: PointerEvent) => {
|
|
221
|
+
updateHover(e)
|
|
159
222
|
if (!dragging) return
|
|
160
223
|
if (!tracking) {
|
|
161
224
|
const d = pointerOrigin
|
|
@@ -195,6 +258,7 @@
|
|
|
195
258
|
tracking = false
|
|
196
259
|
pointerOrigin = null
|
|
197
260
|
element?.releasePointerCapture(e.pointerId)
|
|
261
|
+
updateHover(e)
|
|
198
262
|
input?.dispatchEvent(new Event('change', { bubbles: true }))
|
|
199
263
|
}
|
|
200
264
|
|
|
@@ -215,6 +279,7 @@
|
|
|
215
279
|
dragging && 'np-dragging',
|
|
216
280
|
tracking && 'np-tracking',
|
|
217
281
|
pointerFocused && 'np-pointer-focused',
|
|
282
|
+
hasError && !disabled && 'np-error',
|
|
218
283
|
attributes.class,
|
|
219
284
|
]}
|
|
220
285
|
role="presentation"
|
|
@@ -222,8 +287,13 @@
|
|
|
222
287
|
{onpointermove}
|
|
223
288
|
onpointerup={endDrag}
|
|
224
289
|
onpointercancel={endDrag}
|
|
290
|
+
onpointerleave={() => (hovered = null)}
|
|
225
291
|
onkeydown={() => (pointerFocused = false)}
|
|
226
|
-
|
|
292
|
+
onfocusin={(event) => (focused = event.target === endInputElement ? 'end' : 'start')}
|
|
293
|
+
onfocusout={() => {
|
|
294
|
+
focused = null
|
|
295
|
+
pointerFocused = false
|
|
296
|
+
}}
|
|
227
297
|
>
|
|
228
298
|
<div class="np-slider-track" bind:this={trackElement}>
|
|
229
299
|
{#if hasStartStop}
|
|
@@ -247,21 +317,37 @@
|
|
|
247
317
|
<span class={['np-slider-tick', isActive(t) && 'np-on-active']} style="--_t:{t}"></span>
|
|
248
318
|
{/each}
|
|
249
319
|
|
|
250
|
-
<div
|
|
320
|
+
<div
|
|
321
|
+
bind:this={startHandleElement}
|
|
322
|
+
class="np-slider-handle np-slider-handle-start"
|
|
323
|
+
style="anchor-name: --np-slider-{uid}-start"
|
|
324
|
+
></div>
|
|
325
|
+
{#if labeled}
|
|
326
|
+
<div
|
|
327
|
+
bind:this={startLabelElement}
|
|
328
|
+
popover="manual"
|
|
329
|
+
class="np-slider-label-anchor"
|
|
330
|
+
style="position-anchor: --np-slider-{uid}-start"
|
|
331
|
+
>
|
|
332
|
+
<div class="np-slider-label">{labelFor(range ? Math.min(lo, hi) : lo)}</div>
|
|
333
|
+
</div>
|
|
334
|
+
{/if}
|
|
335
|
+
{#if range}
|
|
336
|
+
<div
|
|
337
|
+
bind:this={endHandleElement}
|
|
338
|
+
class="np-slider-handle np-slider-handle-end"
|
|
339
|
+
style="anchor-name: --np-slider-{uid}-end"
|
|
340
|
+
></div>
|
|
251
341
|
{#if labeled}
|
|
252
|
-
<div
|
|
253
|
-
|
|
342
|
+
<div
|
|
343
|
+
bind:this={endLabelElement}
|
|
344
|
+
popover="manual"
|
|
345
|
+
class="np-slider-label-anchor"
|
|
346
|
+
style="position-anchor: --np-slider-{uid}-end"
|
|
347
|
+
>
|
|
348
|
+
<div class="np-slider-label">{labelFor(Math.max(lo, hi))}</div>
|
|
254
349
|
</div>
|
|
255
350
|
{/if}
|
|
256
|
-
</div>
|
|
257
|
-
{#if range}
|
|
258
|
-
<div class="np-slider-handle np-slider-handle-end">
|
|
259
|
-
{#if labeled}
|
|
260
|
-
<div class="np-slider-label-anchor">
|
|
261
|
-
<div class="np-slider-label">{labelFor(Math.max(lo, hi))}</div>
|
|
262
|
-
</div>
|
|
263
|
-
{/if}
|
|
264
|
-
</div>
|
|
265
351
|
{/if}
|
|
266
352
|
</div>
|
|
267
353
|
|
|
@@ -277,6 +363,7 @@
|
|
|
277
363
|
{disabled}
|
|
278
364
|
aria-orientation={orientation}
|
|
279
365
|
aria-valuetext={format ? labelFor(lo) : attributes['aria-valuetext']}
|
|
366
|
+
aria-invalid={hasError ? 'true' : ariaInvalid}
|
|
280
367
|
/>
|
|
281
368
|
{#if range}
|
|
282
369
|
<input
|
|
@@ -377,6 +464,14 @@
|
|
|
377
464
|
background: var(--np-slider-active-track-color, var(--np-color-primary));
|
|
378
465
|
}
|
|
379
466
|
|
|
467
|
+
.np-error {
|
|
468
|
+
--np-slider-active-track-color: var(--np-color-error);
|
|
469
|
+
--np-slider-handle-color: var(--np-color-error);
|
|
470
|
+
--np-slider-active-stop-color: var(--np-color-on-error);
|
|
471
|
+
--np-slider-label-container-color: var(--np-color-error);
|
|
472
|
+
--np-slider-label-text-color: var(--np-color-on-error);
|
|
473
|
+
}
|
|
474
|
+
|
|
380
475
|
.np-slider-inactive-start {
|
|
381
476
|
inset-inline-start: 0;
|
|
382
477
|
inset-inline-end: calc(100% - var(--_i1));
|
|
@@ -502,15 +597,56 @@
|
|
|
502
597
|
block-size var(--np-motion-expressive-fast-effects);
|
|
503
598
|
}
|
|
504
599
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
.np-slider-label-anchor[popover] {
|
|
610
|
+
position-area: block-start;
|
|
611
|
+
justify-self: anchor-center;
|
|
612
|
+
margin: 0 0 0.375rem 0;
|
|
613
|
+
border: none;
|
|
614
|
+
background: none;
|
|
615
|
+
padding: 0;
|
|
510
616
|
justify-content: center;
|
|
511
617
|
pointer-events: none;
|
|
512
618
|
}
|
|
513
619
|
|
|
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
|
+
.np-slider-label-anchor:popover-open {
|
|
626
|
+
display: flex;
|
|
627
|
+
opacity: 1;
|
|
628
|
+
scale: 1;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
632
|
+
.np-slider-label-anchor:popover-open {
|
|
633
|
+
animation:
|
|
634
|
+
fadeIn var(--np-motion-expressive-fast-effects),
|
|
635
|
+
scaleIn var(--np-motion-expressive-fast-spatial);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@keyframes fadeIn {
|
|
640
|
+
from {
|
|
641
|
+
opacity: 0;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
@keyframes scaleIn {
|
|
645
|
+
from {
|
|
646
|
+
scale: 0.8;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
514
650
|
.np-slider-label {
|
|
515
651
|
flex: none;
|
|
516
652
|
writing-mode: horizontal-tb;
|
|
@@ -526,18 +662,6 @@
|
|
|
526
662
|
font-weight: 500;
|
|
527
663
|
text-align: center;
|
|
528
664
|
white-space: nowrap;
|
|
529
|
-
opacity: 0;
|
|
530
|
-
transition: opacity var(--np-motion-expressive-fast-effects);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
.np-slider.np-labeled:hover .np-slider-label,
|
|
534
|
-
.np-slider.np-labeled.np-dragging .np-slider-label,
|
|
535
|
-
.np-slider.np-labeled:has(.np-slider-input:focus-visible) .np-slider-label {
|
|
536
|
-
opacity: 1;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
.np-slider.np-disabled .np-slider-label {
|
|
540
|
-
opacity: 0;
|
|
541
665
|
}
|
|
542
666
|
|
|
543
667
|
.np-slider-input {
|
package/dist/slider/types.d.ts
CHANGED