noph-ui 0.51.2 → 0.52.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 +26 -1
- package/dist/animation.js +50 -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 +55 -20
- package/dist/button/ButtonGroup.svelte +139 -19
- package/dist/button/ButtonGroup.svelte.d.ts +1 -1
- package/dist/button/IconButton.svelte +49 -18
- package/dist/button/SegmentedButton.svelte +44 -4
- 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/checkbox/Checkbox.svelte +5 -0
- package/dist/chip/ChipSet.svelte +28 -2
- package/dist/chip/FilterChip.svelte +32 -4
- 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 +4 -2
- package/dist/date-picker/DatePickerDialog.svelte +1 -0
- package/dist/date-picker/DatePickerDialog.svelte.d.ts +1 -1
- package/dist/date-picker/DateRangePicker.svelte +1 -0
- package/dist/date-picker/DateRangePicker.svelte.d.ts +1 -1
- package/dist/date-picker/DockedDatePicker.svelte +12 -5
- package/dist/date-picker/DockedDatePicker.svelte.d.ts +1 -1
- package/dist/date-picker/DockedDateTimePicker.svelte +12 -5
- package/dist/date-picker/DockedDateTimePicker.svelte.d.ts +1 -1
- package/dist/date-picker/YearGrid.svelte +4 -2
- 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/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 +47 -0
- package/dist/internal/fieldTypes.d.ts +6 -0
- package/dist/internal/focus-ring.css +4 -0
- package/dist/internal/interest.css +3 -1
- package/dist/keyboard-nav.d.ts +12 -1
- package/dist/keyboard-nav.js +104 -14
- package/dist/list/Item.svelte +52 -6
- 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 +70 -18
- package/dist/menu/Menu.svelte.d.ts +2 -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 +29 -13
- package/dist/navigation-drawer/NavigationDrawer.svelte +96 -20
- package/dist/navigation-drawer/NavigationDrawer.svelte.d.ts +2 -1
- package/dist/navigation-drawer/NavigationDrawerItem.svelte +8 -3
- 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 +190 -53
- 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/progress/CircularProgress.svelte +2 -1
- package/dist/progress/LinearProgress.svelte +48 -20
- package/dist/progress/types.d.ts +1 -0
- package/dist/radio/Radio.svelte +7 -1
- package/dist/search/Search.svelte +245 -58
- package/dist/search/types.d.ts +4 -0
- package/dist/select/NativeSelect.svelte +20 -12
- package/dist/select/Option.svelte +1 -2
- package/dist/select/Select.svelte +59 -140
- package/dist/select/VirtualList.svelte +16 -1
- package/dist/select/VirtualList.svelte.d.ts +6 -2
- package/dist/sheet/Sheet.svelte +223 -5
- package/dist/sheet/Sheet.svelte.d.ts +3 -1
- package/dist/sheet/types.d.ts +10 -0
- package/dist/slider/Slider.svelte +1 -27
- package/dist/snackbar/Snackbar.svelte +3 -0
- package/dist/snackbar/Snackbar.svelte.d.ts +2 -1
- package/dist/switch/Switch.svelte +51 -11
- package/dist/tabs/Tab.svelte +28 -14
- package/dist/tabs/Tabs.svelte +28 -4
- package/dist/tabs/types.d.ts +3 -0
- package/dist/text-field/TextField.svelte +49 -13
- package/dist/time-picker/DockedTimePicker.svelte +6 -1
- package/dist/time-picker/DockedTimePicker.svelte.d.ts +1 -1
- package/dist/time-picker/TimePickerDialog.svelte +1 -0
- package/dist/time-picker/TimePickerDialog.svelte.d.ts +1 -1
- package/dist/toolbar/Toolbar.svelte +21 -11
- package/dist/toolbar/types.d.ts +1 -0
- package/dist/tooltip/RichTooltip.svelte.d.ts +1 -1
- package/dist/tooltip/Tooltip.svelte +3 -1
- package/dist/tooltip/Tooltip.svelte.d.ts +1 -1
- package/package.json +1 -1
|
@@ -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,24 @@
|
|
|
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',
|
|
72
92
|
size,
|
|
73
93
|
width,
|
|
74
94
|
variant,
|
|
75
|
-
|
|
95
|
+
shapeClass,
|
|
76
96
|
'enabled',
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
isToggle && 'toggle',
|
|
98
|
+
isSelected && 'selected',
|
|
79
99
|
morph.pressed && 'pressed',
|
|
80
100
|
attributes.class,
|
|
81
101
|
].filter(Boolean)}
|
|
@@ -85,16 +105,18 @@
|
|
|
85
105
|
{:else}
|
|
86
106
|
<button
|
|
87
107
|
{...attributes as HTMLButtonAttributes}
|
|
88
|
-
aria-describedby={tooltipId
|
|
108
|
+
aria-describedby={(!tooltipNames && tooltipId) || attributes['aria-describedby']}
|
|
89
109
|
interestfor={tooltipId ?? attributes['interestfor']}
|
|
90
|
-
aria-label={
|
|
91
|
-
aria-pressed={
|
|
110
|
+
aria-label={name}
|
|
111
|
+
aria-pressed={isToggle ? isSelected : undefined}
|
|
92
112
|
aria-busy={loading}
|
|
93
113
|
type={(attributes['type'] as 'button' | 'submit' | 'reset' | 'button') ?? undefined}
|
|
94
114
|
disabled={disabled || loading}
|
|
95
115
|
bind:this={element}
|
|
96
116
|
onclick={(event) => {
|
|
97
|
-
if (
|
|
117
|
+
if (groupValue !== undefined) {
|
|
118
|
+
group!.toggle(groupValue)
|
|
119
|
+
} else if (toggle) {
|
|
98
120
|
selected = !selected
|
|
99
121
|
} else {
|
|
100
122
|
handlePress()
|
|
@@ -105,10 +127,10 @@
|
|
|
105
127
|
'np-icon-button',
|
|
106
128
|
size,
|
|
107
129
|
width,
|
|
108
|
-
|
|
130
|
+
shapeClass,
|
|
109
131
|
disabled || loading ? `${variant}-disabled disabled` : `${variant} enabled`,
|
|
110
|
-
|
|
111
|
-
|
|
132
|
+
isToggle && 'toggle',
|
|
133
|
+
isSelected && 'selected',
|
|
112
134
|
morph.pressed && 'pressed',
|
|
113
135
|
attributes.class,
|
|
114
136
|
]}
|
|
@@ -278,6 +300,9 @@
|
|
|
278
300
|
outline-width: 3px;
|
|
279
301
|
outline-offset: 2px;
|
|
280
302
|
}
|
|
303
|
+
.enabled:focus-visible :global(.np-ripple-surface)::before {
|
|
304
|
+
opacity: var(--np-ripple-focus-opacity, 0.1);
|
|
305
|
+
}
|
|
281
306
|
@media (prefers-reduced-motion: no-preference) {
|
|
282
307
|
.enabled:focus-visible {
|
|
283
308
|
animation: focusAnimation var(--np-motion-expressive-slow-effects) forwards;
|
|
@@ -316,8 +341,14 @@
|
|
|
316
341
|
);
|
|
317
342
|
}
|
|
318
343
|
.tonal.toggle {
|
|
319
|
-
color: var(
|
|
320
|
-
|
|
344
|
+
color: var(
|
|
345
|
+
--np-tonal-icon-button-unselected-icon-color,
|
|
346
|
+
var(--np-color-on-secondary-container)
|
|
347
|
+
);
|
|
348
|
+
background-color: var(
|
|
349
|
+
--np-tonal-icon-button-unselected-container-color,
|
|
350
|
+
var(--np-color-secondary-container)
|
|
351
|
+
);
|
|
321
352
|
}
|
|
322
353
|
.tonal.selected {
|
|
323
354
|
color: var(--np-color-on-secondary);
|
|
@@ -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,12 +16,30 @@
|
|
|
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)}
|
|
@@ -48,6 +67,7 @@
|
|
|
48
67
|
{/if}
|
|
49
68
|
{#if !option.disabled}
|
|
50
69
|
<Ripple />
|
|
70
|
+
<span class="np-touch"></span>
|
|
51
71
|
{/if}
|
|
52
72
|
{#if group !== undefined}
|
|
53
73
|
{#if multiSelect}
|
|
@@ -93,11 +113,13 @@
|
|
|
93
113
|
/* State layers take the content color of each variant and state. */
|
|
94
114
|
--np-ripple-hover-color: currentColor;
|
|
95
115
|
--np-ripple-pressed-color: currentColor;
|
|
96
|
-
|
|
116
|
+
/* As wide as its labels need, every segment as wide as the widest. */
|
|
117
|
+
display: inline-grid;
|
|
118
|
+
box-sizing: border-box;
|
|
119
|
+
min-height: 2.5rem;
|
|
97
120
|
color: var(--np-color-on-surface);
|
|
98
121
|
border: 1px solid var(--np-color-outline);
|
|
99
122
|
border-radius: var(--np-shape-corner-full);
|
|
100
|
-
overflow-x: auto;
|
|
101
123
|
}
|
|
102
124
|
|
|
103
125
|
.np-segmented-buttons.np-error {
|
|
@@ -111,7 +133,7 @@
|
|
|
111
133
|
|
|
112
134
|
.np-segmented-button {
|
|
113
135
|
flex: 1;
|
|
114
|
-
padding: 0
|
|
136
|
+
padding: 0 1.75rem;
|
|
115
137
|
text-align: center;
|
|
116
138
|
cursor: pointer;
|
|
117
139
|
font-size: 0.875rem;
|
|
@@ -157,12 +179,30 @@
|
|
|
157
179
|
inset: 0;
|
|
158
180
|
z-index: -1;
|
|
159
181
|
opacity: 0;
|
|
182
|
+
border-radius: inherit;
|
|
160
183
|
transition: opacity var(--np-motion-expressive-fast-effects);
|
|
161
184
|
background-color: var(--np-color-secondary-container);
|
|
162
185
|
}
|
|
163
186
|
.width-icon,
|
|
164
187
|
.np-segmented-button:has(input:checked) {
|
|
165
|
-
padding: 0
|
|
188
|
+
padding: 0 0.75rem;
|
|
189
|
+
}
|
|
190
|
+
/* The outer segments round off the container's ends, for their fill and state layer too. */
|
|
191
|
+
.np-segmented-button:first-child {
|
|
192
|
+
border-start-start-radius: var(--np-shape-corner-full);
|
|
193
|
+
border-end-start-radius: var(--np-shape-corner-full);
|
|
194
|
+
}
|
|
195
|
+
.np-segmented-button:last-child {
|
|
196
|
+
border-start-end-radius: var(--np-shape-corner-full);
|
|
197
|
+
border-end-end-radius: var(--np-shape-corner-full);
|
|
198
|
+
}
|
|
199
|
+
/* Each segment keeps a 48px tall target. */
|
|
200
|
+
.np-touch {
|
|
201
|
+
position: absolute;
|
|
202
|
+
inset-inline: 0;
|
|
203
|
+
top: 50%;
|
|
204
|
+
translate: 0 -50%;
|
|
205
|
+
height: max(3rem, 100%);
|
|
166
206
|
}
|
|
167
207
|
.np-segmented-button:has(input:checked) {
|
|
168
208
|
color: var(--np-color-on-secondary-container);
|
|
@@ -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
|
-
};
|
|
@@ -21,12 +21,3 @@ export const expandedWidths = (widths, index, expandedRatio, compressionLimit) =
|
|
|
21
21
|
}
|
|
22
22
|
return next;
|
|
23
23
|
};
|
|
24
|
-
export const parseMotion = (token) => {
|
|
25
|
-
const [time, ...rest] = token.trim().split(/\s+/);
|
|
26
|
-
const value = Number.parseFloat(time);
|
|
27
|
-
const easing = rest.join(' ');
|
|
28
|
-
return {
|
|
29
|
-
duration: Number.isNaN(value) ? 0 : time.endsWith('ms') ? value : value * 1000,
|
|
30
|
-
easing: easing || 'linear',
|
|
31
|
-
};
|
|
32
|
-
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** What a `ButtonGroup` decides for its buttons: their size and shape, and with `selection` which are selected. */
|
|
2
|
+
export interface ButtonGroupContext {
|
|
3
|
+
readonly selection: 'single' | 'multiple' | undefined;
|
|
4
|
+
readonly size: 'xs' | 's' | 'm' | 'l' | 'xl' | undefined;
|
|
5
|
+
readonly shape: 'round' | 'square' | undefined;
|
|
6
|
+
isSelected: (value: string) => boolean;
|
|
7
|
+
toggle: (value: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const setButtonGroupContext: (context: ButtonGroupContext) => ButtonGroupContext;
|
|
10
|
+
export declare const getButtonGroupContext: () => ButtonGroupContext | undefined;
|
package/dist/button/types.d.ts
CHANGED
|
@@ -25,12 +25,31 @@ export interface IconButtonProps extends BaseButtonProps {
|
|
|
25
25
|
}
|
|
26
26
|
export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
27
27
|
variant?: 'standard' | 'connected';
|
|
28
|
+
/** Size of the buttons that don't set their own. */
|
|
29
|
+
size?: BaseButtonProps['size'];
|
|
30
|
+
/** Shape of the buttons that don't set their own. */
|
|
31
|
+
shape?: BaseButtonProps['shape'];
|
|
32
|
+
/**
|
|
33
|
+
* Lets the group manage selection: its buttons with a `value` become toggles, and `value`
|
|
34
|
+
* holds the selected one (`single`) or the selected ones (`multiple`).
|
|
35
|
+
*/
|
|
36
|
+
selection?: 'single' | 'multiple';
|
|
37
|
+
value?: string | string[] | null;
|
|
38
|
+
/** With `selection`, keeps at least one button selected and makes a form require one. */
|
|
39
|
+
required?: boolean;
|
|
40
|
+
/** With `selection`, submits the selected values under this name, like a radio or checkbox group. */
|
|
41
|
+
name?: string;
|
|
42
|
+
form?: string;
|
|
43
|
+
/** Arrow keys move between the buttons, which share one tab stop. Off, Tab visits each. */
|
|
44
|
+
arrowKeys?: boolean;
|
|
28
45
|
expandedRatio?: number;
|
|
29
46
|
compressionLimit?: number;
|
|
30
47
|
element?: HTMLElement;
|
|
31
48
|
}
|
|
32
49
|
export interface SplitButtonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
|
|
33
50
|
label?: string;
|
|
51
|
+
/** Shows only `icon` on the leading button. `label` then names it and becomes its tooltip. */
|
|
52
|
+
iconOnly?: boolean;
|
|
34
53
|
icon?: Snippet;
|
|
35
54
|
menu?: Snippet<[string]>;
|
|
36
55
|
variant?: Exclude<ButtonProps['variant'], 'text'>;
|