noph-ui 0.51.2 → 0.53.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 +14 -1
- package/dist/animation.js +55 -2
- package/dist/app-bar/AppBar.svelte +71 -7
- package/dist/app-bar/types.d.ts +6 -0
- package/dist/autocomplete/AutoComplete.svelte +6 -31
- package/dist/autocomplete/AutoComplete.svelte.d.ts +1 -1
- package/dist/badge/Badge.svelte +12 -4
- package/dist/badge/description.d.ts +4 -0
- package/dist/badge/description.js +4 -0
- package/dist/button/Button.svelte +53 -30
- package/dist/button/ButtonGroup.svelte +139 -19
- package/dist/button/ButtonGroup.svelte.d.ts +1 -1
- package/dist/button/IconButton.svelte +51 -33
- package/dist/button/SegmentedButton.svelte +52 -15
- package/dist/button/SplitButton.svelte +77 -3
- package/dist/button/SplitButton.svelte.d.ts +1 -1
- package/dist/button/buttonGroup.d.ts +0 -4
- package/dist/button/buttonGroup.js +0 -9
- package/dist/button/groupContext.d.ts +10 -0
- package/dist/button/groupContext.js +4 -0
- package/dist/button/types.d.ts +19 -0
- package/dist/card/Card.svelte +3 -19
- package/dist/card/types.d.ts +12 -2
- package/dist/carousel/Carousel.svelte +77 -14
- package/dist/carousel/CarouselItem.svelte +26 -14
- package/dist/carousel/carouselMorph.js +1 -1
- package/dist/carousel/carouselStrategy.js +25 -27
- package/dist/checkbox/Checkbox.svelte +6 -8
- package/dist/chip/ChipSet.svelte +28 -2
- package/dist/chip/FilterChip.svelte +33 -17
- package/dist/chip/InputChip.svelte +127 -26
- package/dist/chip/SuggestionChip.svelte +6 -22
- package/dist/chip/SuggestionChip.svelte.d.ts +1 -2
- package/dist/chip/types.d.ts +8 -1
- package/dist/date-picker/Calendar.svelte +34 -31
- package/dist/date-picker/Calendar.svelte.d.ts +2 -0
- package/dist/date-picker/CalendarHeader.svelte +84 -62
- package/dist/date-picker/CalendarHeader.svelte.d.ts +3 -0
- package/dist/date-picker/DatePickerDialog.svelte +135 -75
- package/dist/date-picker/DatePickerDialog.svelte.d.ts +4 -3
- package/dist/date-picker/DateRangePicker.svelte +227 -78
- package/dist/date-picker/DateRangePicker.svelte.d.ts +3 -3
- package/dist/date-picker/DockedDatePicker.svelte +76 -457
- package/dist/date-picker/DockedDatePicker.svelte.d.ts +3 -3
- package/dist/date-picker/DockedDateTimePicker.svelte +128 -518
- package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +3 -3
- package/dist/date-picker/DockedPickerField.svelte +221 -0
- package/dist/date-picker/DockedPickerField.svelte.d.ts +9 -0
- package/dist/date-picker/MonthPanel.svelte +291 -0
- package/dist/date-picker/MonthPanel.svelte.d.ts +12 -0
- package/dist/date-picker/SelectionList.svelte +7 -6
- package/dist/date-picker/SelectionList.svelte.d.ts +1 -0
- package/dist/date-picker/TimeColumn.svelte +2 -6
- package/dist/date-picker/TimeColumn.svelte.d.ts +1 -0
- package/dist/date-picker/YearGrid.svelte +6 -13
- package/dist/date-picker/YearGrid.svelte.d.ts +1 -0
- package/dist/date-picker/dateUtils.d.ts +4 -0
- package/dist/date-picker/dateUtils.js +15 -88
- package/dist/date-picker/exitVisibility.svelte.js +13 -6
- package/dist/date-picker/formatters.d.ts +16 -0
- package/dist/date-picker/formatters.js +48 -15
- package/dist/date-picker/internalTypes.d.ts +59 -1
- package/dist/date-picker/shortcuts.d.ts +11 -0
- package/dist/date-picker/shortcuts.js +40 -0
- package/dist/date-picker/timeUtils.d.ts +8 -3
- package/dist/date-picker/timeUtils.js +33 -51
- package/dist/date-picker/types.d.ts +22 -0
- package/dist/dialog/Dialog.svelte +171 -33
- package/dist/dialog/Dialog.svelte.d.ts +2 -1
- package/dist/dialog/types.d.ts +6 -0
- package/dist/divider/Divider.svelte +36 -10
- package/dist/divider/types.d.ts +1 -0
- package/dist/fab/ExtendedFab.svelte +26 -60
- package/dist/fab/Fab.svelte +13 -48
- package/dist/fab/FabMenu.svelte +102 -44
- package/dist/fab/FabMenu.svelte.d.ts +2 -1
- package/dist/fab/FabMenuItem.svelte +8 -0
- package/dist/fab/FabMenuItem.svelte.d.ts +4 -0
- package/dist/fab/index.d.ts +1 -0
- package/dist/fab/index.js +1 -0
- package/dist/fab/types.d.ts +6 -4
- package/dist/icons/Icon.svelte +1 -4
- package/dist/icons/MenuIcon.svelte +13 -0
- package/dist/icons/MenuIcon.svelte.d.ts +7 -0
- package/dist/icons/MenuOpenIcon.svelte +15 -0
- package/dist/icons/MenuOpenIcon.svelte.d.ts +7 -0
- package/dist/icons/index.d.ts +2 -0
- package/dist/icons/index.js +2 -0
- package/dist/internal/exit.css +33 -0
- package/dist/internal/field.css +337 -0
- package/dist/internal/fieldTypes.d.ts +6 -0
- package/dist/internal/focus-ring.css +25 -6
- package/dist/internal/interest.css +3 -1
- package/dist/internal/pop-in.css +19 -0
- package/dist/internal/visually-hidden.css +11 -0
- package/dist/keyboard-nav.d.ts +12 -1
- package/dist/keyboard-nav.js +104 -14
- package/dist/list/Item.svelte +84 -24
- package/dist/list/List.svelte +35 -2
- package/dist/list/ListItem.svelte +16 -2
- package/dist/list/context.d.ts +6 -0
- package/dist/list/context.js +4 -0
- package/dist/list/types.d.ts +7 -0
- package/dist/media.d.ts +1 -0
- package/dist/media.js +1 -0
- package/dist/menu/Menu.svelte +53 -20
- package/dist/menu/Menu.svelte.d.ts +3 -1
- package/dist/menu/MenuItem.svelte +9 -3
- package/dist/menu/types.d.ts +7 -1
- package/dist/navigation-bar/NavigationBar.svelte +9 -3
- package/dist/navigation-bar/NavigationBarItem.svelte +31 -39
- package/dist/navigation-drawer/NavigationDrawer.svelte +96 -20
- package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +2 -1
- package/dist/navigation-drawer/NavigationDrawerItem.svelte +10 -12
- package/dist/navigation-drawer/NavigationDrawerSection.svelte +49 -0
- package/dist/navigation-drawer/NavigationDrawerSection.svelte.d.ts +4 -0
- package/dist/navigation-drawer/index.d.ts +1 -0
- package/dist/navigation-drawer/index.js +1 -0
- package/dist/navigation-drawer/types.d.ts +12 -0
- package/dist/navigation-rail/NavigationRail.svelte +367 -7
- package/dist/navigation-rail/NavigationRail.svelte.d.ts +1 -1
- package/dist/navigation-rail/NavigationRailItem.svelte +185 -59
- package/dist/navigation-rail/NavigationRailSection.svelte +57 -0
- package/dist/navigation-rail/NavigationRailSection.svelte.d.ts +4 -0
- package/dist/navigation-rail/context.d.ts +6 -0
- package/dist/navigation-rail/context.js +5 -0
- package/dist/navigation-rail/index.d.ts +1 -0
- package/dist/navigation-rail/index.js +1 -0
- package/dist/navigation-rail/morph.d.ts +14 -0
- package/dist/navigation-rail/morph.js +78 -0
- package/dist/navigation-rail/types.d.ts +21 -0
- package/dist/press.svelte.d.ts +0 -1
- package/dist/press.svelte.js +0 -11
- package/dist/progress/CircularProgress.svelte +28 -82
- package/dist/progress/CircularProgress.svelte.d.ts +1 -0
- package/dist/progress/LinearProgress.svelte +67 -171
- package/dist/progress/LinearProgress.svelte.d.ts +1 -0
- package/dist/progress/four-color.css +34 -0
- package/dist/progress/types.d.ts +1 -0
- package/dist/radio/Radio.svelte +13 -10
- package/dist/ripple/Ripple.svelte +8 -16
- package/dist/search/Search.svelte +238 -58
- package/dist/search/Search.svelte.d.ts +1 -0
- package/dist/search/types.d.ts +4 -0
- package/dist/select/Check.svelte +8 -31
- package/dist/select/Check.svelte.d.ts +1 -1
- package/dist/select/NativeSelect.svelte +113 -374
- package/dist/select/NativeSelect.svelte.d.ts +1 -0
- package/dist/select/Option.svelte +2 -11
- package/dist/select/Select.svelte +170 -858
- package/dist/select/Select.svelte.d.ts +1 -0
- package/dist/select/VirtualList.svelte +16 -2
- package/dist/select/VirtualList.svelte.d.ts +6 -2
- package/dist/sheet/Sheet.svelte +229 -22
- package/dist/sheet/Sheet.svelte.d.ts +3 -1
- package/dist/sheet/types.d.ts +10 -0
- package/dist/slider/Slider.svelte +14 -64
- package/dist/slider/Slider.svelte.d.ts +1 -0
- package/dist/snackbar/Snackbar.svelte +3 -0
- package/dist/snackbar/Snackbar.svelte.d.ts +2 -1
- package/dist/switch/Switch.svelte +53 -19
- package/dist/tabs/Tab.svelte +30 -18
- package/dist/tabs/Tabs.svelte +28 -4
- package/dist/tabs/types.d.ts +3 -0
- package/dist/text-field/TextField.svelte +124 -616
- package/dist/text-field/TextField.svelte.d.ts +1 -0
- package/dist/time-picker/ClockDial.svelte +8 -25
- package/dist/time-picker/ClockDial.svelte.d.ts +2 -0
- package/dist/time-picker/DockedTimePicker.svelte +102 -239
- package/dist/time-picker/DockedTimePicker.svelte.d.ts +3 -3
- package/dist/time-picker/PeriodSelector.svelte +8 -7
- package/dist/time-picker/PeriodSelector.svelte.d.ts +1 -0
- package/dist/time-picker/TimeInputs.svelte +13 -2
- package/dist/time-picker/TimePickerDialog.svelte +33 -26
- package/dist/time-picker/TimePickerDialog.svelte.d.ts +3 -3
- package/dist/time-picker/TimePickerPanel.svelte +26 -4
- package/dist/time-picker/TimeSelectors.svelte +4 -9
- package/dist/time-picker/TimeSelectors.svelte.d.ts +1 -0
- package/dist/time-picker/internalTypes.d.ts +2 -0
- package/dist/time-picker/timePickerState.svelte.js +3 -9
- package/dist/toolbar/Toolbar.svelte +21 -11
- package/dist/toolbar/types.d.ts +1 -0
- package/dist/tooltip/RichTooltip.svelte +2 -23
- package/dist/tooltip/RichTooltip.svelte.d.ts +2 -1
- package/dist/tooltip/Tooltip.svelte +5 -25
- package/dist/tooltip/Tooltip.svelte.d.ts +2 -1
- package/package.json +12 -12
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { formReset } from '../form-reset.js'
|
|
3
|
+
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
2
4
|
import { reducedMotion } from '../media.js'
|
|
3
|
-
import { onMount } from 'svelte'
|
|
4
|
-
import {
|
|
5
|
+
import { onMount, untrack } from 'svelte'
|
|
6
|
+
import { motionOf } from '../animation.js'
|
|
7
|
+
import { expandedWidths, resolveItem } from './buttonGroup.js'
|
|
8
|
+
import { setButtonGroupContext } from './groupContext.js'
|
|
5
9
|
import type { ButtonGroupProps } from './types.js'
|
|
6
10
|
|
|
7
11
|
let {
|
|
8
12
|
variant = 'standard',
|
|
13
|
+
size,
|
|
14
|
+
shape,
|
|
15
|
+
selection,
|
|
16
|
+
value = $bindable(),
|
|
17
|
+
required = false,
|
|
18
|
+
name,
|
|
19
|
+
form,
|
|
20
|
+
arrowKeys = true,
|
|
9
21
|
expandedRatio = 0.15,
|
|
10
22
|
compressionLimit = 24,
|
|
11
23
|
element = $bindable(),
|
|
@@ -13,6 +25,37 @@
|
|
|
13
25
|
...attributes
|
|
14
26
|
}: ButtonGroupProps = $props()
|
|
15
27
|
|
|
28
|
+
const selectedValues = $derived(
|
|
29
|
+
value == null ? [] : Array.isArray(value) ? value.map(String) : [String(value)],
|
|
30
|
+
)
|
|
31
|
+
setButtonGroupContext({
|
|
32
|
+
get selection() {
|
|
33
|
+
return selection
|
|
34
|
+
},
|
|
35
|
+
get size() {
|
|
36
|
+
return size
|
|
37
|
+
},
|
|
38
|
+
get shape() {
|
|
39
|
+
return shape
|
|
40
|
+
},
|
|
41
|
+
isSelected: (item) => selectedValues.includes(item),
|
|
42
|
+
toggle: (item) => {
|
|
43
|
+
const on = selectedValues.includes(item)
|
|
44
|
+
if (on && required && selectedValues.length === 1) return
|
|
45
|
+
if (selection === 'single') value = on ? null : item
|
|
46
|
+
else value = on ? selectedValues.filter((v) => v !== item) : [...selectedValues, item]
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// A form reset brings back the selection the group started with.
|
|
51
|
+
const initialValue = untrack(() => (Array.isArray(value) ? [...value] : value))
|
|
52
|
+
const restore = () => (value = Array.isArray(initialValue) ? [...initialValue] : initialValue)
|
|
53
|
+
|
|
54
|
+
// The buttons share one tab stop, Tab enters the group and the arrow keys move inside it.
|
|
55
|
+
const ITEMS = 'button, a[href]'
|
|
56
|
+
const roving = rovingTabindex(ITEMS)
|
|
57
|
+
const arrows = arrowKeyNav(ITEMS, 'horizontal')
|
|
58
|
+
|
|
16
59
|
const settleRatio = 0.75
|
|
17
60
|
|
|
18
61
|
type Timing = { duration: number; easing: string }
|
|
@@ -30,15 +73,15 @@
|
|
|
30
73
|
const items = () =>
|
|
31
74
|
element
|
|
32
75
|
? (Array.from(element.children) as HTMLElement[])
|
|
33
|
-
.filter(
|
|
76
|
+
.filter(
|
|
77
|
+
(item) => !item.hasAttribute('popover') && !item.matches('.np-button-group-input'),
|
|
78
|
+
)
|
|
34
79
|
.map(resolveItem)
|
|
35
80
|
: []
|
|
36
81
|
|
|
37
82
|
const motion = () =>
|
|
38
83
|
element
|
|
39
|
-
?
|
|
40
|
-
getComputedStyle(element).getPropertyValue('--np-motion-expressive-fast-spatial'),
|
|
41
|
-
)
|
|
84
|
+
? motionOf(element, '--np-motion-expressive-fast-spatial')
|
|
42
85
|
: { duration: 0, easing: 'linear' }
|
|
43
86
|
|
|
44
87
|
const animate = (
|
|
@@ -130,6 +173,7 @@
|
|
|
130
173
|
{...attributes}
|
|
131
174
|
role={attributes.role ?? 'group'}
|
|
132
175
|
bind:this={element}
|
|
176
|
+
{@attach (node) => (arrowKeys ? roving(node) : undefined)}
|
|
133
177
|
class={['np-button-group', variant, moving && 'moving', attributes.class]}
|
|
134
178
|
onpointerdown={(event) => {
|
|
135
179
|
press(event.target)
|
|
@@ -152,6 +196,7 @@
|
|
|
152
196
|
press(event.target)
|
|
153
197
|
}
|
|
154
198
|
attributes.onkeydown?.(event)
|
|
199
|
+
if (arrowKeys && !event.defaultPrevented) arrows(event)
|
|
155
200
|
}}
|
|
156
201
|
onkeyup={(event) => {
|
|
157
202
|
release()
|
|
@@ -163,38 +208,109 @@
|
|
|
163
208
|
}}
|
|
164
209
|
>
|
|
165
210
|
{@render children?.()}
|
|
211
|
+
{#if selection && name}
|
|
212
|
+
<!-- Takes part in the form like a radio or checkbox group: submit, reset and required. -->
|
|
213
|
+
<select
|
|
214
|
+
class="np-button-group-input"
|
|
215
|
+
{name}
|
|
216
|
+
{form}
|
|
217
|
+
{required}
|
|
218
|
+
multiple={selection === 'multiple'}
|
|
219
|
+
tabindex="-1"
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
{@attach formReset(restore)}
|
|
222
|
+
>
|
|
223
|
+
{#each selectedValues as selected (selected)}
|
|
224
|
+
<option value={selected} selected>{selected}</option>
|
|
225
|
+
{/each}
|
|
226
|
+
</select>
|
|
227
|
+
{/if}
|
|
166
228
|
</div>
|
|
167
229
|
|
|
168
230
|
<style>
|
|
231
|
+
/* A standard group hugs its buttons. The space between them grows as they shrink, so each
|
|
232
|
+
keeps a 48px target: 18px beside extra small buttons, 12px beside small ones, 8px above. */
|
|
169
233
|
.np-button-group {
|
|
170
|
-
|
|
234
|
+
position: relative;
|
|
235
|
+
display: inline-flex;
|
|
171
236
|
align-items: flex-start;
|
|
172
|
-
gap: var(--np-button-group-space, 0.
|
|
237
|
+
gap: var(--np-button-group-space, var(--_space, 0.5rem));
|
|
173
238
|
isolation: isolate;
|
|
174
239
|
}
|
|
240
|
+
.np-button-group:has(> :global(:is(.np-button, .np-icon-button).s)) {
|
|
241
|
+
--_space: 0.75rem;
|
|
242
|
+
}
|
|
243
|
+
.np-button-group:has(> :global(:is(.np-button, .np-icon-button).xs)) {
|
|
244
|
+
--_space: 1.125rem;
|
|
245
|
+
}
|
|
246
|
+
/* A connected group spans its container and widens its buttons to fill it. */
|
|
175
247
|
.connected {
|
|
248
|
+
display: flex;
|
|
176
249
|
gap: var(--np-button-group-space, 0.125rem);
|
|
177
250
|
}
|
|
251
|
+
.connected > :global(:not([popover], .np-button-group-input)) {
|
|
252
|
+
flex: 1 1 auto;
|
|
253
|
+
}
|
|
254
|
+
.connected > :global(.np-icon-button:is(.xs, .s)) {
|
|
255
|
+
min-width: 3rem;
|
|
256
|
+
}
|
|
257
|
+
/* The corners where two buttons meet, and every corner of a square group. */
|
|
258
|
+
.connected > :global(.xs) {
|
|
259
|
+
--_inner-corner: 0.25rem;
|
|
260
|
+
}
|
|
261
|
+
.connected > :global(:is(.s, .m)) {
|
|
262
|
+
--_inner-corner: 0.5rem;
|
|
263
|
+
}
|
|
264
|
+
.connected > :global(.l) {
|
|
265
|
+
--_inner-corner: 1rem;
|
|
266
|
+
}
|
|
267
|
+
.connected > :global(.xl) {
|
|
268
|
+
--_inner-corner: 1.25rem;
|
|
269
|
+
}
|
|
270
|
+
.connected > :global(.square:not(.selected)) {
|
|
271
|
+
border-radius: var(--np-button-group-inner-corner, var(--_inner-corner));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.np-button-group-input {
|
|
275
|
+
position: absolute;
|
|
276
|
+
inset-inline-start: 0;
|
|
277
|
+
bottom: 0;
|
|
278
|
+
width: 1px;
|
|
279
|
+
height: 1px;
|
|
280
|
+
opacity: 0;
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
}
|
|
178
283
|
|
|
179
284
|
.np-button-group > :global(:focus-visible) {
|
|
180
285
|
z-index: 1;
|
|
181
286
|
}
|
|
287
|
+
/* While widths animate, a button may be narrower than its label for a moment. */
|
|
182
288
|
.moving {
|
|
183
|
-
--_button-
|
|
184
|
-
--_button-label-
|
|
289
|
+
--_button-min-width: 0;
|
|
290
|
+
--_button-label-overflow: hidden;
|
|
185
291
|
}
|
|
186
292
|
|
|
187
|
-
.connected
|
|
188
|
-
|
|
189
|
-
|
|
293
|
+
.connected
|
|
294
|
+
> :global(
|
|
295
|
+
:not([popover], .np-button-group-input):has(~ :not([popover], .np-button-group-input))
|
|
296
|
+
) {
|
|
297
|
+
border-start-end-radius: var(--np-button-group-inner-corner, var(--_inner-corner));
|
|
298
|
+
border-end-end-radius: var(--np-button-group-inner-corner, var(--_inner-corner));
|
|
190
299
|
}
|
|
191
|
-
.connected
|
|
192
|
-
|
|
193
|
-
border-
|
|
300
|
+
.connected
|
|
301
|
+
> :global(:not([popover], .np-button-group-input) ~ :not([popover], .np-button-group-input)) {
|
|
302
|
+
border-start-start-radius: var(--np-button-group-inner-corner, var(--_inner-corner));
|
|
303
|
+
border-end-start-radius: var(--np-button-group-inner-corner, var(--_inner-corner));
|
|
194
304
|
}
|
|
195
305
|
|
|
196
306
|
@media (prefers-reduced-motion: no-preference) {
|
|
197
|
-
.connected
|
|
307
|
+
.connected
|
|
308
|
+
> :global(
|
|
309
|
+
:not([popover], .np-button-group-input):has(~ :not([popover], .np-button-group-input)):is(
|
|
310
|
+
:active,
|
|
311
|
+
.pressed
|
|
312
|
+
)
|
|
313
|
+
) {
|
|
198
314
|
border-start-end-radius: var(
|
|
199
315
|
--np-button-group-pressed-inner-corner,
|
|
200
316
|
var(--np-shape-corner-extra-small)
|
|
@@ -204,7 +320,11 @@
|
|
|
204
320
|
var(--np-shape-corner-extra-small)
|
|
205
321
|
);
|
|
206
322
|
}
|
|
207
|
-
.connected
|
|
323
|
+
.connected
|
|
324
|
+
> :global(
|
|
325
|
+
:not([popover], .np-button-group-input)
|
|
326
|
+
~ :not([popover], .np-button-group-input):is(:active, .pressed)
|
|
327
|
+
) {
|
|
208
328
|
border-start-start-radius: var(
|
|
209
329
|
--np-button-group-pressed-inner-corner,
|
|
210
330
|
var(--np-shape-corner-extra-small)
|
|
@@ -216,7 +336,7 @@
|
|
|
216
336
|
}
|
|
217
337
|
}
|
|
218
338
|
|
|
219
|
-
.connected > :global(:not([popover]).selected) {
|
|
339
|
+
.connected > :global(:not([popover], .np-button-group-input).selected) {
|
|
220
340
|
border-radius: var(--_round-radius, var(--np-shape-corner-full));
|
|
221
341
|
}
|
|
222
342
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ButtonGroupProps } from './types.js';
|
|
2
|
-
declare const ButtonGroup: import("svelte").Component<ButtonGroupProps, {}, "element">;
|
|
2
|
+
declare const ButtonGroup: import("svelte").Component<ButtonGroupProps, {}, "element" | "value">;
|
|
3
3
|
type ButtonGroup = ReturnType<typeof ButtonGroup>;
|
|
4
4
|
export default ButtonGroup;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import Ripple from '../ripple/Ripple.svelte'
|
|
6
6
|
import Tooltip from '../tooltip/Tooltip.svelte'
|
|
7
7
|
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
8
|
+
import { getButtonGroupContext } from './groupContext.js'
|
|
8
9
|
import type { IconButtonProps } from './types.js'
|
|
9
10
|
|
|
10
11
|
let {
|
|
@@ -19,8 +20,8 @@
|
|
|
19
20
|
selected = $bindable(false),
|
|
20
21
|
selectedIcon,
|
|
21
22
|
width = 'default',
|
|
22
|
-
size
|
|
23
|
-
shape
|
|
23
|
+
size: sizeProp,
|
|
24
|
+
shape: shapeProp,
|
|
24
25
|
...attributes
|
|
25
26
|
}: IconButtonProps = $props()
|
|
26
27
|
|
|
@@ -28,6 +29,25 @@
|
|
|
28
29
|
|
|
29
30
|
let isLink = $derived(attributes.href != null && !disabled && !loading)
|
|
30
31
|
let tooltipId = $derived(title && !disabled && !loading ? uid : undefined)
|
|
32
|
+
// Without an aria-label, the tooltip text is the name, and describing the button with it again
|
|
33
|
+
// would read it twice.
|
|
34
|
+
let tooltipNames = $derived(!!tooltipId && !attributes['aria-label'])
|
|
35
|
+
let name = $derived(attributes['aria-label'] ?? (title || undefined))
|
|
36
|
+
|
|
37
|
+
// Inside a ButtonGroup, the group sets the size and shape its buttons leave open, and with
|
|
38
|
+
// `selection` it decides what is selected.
|
|
39
|
+
const group = getButtonGroupContext()
|
|
40
|
+
let size = $derived(sizeProp ?? group?.size ?? 's')
|
|
41
|
+
let shape = $derived(shapeProp ?? group?.shape ?? 'round')
|
|
42
|
+
let groupValue = $derived(
|
|
43
|
+
group?.selection && attributes.value != null ? String(attributes.value) : undefined,
|
|
44
|
+
)
|
|
45
|
+
let isToggle = $derived(toggle || groupValue !== undefined)
|
|
46
|
+
let isSelected = $derived(groupValue !== undefined ? group!.isSelected(groupValue) : selected)
|
|
47
|
+
// A selected toggle swaps its resting shape: round turns square, square turns round.
|
|
48
|
+
let shapeClass = $derived(
|
|
49
|
+
isSelected ? (shape === 'square' ? 'round' : 'square') : loading ? 'square' : shape,
|
|
50
|
+
)
|
|
31
51
|
|
|
32
52
|
const morph = pressMorph()
|
|
33
53
|
|
|
@@ -47,7 +67,7 @@
|
|
|
47
67
|
<CircularProgress aria-label={loadingAriaLabel} indeterminate track={false} />
|
|
48
68
|
</div>
|
|
49
69
|
{/if}
|
|
50
|
-
{#if selectedIcon &&
|
|
70
|
+
{#if selectedIcon && isSelected}
|
|
51
71
|
{@render selectedIcon()}
|
|
52
72
|
{:else if children}
|
|
53
73
|
{@render children()}
|
|
@@ -58,24 +78,25 @@
|
|
|
58
78
|
<a
|
|
59
79
|
{...attributes}
|
|
60
80
|
onclick={(event) => {
|
|
61
|
-
if (!
|
|
81
|
+
if (!isToggle) {
|
|
62
82
|
handlePress()
|
|
63
83
|
}
|
|
64
84
|
attributes.onclick?.(event)
|
|
65
85
|
}}
|
|
66
|
-
aria-describedby={tooltipId
|
|
86
|
+
aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
|
|
67
87
|
interestfor={tooltipId ?? attributes['interestfor']}
|
|
68
|
-
aria-label={
|
|
88
|
+
aria-label={name}
|
|
69
89
|
bind:this={element}
|
|
70
90
|
class={[
|
|
71
91
|
'np-icon-button',
|
|
92
|
+
'np-focus-ring',
|
|
72
93
|
size,
|
|
73
94
|
width,
|
|
74
95
|
variant,
|
|
75
|
-
|
|
96
|
+
shapeClass,
|
|
76
97
|
'enabled',
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
isToggle && 'toggle',
|
|
99
|
+
isSelected && 'selected',
|
|
79
100
|
morph.pressed && 'pressed',
|
|
80
101
|
attributes.class,
|
|
81
102
|
].filter(Boolean)}
|
|
@@ -85,16 +106,18 @@
|
|
|
85
106
|
{:else}
|
|
86
107
|
<button
|
|
87
108
|
{...attributes as HTMLButtonAttributes}
|
|
88
|
-
aria-describedby={tooltipId
|
|
109
|
+
aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
|
|
89
110
|
interestfor={tooltipId ?? attributes['interestfor']}
|
|
90
|
-
aria-label={
|
|
91
|
-
aria-pressed={
|
|
111
|
+
aria-label={name}
|
|
112
|
+
aria-pressed={isToggle ? isSelected : undefined}
|
|
92
113
|
aria-busy={loading}
|
|
93
114
|
type={(attributes['type'] as 'button' | 'submit' | 'reset' | 'button') ?? undefined}
|
|
94
115
|
disabled={disabled || loading}
|
|
95
116
|
bind:this={element}
|
|
96
117
|
onclick={(event) => {
|
|
97
|
-
if (
|
|
118
|
+
if (groupValue !== undefined) {
|
|
119
|
+
group!.toggle(groupValue)
|
|
120
|
+
} else if (toggle) {
|
|
98
121
|
selected = !selected
|
|
99
122
|
} else {
|
|
100
123
|
handlePress()
|
|
@@ -103,12 +126,13 @@
|
|
|
103
126
|
}}
|
|
104
127
|
class={[
|
|
105
128
|
'np-icon-button',
|
|
129
|
+
'np-focus-ring',
|
|
106
130
|
size,
|
|
107
131
|
width,
|
|
108
|
-
|
|
132
|
+
shapeClass,
|
|
109
133
|
disabled || loading ? `${variant}-disabled disabled` : `${variant} enabled`,
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
isToggle && 'toggle',
|
|
135
|
+
isSelected && 'selected',
|
|
112
136
|
morph.pressed && 'pressed',
|
|
113
137
|
attributes.class,
|
|
114
138
|
]}
|
|
@@ -272,17 +296,6 @@
|
|
|
272
296
|
outline-width: 1px;
|
|
273
297
|
outline-offset: -1px;
|
|
274
298
|
}
|
|
275
|
-
.enabled:focus-visible {
|
|
276
|
-
outline-style: solid;
|
|
277
|
-
outline-color: var(--np-color-secondary);
|
|
278
|
-
outline-width: 3px;
|
|
279
|
-
outline-offset: 2px;
|
|
280
|
-
}
|
|
281
|
-
@media (prefers-reduced-motion: no-preference) {
|
|
282
|
-
.enabled:focus-visible {
|
|
283
|
-
animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
299
|
.text {
|
|
287
300
|
color: var(--np-icon-button-icon-color, var(--np-color-on-surface-variant));
|
|
288
301
|
}
|
|
@@ -316,8 +329,14 @@
|
|
|
316
329
|
);
|
|
317
330
|
}
|
|
318
331
|
.tonal.toggle {
|
|
319
|
-
color: var(
|
|
320
|
-
|
|
332
|
+
color: var(
|
|
333
|
+
--np-tonal-icon-button-unselected-icon-color,
|
|
334
|
+
var(--np-color-on-secondary-container)
|
|
335
|
+
);
|
|
336
|
+
background-color: var(
|
|
337
|
+
--np-tonal-icon-button-unselected-container-color,
|
|
338
|
+
var(--np-color-secondary-container)
|
|
339
|
+
);
|
|
321
340
|
}
|
|
322
341
|
.tonal.selected {
|
|
323
342
|
color: var(--np-color-on-secondary);
|
|
@@ -332,8 +351,7 @@
|
|
|
332
351
|
color: var(--np-color-on-surface-variant);
|
|
333
352
|
}
|
|
334
353
|
|
|
335
|
-
.outlined:not(.selected)::after
|
|
336
|
-
.outlined-disabled::after {
|
|
354
|
+
.outlined:not(.selected)::after {
|
|
337
355
|
content: '';
|
|
338
356
|
position: absolute;
|
|
339
357
|
inset: 0;
|
|
@@ -348,8 +366,8 @@
|
|
|
348
366
|
}
|
|
349
367
|
.np-touch {
|
|
350
368
|
position: absolute;
|
|
351
|
-
height: max(
|
|
352
|
-
width: max(
|
|
369
|
+
height: max(3rem, 100%);
|
|
370
|
+
width: max(3rem, 100%);
|
|
353
371
|
}
|
|
354
372
|
|
|
355
373
|
.np-icon-button-circular-progress {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import '../internal/focus-ring.css'
|
|
3
3
|
import CheckIcon from '../icons/CheckIcon.svelte'
|
|
4
|
+
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
4
5
|
import Ripple from '../ripple/Ripple.svelte'
|
|
5
6
|
import type { SegmentedButtonProps } from './types.js'
|
|
6
7
|
|
|
@@ -15,18 +16,43 @@
|
|
|
15
16
|
}: SegmentedButtonProps = $props()
|
|
16
17
|
|
|
17
18
|
let hasError = $derived(!!issues?.length)
|
|
19
|
+
|
|
20
|
+
// Radios already share one tab stop and move with the arrow keys. Checkboxes get the same.
|
|
21
|
+
const SEGMENTS = 'input'
|
|
22
|
+
const roving = rovingTabindex(SEGMENTS)
|
|
23
|
+
const arrows = arrowKeyNav(SEGMENTS, 'horizontal')
|
|
18
24
|
</script>
|
|
19
25
|
|
|
20
26
|
<div
|
|
27
|
+
role={multiSelect ? 'group' : 'radiogroup'}
|
|
21
28
|
{...attributes}
|
|
22
29
|
class={['np-segmented-buttons', hasError && 'np-error', attributes.class]}
|
|
23
30
|
bind:this={element}
|
|
31
|
+
{@attach (node) => (multiSelect ? roving(node) : undefined)}
|
|
32
|
+
onkeydown={(event) => {
|
|
33
|
+
attributes.onkeydown?.(event)
|
|
34
|
+
if (event.defaultPrevented) return
|
|
35
|
+
// Enter picks the focused segment, like Space, instead of submitting the form.
|
|
36
|
+
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
|
|
37
|
+
event.preventDefault()
|
|
38
|
+
if (!(event.target.type === 'radio' && event.target.checked)) event.target.click()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (multiSelect) arrows(event)
|
|
42
|
+
}}
|
|
24
43
|
style="{attributes.style};grid-template-columns: repeat({options.length}, minmax(max-content, 1fr));"
|
|
25
44
|
>
|
|
26
45
|
{#each options as option, i (i)}
|
|
27
46
|
{let optionLabel = $derived(option.label ?? `${name}-${i}`)}
|
|
28
47
|
{let optionValue = $derived(option.value ?? optionLabel)}
|
|
29
|
-
<label
|
|
48
|
+
<label
|
|
49
|
+
class={[
|
|
50
|
+
'np-segmented-button',
|
|
51
|
+
'np-focus-ring-within',
|
|
52
|
+
'np-focus-ring-inset',
|
|
53
|
+
option.icon && 'width-icon',
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
30
56
|
<div class="check-icon-wrapper">
|
|
31
57
|
<div class="check-icon">
|
|
32
58
|
<CheckIcon />
|
|
@@ -48,6 +74,7 @@
|
|
|
48
74
|
{/if}
|
|
49
75
|
{#if !option.disabled}
|
|
50
76
|
<Ripple />
|
|
77
|
+
<span class="np-touch"></span>
|
|
51
78
|
{/if}
|
|
52
79
|
{#if group !== undefined}
|
|
53
80
|
{#if multiSelect}
|
|
@@ -93,11 +120,13 @@
|
|
|
93
120
|
/* State layers take the content color of each variant and state. */
|
|
94
121
|
--np-ripple-hover-color: currentColor;
|
|
95
122
|
--np-ripple-pressed-color: currentColor;
|
|
96
|
-
|
|
123
|
+
/* As wide as its labels need, every segment as wide as the widest. */
|
|
124
|
+
display: inline-grid;
|
|
125
|
+
box-sizing: border-box;
|
|
126
|
+
min-height: 2.5rem;
|
|
97
127
|
color: var(--np-color-on-surface);
|
|
98
128
|
border: 1px solid var(--np-color-outline);
|
|
99
129
|
border-radius: var(--np-shape-corner-full);
|
|
100
|
-
overflow-x: auto;
|
|
101
130
|
}
|
|
102
131
|
|
|
103
132
|
.np-segmented-buttons.np-error {
|
|
@@ -111,7 +140,7 @@
|
|
|
111
140
|
|
|
112
141
|
.np-segmented-button {
|
|
113
142
|
flex: 1;
|
|
114
|
-
padding: 0
|
|
143
|
+
padding: 0 1.75rem;
|
|
115
144
|
text-align: center;
|
|
116
145
|
cursor: pointer;
|
|
117
146
|
font-size: 0.875rem;
|
|
@@ -157,12 +186,30 @@
|
|
|
157
186
|
inset: 0;
|
|
158
187
|
z-index: -1;
|
|
159
188
|
opacity: 0;
|
|
189
|
+
border-radius: inherit;
|
|
160
190
|
transition: opacity var(--np-motion-expressive-fast-effects);
|
|
161
191
|
background-color: var(--np-color-secondary-container);
|
|
162
192
|
}
|
|
163
193
|
.width-icon,
|
|
164
194
|
.np-segmented-button:has(input:checked) {
|
|
165
|
-
padding: 0
|
|
195
|
+
padding: 0 0.75rem;
|
|
196
|
+
}
|
|
197
|
+
/* The outer segments round off the container's ends, for their fill and state layer too. */
|
|
198
|
+
.np-segmented-button:first-child {
|
|
199
|
+
border-start-start-radius: var(--np-shape-corner-full);
|
|
200
|
+
border-end-start-radius: var(--np-shape-corner-full);
|
|
201
|
+
}
|
|
202
|
+
.np-segmented-button:last-child {
|
|
203
|
+
border-start-end-radius: var(--np-shape-corner-full);
|
|
204
|
+
border-end-end-radius: var(--np-shape-corner-full);
|
|
205
|
+
}
|
|
206
|
+
/* Each segment keeps a 48px tall target. */
|
|
207
|
+
.np-touch {
|
|
208
|
+
position: absolute;
|
|
209
|
+
inset-inline: 0;
|
|
210
|
+
top: 50%;
|
|
211
|
+
translate: 0 -50%;
|
|
212
|
+
height: max(3rem, 100%);
|
|
166
213
|
}
|
|
167
214
|
.np-segmented-button:has(input:checked) {
|
|
168
215
|
color: var(--np-color-on-secondary-container);
|
|
@@ -217,13 +264,6 @@
|
|
|
217
264
|
opacity: var(--np-ripple-focus-opacity, 0.1);
|
|
218
265
|
}
|
|
219
266
|
|
|
220
|
-
.np-segmented-button:has(input:focus-visible) {
|
|
221
|
-
outline-style: solid;
|
|
222
|
-
outline-color: var(--np-color-secondary);
|
|
223
|
-
outline-width: 3px;
|
|
224
|
-
outline-offset: -3px;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
267
|
@media (prefers-reduced-motion: no-preference) {
|
|
228
268
|
.np-segmented-button {
|
|
229
269
|
transition: all var(--np-motion-expressive-fast-effects);
|
|
@@ -242,8 +282,5 @@
|
|
|
242
282
|
width var(--np-motion-expressive-fast-effects),
|
|
243
283
|
opacity var(--np-motion-expressive-fast-effects);
|
|
244
284
|
}
|
|
245
|
-
.np-segmented-button:has(input:focus-visible) {
|
|
246
|
-
animation: focusAnimationInset var(--np-motion-expressive-slow-effects) forwards;
|
|
247
|
-
}
|
|
248
285
|
}
|
|
249
286
|
</style>
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
let {
|
|
9
9
|
label = '',
|
|
10
|
+
iconOnly = false,
|
|
10
11
|
icon,
|
|
11
12
|
menu,
|
|
12
13
|
variant = 'filled',
|
|
@@ -29,13 +30,19 @@
|
|
|
29
30
|
<ChevronDownIcon />
|
|
30
31
|
{/snippet}
|
|
31
32
|
|
|
33
|
+
<!-- Tab moves from the leading to the trailing button, the arrow keys are not needed. -->
|
|
32
34
|
<ButtonGroup
|
|
33
35
|
{...attributes}
|
|
34
36
|
bind:element
|
|
35
37
|
variant="connected"
|
|
38
|
+
arrowKeys={false}
|
|
36
39
|
class={['np-split-button', attributes.class]}
|
|
37
40
|
>
|
|
38
|
-
|
|
41
|
+
{#if iconOnly}
|
|
42
|
+
<Button {variant} {size} {disabled} {onclick} start={icon} title={label} />
|
|
43
|
+
{:else}
|
|
44
|
+
<Button {variant} {size} {disabled} {onclick} start={icon}>{label}</Button>
|
|
45
|
+
{/if}
|
|
39
46
|
<Button
|
|
40
47
|
{variant}
|
|
41
48
|
{size}
|
|
@@ -55,12 +62,79 @@
|
|
|
55
62
|
</ButtonGroup>
|
|
56
63
|
|
|
57
64
|
<style>
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
/* A split button keeps its own width instead of spanning its container like other connected
|
|
66
|
+
groups. */
|
|
67
|
+
:global(.np-button-group.np-split-button) {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
}
|
|
70
|
+
:global(.np-split-button > .np-button) {
|
|
71
|
+
flex: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The inner corners per size, rounder while hovered, focused or pressed. */
|
|
75
|
+
:global(.np-split-button > .np-button) {
|
|
76
|
+
--np-button-group-inner-corner: var(--_split-inner, 0.25rem);
|
|
77
|
+
--np-button-group-pressed-inner-corner: var(--_split-inner-active, 0.75rem);
|
|
78
|
+
}
|
|
79
|
+
:global(.np-split-button > .np-button:is(:focus-visible, :active, .pressed)) {
|
|
80
|
+
--_split-inner: var(--_split-inner-active, 0.75rem);
|
|
81
|
+
}
|
|
82
|
+
@media (hover: hover) {
|
|
83
|
+
:global(.np-split-button > .np-button:hover) {
|
|
84
|
+
--_split-inner: var(--_split-inner-active, 0.75rem);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/* Pressed, only the inner corners change, the outer ends stay round. */
|
|
88
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
89
|
+
:global(.np-split-button > .np-button:not(.np-split-button-trigger):is(:active, .pressed)) {
|
|
90
|
+
border-start-start-radius: var(--_round-radius);
|
|
91
|
+
border-end-start-radius: var(--_round-radius);
|
|
92
|
+
}
|
|
93
|
+
:global(.np-split-button > .np-split-button-trigger:is(:active, .pressed)) {
|
|
94
|
+
border-start-end-radius: var(--_round-radius);
|
|
95
|
+
border-end-end-radius: var(--_round-radius);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
:global(.np-split-button > .xs) {
|
|
99
|
+
--_split-inner-active: 0.5rem;
|
|
100
|
+
}
|
|
101
|
+
:global(.np-split-button > .l) {
|
|
102
|
+
--_split-inner: 0.5rem;
|
|
103
|
+
--_split-inner-active: 1.25rem;
|
|
104
|
+
}
|
|
105
|
+
:global(.np-split-button > .xl) {
|
|
106
|
+
--_split-inner: 0.75rem;
|
|
107
|
+
--_split-inner-active: 1.25rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Closed, the caret sits a little toward the leading button; open, it is centered. */
|
|
111
|
+
:global(.np-split-button-trigger .button-icon) {
|
|
112
|
+
position: relative;
|
|
113
|
+
inset-inline-start: var(--_caret-offset, -1px);
|
|
114
|
+
}
|
|
115
|
+
:global(.np-split-button-trigger.m .button-icon) {
|
|
116
|
+
--_caret-offset: -2px;
|
|
117
|
+
}
|
|
118
|
+
:global(.np-split-button-trigger.l .button-icon) {
|
|
119
|
+
--_caret-offset: -3px;
|
|
120
|
+
}
|
|
121
|
+
:global(.np-split-button-trigger.xl .button-icon) {
|
|
122
|
+
--_caret-offset: -6px;
|
|
123
|
+
}
|
|
124
|
+
:global(.np-split-button-trigger[aria-expanded='true'] .button-icon) {
|
|
125
|
+
--_caret-offset: 0px;
|
|
60
126
|
}
|
|
61
127
|
:global(.np-split-button-trigger[aria-expanded='true'] svg) {
|
|
62
128
|
rotate: 180deg;
|
|
63
129
|
}
|
|
130
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
131
|
+
:global(.np-split-button-trigger svg) {
|
|
132
|
+
transition: rotate var(--np-motion-standard-fast-spatial);
|
|
133
|
+
}
|
|
134
|
+
:global(.np-split-button-trigger .button-icon) {
|
|
135
|
+
transition: inset-inline-start var(--np-motion-standard-fast-spatial);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
64
138
|
:global(.np-split-button > .np-split-button-trigger) {
|
|
65
139
|
--np-button-padding: 0;
|
|
66
140
|
aspect-ratio: 1;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { SplitButtonProps } from './types.js';
|
|
2
|
-
declare const SplitButton: import("svelte").Component<SplitButtonProps, {}, "
|
|
2
|
+
declare const SplitButton: import("svelte").Component<SplitButtonProps, {}, "open" | "element">;
|
|
3
3
|
type SplitButton = ReturnType<typeof SplitButton>;
|
|
4
4
|
export default SplitButton;
|
|
@@ -1,6 +1,2 @@
|
|
|
1
1
|
export declare const resolveItem: (item: HTMLElement) => HTMLElement;
|
|
2
2
|
export declare const expandedWidths: (widths: number[], index: number, expandedRatio: number, compressionLimit: number) => number[];
|
|
3
|
-
export declare const parseMotion: (token: string) => {
|
|
4
|
-
duration: number;
|
|
5
|
-
easing: string;
|
|
6
|
-
};
|