noph-ui 0.34.3 → 0.35.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/badge/Badge.svelte +4 -2
- package/dist/badge/Badge.svelte.d.ts +1 -0
- package/dist/button/Button.svelte +3 -5
- package/dist/button/IconButton.svelte +7 -7
- package/dist/button/SegmentedButton.svelte +22 -25
- package/dist/card/Card.svelte +11 -35
- package/dist/checkbox/Checkbox.svelte +68 -32
- package/dist/checkbox/types.d.ts +1 -0
- package/dist/chip/ChipSet.svelte +10 -2
- package/dist/chip/FilterChip.svelte +48 -52
- package/dist/chip/InputChip.svelte +32 -31
- package/dist/chip/InputChip.svelte.d.ts +1 -1
- package/dist/chip/types.d.ts +3 -1
- package/dist/dialog/Dialog.svelte +53 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keyboard-nav.js +12 -4
- package/dist/list/Item.svelte +39 -66
- package/dist/menu/Menu.svelte +10 -1
- package/dist/menu/MenuItem.svelte +4 -4
- package/dist/navigation-drawer/NavigationDrawer.svelte +35 -1
- package/dist/navigation-rail/NavigationRailItem.svelte +2 -2
- package/dist/progress/CircularProgress.svelte +214 -117
- package/dist/progress/LinearProgress.svelte +382 -102
- package/dist/progress/types.d.ts +2 -0
- package/dist/ripple/Ripple.svelte +6 -7
- package/dist/select/Select.svelte +41 -20
- package/dist/slider/Slider.svelte +555 -0
- package/dist/slider/Slider.svelte.d.ts +4 -0
- package/dist/slider/index.d.ts +1 -0
- package/dist/slider/index.js +1 -0
- package/dist/slider/types.d.ts +23 -0
- package/dist/slider/types.js +1 -0
- package/dist/snackbar/Snackbar.svelte +21 -10
- package/dist/switch/Switch.svelte +1 -1
- package/dist/tabs/Tab.svelte +18 -16
- package/dist/tabs/Tabs.svelte +4 -2
- package/dist/tabs/types.d.ts +2 -0
- package/dist/text-field/TextField.svelte +3 -1
- package/dist/themes/defaultTheme.css +3 -9
- package/dist/tooltip/Tooltip.svelte +53 -10
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/package.json +11 -11
|
@@ -10,22 +10,20 @@
|
|
|
10
10
|
label = '',
|
|
11
11
|
icon,
|
|
12
12
|
element = $bindable(),
|
|
13
|
+
actionElement = $bindable(),
|
|
13
14
|
ariaLabelRemove = 'Remove',
|
|
15
|
+
onclick,
|
|
14
16
|
onremove,
|
|
15
17
|
name,
|
|
16
18
|
value,
|
|
17
19
|
...attributes
|
|
18
20
|
}: InputChipProps = $props()
|
|
19
|
-
|
|
20
|
-
let chipLabel: HTMLDivElement | undefined = $state()
|
|
21
21
|
</script>
|
|
22
22
|
|
|
23
23
|
<div
|
|
24
24
|
{...attributes}
|
|
25
25
|
bind:this={element}
|
|
26
|
-
tabindex={disabled ? -1 : 0}
|
|
27
26
|
aria-disabled={disabled}
|
|
28
|
-
role="button"
|
|
29
27
|
class={[
|
|
30
28
|
'np-input-chip',
|
|
31
29
|
icon ? 'np-input-chip-icon' : '',
|
|
@@ -34,17 +32,24 @@
|
|
|
34
32
|
attributes.class,
|
|
35
33
|
]}
|
|
36
34
|
>
|
|
37
|
-
<
|
|
35
|
+
<button
|
|
36
|
+
bind:this={actionElement}
|
|
37
|
+
type="button"
|
|
38
|
+
class="np-input-chip-label"
|
|
39
|
+
aria-pressed={selected === undefined ? undefined : selected}
|
|
40
|
+
{disabled}
|
|
41
|
+
{onclick}
|
|
42
|
+
>
|
|
38
43
|
{#if icon}
|
|
39
44
|
<div class="np-chip-icon">
|
|
40
45
|
{@render icon()}
|
|
41
46
|
</div>
|
|
42
47
|
{/if}
|
|
43
48
|
<div class="np-chip-label">{label || value}</div>
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
</button>
|
|
50
|
+
<input type="hidden" {value} {name} {disabled} />
|
|
46
51
|
{#if !disabled}
|
|
47
|
-
<Ripple forElement={
|
|
52
|
+
<Ripple forElement={actionElement} />
|
|
48
53
|
{/if}
|
|
49
54
|
<IconButton
|
|
50
55
|
{disabled}
|
|
@@ -71,12 +76,14 @@
|
|
|
71
76
|
padding-inline-end: 1px;
|
|
72
77
|
min-width: 0;
|
|
73
78
|
}
|
|
74
|
-
.np-input-chip-label input {
|
|
75
|
-
opacity: 0;
|
|
76
|
-
position: absolute;
|
|
77
|
-
pointer-events: none;
|
|
78
|
-
}
|
|
79
79
|
.np-input-chip-label {
|
|
80
|
+
appearance: none;
|
|
81
|
+
background: none;
|
|
82
|
+
border-width: 0;
|
|
83
|
+
font: inherit;
|
|
84
|
+
margin: 0;
|
|
85
|
+
padding-block: 0;
|
|
86
|
+
padding-inline-end: 0;
|
|
80
87
|
cursor: pointer;
|
|
81
88
|
display: inline-flex;
|
|
82
89
|
align-items: center;
|
|
@@ -88,6 +95,12 @@
|
|
|
88
95
|
z-index: 1;
|
|
89
96
|
padding-inline-start: 0.75rem;
|
|
90
97
|
overflow: hidden;
|
|
98
|
+
min-width: 0;
|
|
99
|
+
}
|
|
100
|
+
/* The chip container draws the focus ring, so the body keeps none of its own.
|
|
101
|
+
`outline-width: 0` would not do, the UA ring is `outline-style: auto`. */
|
|
102
|
+
.np-input-chip-label:focus-visible {
|
|
103
|
+
outline: none;
|
|
91
104
|
}
|
|
92
105
|
.np-chip-icon {
|
|
93
106
|
color: var(--np-color-on-surface-variant);
|
|
@@ -133,7 +146,7 @@
|
|
|
133
146
|
.np-input-chip-selected .np-input-chip-label {
|
|
134
147
|
color: var(--np-color-on-secondary-container);
|
|
135
148
|
}
|
|
136
|
-
.np-input-chip:focus-visible {
|
|
149
|
+
.np-input-chip:has(:focus-visible) {
|
|
137
150
|
outline-style: solid;
|
|
138
151
|
outline-color: var(--np-color-secondary);
|
|
139
152
|
outline-width: 3px;
|
|
@@ -157,24 +170,12 @@
|
|
|
157
170
|
color: var(--np-color-on-surface);
|
|
158
171
|
opacity: 0.38;
|
|
159
172
|
}
|
|
160
|
-
.np-input-chip-disabled.np-input-chip-selected::before {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.np-input-chip-disabled:not(.np-input-chip-selected).np-input-chip::after {
|
|
164
|
-
content: '';
|
|
165
|
-
position: absolute;
|
|
166
|
-
inset: 0;
|
|
167
|
-
border-radius: inherit;
|
|
168
|
-
pointer-events: none;
|
|
169
|
-
border-width: 1px;
|
|
170
|
-
border-style: solid;
|
|
171
|
-
border-color: var(--np-color-on-surface);
|
|
172
|
-
opacity: 0.12;
|
|
173
|
-
}
|
|
174
|
-
.np-input-chip-disabled::before {
|
|
175
|
-
opacity: 0.12;
|
|
173
|
+
.np-input-chip-disabled:not(.np-input-chip-selected)::before {
|
|
174
|
+
outline-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
|
|
175
|
+
background-color: transparent;
|
|
176
176
|
}
|
|
177
177
|
.np-input-chip-selected.np-input-chip-disabled::before {
|
|
178
|
-
|
|
178
|
+
outline-color: transparent;
|
|
179
|
+
background-color: color-mix(in srgb, var(--np-color-on-surface) 12%, transparent);
|
|
179
180
|
}
|
|
180
181
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { InputChipProps } from './types.ts';
|
|
2
|
-
declare const InputChip: import("svelte").Component<InputChipProps, {}, "element" | "selected">;
|
|
2
|
+
declare const InputChip: import("svelte").Component<InputChipProps, {}, "element" | "selected" | "actionElement">;
|
|
3
3
|
type InputChip = ReturnType<typeof InputChip>;
|
|
4
4
|
export default InputChip;
|
package/dist/chip/types.d.ts
CHANGED
|
@@ -26,15 +26,17 @@ export interface FilterChipProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
26
26
|
defaultSelected?: boolean | null;
|
|
27
27
|
onremove?: MouseEventHandler<ButtonElement>;
|
|
28
28
|
}
|
|
29
|
-
export interface InputChipProps extends HTMLAttributes<HTMLDivElement> {
|
|
29
|
+
export interface InputChipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onclick'> {
|
|
30
30
|
selected?: boolean;
|
|
31
31
|
disabled?: boolean;
|
|
32
32
|
label?: string;
|
|
33
33
|
icon?: Snippet;
|
|
34
34
|
ariaLabelRemove?: string;
|
|
35
35
|
element?: HTMLDivElement;
|
|
36
|
+
actionElement?: HTMLButtonElement;
|
|
36
37
|
name?: string;
|
|
37
38
|
value?: string | number;
|
|
39
|
+
onclick?: MouseEventHandler<HTMLButtonElement>;
|
|
38
40
|
onremove?: MouseEventHandler<ButtonElement>;
|
|
39
41
|
}
|
|
40
42
|
export interface ChipSetContext {
|
|
@@ -11,11 +11,16 @@
|
|
|
11
11
|
supportingText,
|
|
12
12
|
actions,
|
|
13
13
|
divider,
|
|
14
|
+
ontoggle,
|
|
14
15
|
...attributes
|
|
15
16
|
}: DialogProps = $props()
|
|
16
17
|
|
|
17
18
|
const uid = $props.id()
|
|
18
19
|
|
|
20
|
+
let dialogEl: HTMLDivElement | undefined = $state()
|
|
21
|
+
let previouslyFocused: HTMLElement | undefined
|
|
22
|
+
let inerted: HTMLElement[] = []
|
|
23
|
+
|
|
19
24
|
export function showPopover() {
|
|
20
25
|
element?.showPopover()
|
|
21
26
|
}
|
|
@@ -23,12 +28,43 @@
|
|
|
23
28
|
export function hidePopover() {
|
|
24
29
|
element?.hidePopover()
|
|
25
30
|
}
|
|
31
|
+
|
|
32
|
+
const trapFocus = () => {
|
|
33
|
+
if (!element) return
|
|
34
|
+
previouslyFocused = document.activeElement as HTMLElement | undefined
|
|
35
|
+
for (let node: HTMLElement | null = element; node && node !== document.body;) {
|
|
36
|
+
const parent: HTMLElement | null = node.parentElement
|
|
37
|
+
if (!parent) break
|
|
38
|
+
for (const sibling of parent.children) {
|
|
39
|
+
if (sibling === node || !(sibling instanceof HTMLElement) || sibling.inert) continue
|
|
40
|
+
sibling.inert = true
|
|
41
|
+
inerted.push(sibling)
|
|
42
|
+
}
|
|
43
|
+
node = parent
|
|
44
|
+
}
|
|
45
|
+
dialogEl?.focus()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const releaseFocus = () => {
|
|
49
|
+
for (const node of inerted) node.inert = false
|
|
50
|
+
inerted = []
|
|
51
|
+
previouslyFocused?.focus?.()
|
|
52
|
+
previouslyFocused = undefined
|
|
53
|
+
}
|
|
26
54
|
</script>
|
|
27
55
|
|
|
28
56
|
<div
|
|
29
57
|
bind:this={element}
|
|
30
58
|
popover="auto"
|
|
31
59
|
{...attributes}
|
|
60
|
+
ontoggle={(event) => {
|
|
61
|
+
if (event.newState === 'open') {
|
|
62
|
+
trapFocus()
|
|
63
|
+
} else {
|
|
64
|
+
releaseFocus()
|
|
65
|
+
}
|
|
66
|
+
ontoggle?.(event)
|
|
67
|
+
}}
|
|
32
68
|
class={['np-dialog-container', !quick && 'np-animate', attributes.class]}
|
|
33
69
|
>
|
|
34
70
|
<div
|
|
@@ -39,24 +75,28 @@
|
|
|
39
75
|
}}
|
|
40
76
|
></div>
|
|
41
77
|
<div
|
|
78
|
+
bind:this={dialogEl}
|
|
42
79
|
class="np-dialog"
|
|
43
80
|
role="dialog"
|
|
81
|
+
tabindex="-1"
|
|
44
82
|
aria-modal="true"
|
|
45
|
-
aria-labelledby=
|
|
46
|
-
aria-describedby={supportingText ?
|
|
83
|
+
aria-labelledby={headline ? `${uid}-dialog-headline` : undefined}
|
|
84
|
+
aria-describedby={supportingText ? `${uid}-dialog-supporting-text` : undefined}
|
|
47
85
|
>
|
|
48
86
|
{#if icon}
|
|
49
87
|
<div class="np-dialog-icon">
|
|
50
88
|
{@render icon()}
|
|
51
89
|
</div>
|
|
52
90
|
{/if}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
91
|
+
{#if headline}
|
|
92
|
+
<h1
|
|
93
|
+
id="{uid}-dialog-headline"
|
|
94
|
+
class="np-dialog-headline"
|
|
95
|
+
style={icon ? 'text-align: center' : ''}
|
|
96
|
+
>
|
|
97
|
+
{headline}
|
|
98
|
+
</h1>
|
|
99
|
+
{/if}
|
|
60
100
|
{#if supportingText}
|
|
61
101
|
<p id="{uid}-dialog-supporting-text" class="np-dialog-supporting-text">{supportingText}</p>
|
|
62
102
|
{/if}
|
|
@@ -126,7 +166,10 @@
|
|
|
126
166
|
inset: 0;
|
|
127
167
|
position: fixed;
|
|
128
168
|
background-color: var(--np-color-scrim);
|
|
129
|
-
opacity: 0.
|
|
169
|
+
opacity: 0.32;
|
|
170
|
+
}
|
|
171
|
+
.np-dialog:focus-visible {
|
|
172
|
+
outline: none;
|
|
130
173
|
}
|
|
131
174
|
|
|
132
175
|
.np-dialog-icon {
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './progress/index.js';
|
|
|
14
14
|
export * from './radio/index.js';
|
|
15
15
|
export * from './ripple/index.js';
|
|
16
16
|
export * from './select/index.js';
|
|
17
|
+
export * from './slider/index.js';
|
|
17
18
|
export * from './snackbar/index.js';
|
|
18
19
|
export * from './switch/index.js';
|
|
19
20
|
export * from './tabs/index.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from './progress/index.js';
|
|
|
14
14
|
export * from './radio/index.js';
|
|
15
15
|
export * from './ripple/index.js';
|
|
16
16
|
export * from './select/index.js';
|
|
17
|
+
export * from './slider/index.js';
|
|
17
18
|
export * from './snackbar/index.js';
|
|
18
19
|
export * from './switch/index.js';
|
|
19
20
|
export * from './tabs/index.js';
|
package/dist/keyboard-nav.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
const focusable = (items) => items.filter((item) => !item.matches(':disabled'));
|
|
2
|
+
const focusedItem = (node, itemSelector, items) => {
|
|
3
|
+
const active = document.activeElement;
|
|
4
|
+
if (!(active instanceof HTMLElement) || !node.contains(active))
|
|
5
|
+
return null;
|
|
6
|
+
const item = active.closest(itemSelector);
|
|
7
|
+
return item && items.includes(item) ? item : null;
|
|
8
|
+
};
|
|
1
9
|
export const rovingTabindex = (itemSelector, options = {}) => {
|
|
2
10
|
const { currentAttr = 'aria-current', currentValue = 'page' } = options;
|
|
3
11
|
return (node) => {
|
|
4
|
-
const getItems = () => Array.from(node.querySelectorAll(itemSelector));
|
|
12
|
+
const getItems = () => focusable(Array.from(node.querySelectorAll(itemSelector)));
|
|
5
13
|
const setTabstop = (target) => {
|
|
6
14
|
for (const i of getItems()) {
|
|
7
15
|
const wanted = i === target ? 0 : -1;
|
|
@@ -13,7 +21,7 @@ export const rovingTabindex = (itemSelector, options = {}) => {
|
|
|
13
21
|
const items = getItems();
|
|
14
22
|
if (items.length === 0)
|
|
15
23
|
return;
|
|
16
|
-
const focused = node
|
|
24
|
+
const focused = focusedItem(node, itemSelector, items);
|
|
17
25
|
const current = items.find((i) => i.getAttribute(currentAttr) === currentValue);
|
|
18
26
|
setTabstop(focused ?? current ?? items[0]);
|
|
19
27
|
};
|
|
@@ -45,10 +53,10 @@ export const arrowKeyNav = (itemSelector, orientation = 'vertical') => (event) =
|
|
|
45
53
|
const { key } = event;
|
|
46
54
|
if (key !== prev && key !== next && key !== 'Home' && key !== 'End')
|
|
47
55
|
return;
|
|
48
|
-
const items = Array.from(event.currentTarget.querySelectorAll(itemSelector));
|
|
56
|
+
const items = focusable(Array.from(event.currentTarget.querySelectorAll(itemSelector)));
|
|
49
57
|
if (items.length === 0)
|
|
50
58
|
return;
|
|
51
|
-
const focused = event.currentTarget
|
|
59
|
+
const focused = focusedItem(event.currentTarget, itemSelector, items);
|
|
52
60
|
if (!focused)
|
|
53
61
|
return;
|
|
54
62
|
const currentIndex = items.indexOf(focused);
|
package/dist/list/Item.svelte
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Ripple from '../ripple/Ripple.svelte'
|
|
3
|
-
import { onMount } from 'svelte'
|
|
4
3
|
import type { ItemProps } from './types.ts'
|
|
5
4
|
|
|
6
5
|
let {
|
|
@@ -10,37 +9,11 @@
|
|
|
10
9
|
children,
|
|
11
10
|
supportingText,
|
|
12
11
|
disabled = false,
|
|
13
|
-
onfocus,
|
|
14
|
-
onblur,
|
|
15
12
|
softFocus = false,
|
|
16
13
|
lazy = false,
|
|
14
|
+
variant,
|
|
17
15
|
...attributes
|
|
18
16
|
}: ItemProps = $props()
|
|
19
|
-
|
|
20
|
-
let focused = $derived(softFocus)
|
|
21
|
-
let visible = $derived(!lazy)
|
|
22
|
-
let element: HTMLButtonElement | HTMLAnchorElement | HTMLDivElement | undefined = $state()
|
|
23
|
-
let observer: IntersectionObserver | undefined
|
|
24
|
-
onMount(() => {
|
|
25
|
-
if (!lazy) {
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
observer = new IntersectionObserver((entries) => {
|
|
29
|
-
entries.forEach((entry) => {
|
|
30
|
-
if (entry.isIntersecting) {
|
|
31
|
-
visible = true
|
|
32
|
-
observer?.disconnect()
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
return () => observer?.disconnect()
|
|
38
|
-
})
|
|
39
|
-
$effect(() => {
|
|
40
|
-
if (element && lazy) {
|
|
41
|
-
observer?.observe(element)
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
17
|
</script>
|
|
45
18
|
|
|
46
19
|
{#snippet content()}
|
|
@@ -70,53 +43,36 @@
|
|
|
70
43
|
{/snippet}
|
|
71
44
|
|
|
72
45
|
{#if disabled}
|
|
73
|
-
<div
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
{
|
|
46
|
+
<div
|
|
47
|
+
{...attributes}
|
|
48
|
+
aria-disabled="true"
|
|
49
|
+
class={['np-item disabled', lazy && 'np-item-lazy', attributes.class]}
|
|
50
|
+
>
|
|
51
|
+
{@render content()}
|
|
77
52
|
</div>
|
|
78
|
-
{:else if
|
|
79
|
-
<div
|
|
53
|
+
{:else if variant === 'text' || variant === undefined}
|
|
54
|
+
<div
|
|
55
|
+
{...attributes}
|
|
56
|
+
class={['np-item', selected && 'selected', lazy && 'np-item-lazy', attributes.class]}
|
|
57
|
+
>
|
|
80
58
|
{@render content()}
|
|
81
59
|
</div>
|
|
82
|
-
{:else if
|
|
60
|
+
{:else if variant === 'button'}
|
|
83
61
|
<button
|
|
84
62
|
{...attributes}
|
|
85
63
|
type={attributes.type ?? 'button'}
|
|
86
|
-
|
|
87
|
-
focused = true
|
|
88
|
-
onfocus?.(event)
|
|
89
|
-
}}
|
|
90
|
-
onblur={(event) => {
|
|
91
|
-
focused = false
|
|
92
|
-
onblur?.(event)
|
|
93
|
-
}}
|
|
94
|
-
class={['np-item', selected && 'selected', attributes.class]}
|
|
95
|
-
bind:this={element}
|
|
64
|
+
class={['np-item', selected && 'selected', lazy && 'np-item-lazy', attributes.class]}
|
|
96
65
|
>
|
|
97
|
-
{
|
|
98
|
-
|
|
99
|
-
<Ripple forceHover={focused} />
|
|
100
|
-
{/if}
|
|
66
|
+
{@render content()}
|
|
67
|
+
<Ripple forceHover={softFocus} />
|
|
101
68
|
</button>
|
|
102
|
-
{:else if
|
|
69
|
+
{:else if variant === 'link'}
|
|
103
70
|
<a
|
|
104
71
|
{...attributes}
|
|
105
|
-
|
|
106
|
-
focused = true
|
|
107
|
-
onfocus?.(event)
|
|
108
|
-
}}
|
|
109
|
-
onblur={(event) => {
|
|
110
|
-
focused = false
|
|
111
|
-
onblur?.(event)
|
|
112
|
-
}}
|
|
113
|
-
class={['np-item', selected && 'selected', attributes.class]}
|
|
114
|
-
bind:this={element}
|
|
72
|
+
class={['np-item', selected && 'selected', lazy && 'np-item-lazy', attributes.class]}
|
|
115
73
|
>
|
|
116
|
-
{
|
|
117
|
-
|
|
118
|
-
<Ripple forceHover={focused} />
|
|
119
|
-
{/if}
|
|
74
|
+
{@render content()}
|
|
75
|
+
<Ripple forceHover={softFocus} />
|
|
120
76
|
</a>
|
|
121
77
|
{/if}
|
|
122
78
|
|
|
@@ -161,9 +117,9 @@
|
|
|
161
117
|
text-align: start;
|
|
162
118
|
overflow: hidden;
|
|
163
119
|
width: 100%;
|
|
164
|
-
padding-block: 0.
|
|
120
|
+
padding-block: 0.625rem;
|
|
165
121
|
padding-inline: 1rem;
|
|
166
|
-
min-height:
|
|
122
|
+
min-height: var(--np-item-container-height, 3.5rem);
|
|
167
123
|
gap: 0.75rem;
|
|
168
124
|
text-decoration: none;
|
|
169
125
|
-webkit-tap-highlight-color: transparent;
|
|
@@ -171,6 +127,23 @@
|
|
|
171
127
|
box-sizing: border-box;
|
|
172
128
|
}
|
|
173
129
|
|
|
130
|
+
.np-item:has(.np-item-supporting-text) {
|
|
131
|
+
min-height: var(--np-item-container-height, 4.5rem);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.np-item-lazy {
|
|
135
|
+
content-visibility: auto;
|
|
136
|
+
contain-intrinsic-block-size: auto var(--np-item-container-height, 3.5rem);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.np-item-lazy:has(.np-item-supporting-text) {
|
|
140
|
+
contain-intrinsic-block-size: auto var(--np-item-container-height, 4.5rem);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.np-item:focus-visible :global(.np-ripple-surface)::before {
|
|
144
|
+
opacity: var(--np-ripple-focus-opacity, 0.1);
|
|
145
|
+
}
|
|
146
|
+
|
|
174
147
|
.np-item:focus-visible {
|
|
175
148
|
outline-style: solid;
|
|
176
149
|
outline-color: var(--np-color-secondary);
|
package/dist/menu/Menu.svelte
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
2
3
|
import type { MenuProps } from './types.ts'
|
|
3
4
|
import { on } from 'svelte/events'
|
|
4
5
|
|
|
6
|
+
const MENU_ITEM_SELECTOR = '[role="menuitem"]'
|
|
7
|
+
const attach = rovingTabindex(MENU_ITEM_SELECTOR)
|
|
8
|
+
const arrowHandler = arrowKeyNav(MENU_ITEM_SELECTOR)
|
|
9
|
+
|
|
5
10
|
let {
|
|
6
11
|
children,
|
|
7
12
|
element = $bindable(),
|
|
@@ -101,8 +106,12 @@
|
|
|
101
106
|
{popover}
|
|
102
107
|
class={['np-menu-container', attributes.class]}
|
|
103
108
|
{style}
|
|
109
|
+
onkeydown={(event) => {
|
|
110
|
+
attributes.onkeydown?.(event)
|
|
111
|
+
if (!event.defaultPrevented) arrowHandler(event)
|
|
112
|
+
}}
|
|
104
113
|
>
|
|
105
|
-
<div class="np-menu">
|
|
114
|
+
<div {@attach attach} class="np-menu" role="none">
|
|
106
115
|
{@render children()}
|
|
107
116
|
</div>
|
|
108
117
|
</div>
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
let { ...attributes }: MenuItemProps = $props()
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
|
-
<
|
|
8
|
+
<div class="np-menu-item" role="none">
|
|
9
9
|
{#if 'href' in attributes}
|
|
10
|
-
<Item {...attributes} variant="link" />
|
|
10
|
+
<Item role="menuitem" {...attributes} variant="link" />
|
|
11
11
|
{:else}
|
|
12
|
-
<Item {...attributes} variant="button" />
|
|
12
|
+
<Item role="menuitem" {...attributes} variant="button" />
|
|
13
13
|
{/if}
|
|
14
|
-
</
|
|
14
|
+
</div>
|
|
15
15
|
|
|
16
16
|
<style>
|
|
17
17
|
.np-menu-item {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
popover,
|
|
10
10
|
children,
|
|
11
11
|
onkeydown: userKeydown,
|
|
12
|
+
ontoggle,
|
|
12
13
|
...attributes
|
|
13
14
|
}: NavigationDrawerProps = $props()
|
|
14
15
|
|
|
@@ -19,6 +20,32 @@
|
|
|
19
20
|
userKeydown?.(event)
|
|
20
21
|
if (!event.defaultPrevented) arrowHandler(event)
|
|
21
22
|
}
|
|
23
|
+
|
|
24
|
+
let previouslyFocused: HTMLElement | undefined
|
|
25
|
+
let inerted: HTMLElement[] = []
|
|
26
|
+
|
|
27
|
+
const trapFocus = () => {
|
|
28
|
+
if (!element) return
|
|
29
|
+
previouslyFocused = document.activeElement as HTMLElement | undefined
|
|
30
|
+
for (let node: HTMLElement | null = element; node && node !== document.body;) {
|
|
31
|
+
const parent: HTMLElement | null = node.parentElement
|
|
32
|
+
if (!parent) break
|
|
33
|
+
for (const sibling of parent.children) {
|
|
34
|
+
if (sibling === node || !(sibling instanceof HTMLElement) || sibling.inert) continue
|
|
35
|
+
sibling.inert = true
|
|
36
|
+
inerted.push(sibling)
|
|
37
|
+
}
|
|
38
|
+
node = parent
|
|
39
|
+
}
|
|
40
|
+
element.querySelector<HTMLElement>('.np-navigation-drawer-item')?.focus()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const releaseFocus = () => {
|
|
44
|
+
for (const node of inerted) node.inert = false
|
|
45
|
+
inerted = []
|
|
46
|
+
previouslyFocused?.focus?.()
|
|
47
|
+
previouslyFocused = undefined
|
|
48
|
+
}
|
|
22
49
|
</script>
|
|
23
50
|
|
|
24
51
|
<nav
|
|
@@ -36,6 +63,13 @@
|
|
|
36
63
|
attributes.class,
|
|
37
64
|
]}
|
|
38
65
|
onkeydown={handleKeydown}
|
|
66
|
+
ontoggle={(event) => {
|
|
67
|
+
if (modal) {
|
|
68
|
+
if (event.newState === 'open') trapFocus()
|
|
69
|
+
else releaseFocus()
|
|
70
|
+
}
|
|
71
|
+
ontoggle?.(event)
|
|
72
|
+
}}
|
|
39
73
|
>
|
|
40
74
|
{#if backdrop}
|
|
41
75
|
<div
|
|
@@ -108,7 +142,7 @@
|
|
|
108
142
|
transition: opacity 0.25s ease;
|
|
109
143
|
}
|
|
110
144
|
.np-navigation-drawer-backdrop[popover]:popover-open .np-backdrop {
|
|
111
|
-
opacity: 0.
|
|
145
|
+
opacity: 0.32;
|
|
112
146
|
@starting-style {
|
|
113
147
|
opacity: 0;
|
|
114
148
|
}
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
font-size: 0.75rem;
|
|
90
90
|
line-height: 1rem;
|
|
91
91
|
font-weight: var(--np-navigation-rail-item-font-weight, 500);
|
|
92
|
-
transition:
|
|
92
|
+
transition: color var(--np-motion-expressive-default-effects);
|
|
93
93
|
color: var(--np-color-on-surface-variant);
|
|
94
94
|
}
|
|
95
95
|
.np-navigation-action-selected .np-navigation-action-icon {
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
|
|
118
118
|
.np-navigation-action-selected .np-navigation-action-label {
|
|
119
119
|
font-weight: var(--np-navigation-rail-item-selected-font-weight, 500);
|
|
120
|
-
color: var(--np-color-
|
|
120
|
+
color: var(--np-color-on-surface);
|
|
121
121
|
}
|
|
122
122
|
.np-touch {
|
|
123
123
|
height: 56px;
|