noph-ui 0.33.0 → 0.33.2
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/keyboard-nav.d.ts +8 -0
- package/dist/keyboard-nav.js +69 -0
- package/dist/navigation-drawer/NavigationDrawer.svelte +12 -0
- package/dist/navigation-drawer/NavigationDrawerItem.svelte +9 -2
- package/dist/navigation-rail/NavigationRail.svelte +16 -2
- package/dist/navigation-rail/NavigationRailItem.svelte +9 -2
- package/dist/tabs/Tabs.svelte +5 -17
- package/package.json +13 -13
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Attachment } from 'svelte/attachments';
|
|
2
|
+
export declare const rovingTabindex: (itemSelector: string, options?: {
|
|
3
|
+
currentAttr?: string;
|
|
4
|
+
currentValue?: string;
|
|
5
|
+
}) => Attachment<HTMLElement>;
|
|
6
|
+
export declare const arrowKeyNav: (itemSelector: string, orientation?: "vertical" | "horizontal") => (event: KeyboardEvent & {
|
|
7
|
+
currentTarget: EventTarget & HTMLElement;
|
|
8
|
+
}) => void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const rovingTabindex = (itemSelector, options = {}) => {
|
|
2
|
+
const { currentAttr = 'aria-current', currentValue = 'page' } = options;
|
|
3
|
+
return (node) => {
|
|
4
|
+
const getItems = () => Array.from(node.querySelectorAll(itemSelector));
|
|
5
|
+
const setTabstop = (target) => {
|
|
6
|
+
for (const i of getItems()) {
|
|
7
|
+
const wanted = i === target ? 0 : -1;
|
|
8
|
+
if (i.tabIndex !== wanted)
|
|
9
|
+
i.tabIndex = wanted;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const sync = () => {
|
|
13
|
+
const items = getItems();
|
|
14
|
+
if (items.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
const focused = node.querySelector(`${itemSelector}:focus`);
|
|
17
|
+
const current = items.find((i) => i.getAttribute(currentAttr) === currentValue);
|
|
18
|
+
setTabstop(focused ?? current ?? items[0]);
|
|
19
|
+
};
|
|
20
|
+
const onFocusIn = (event) => {
|
|
21
|
+
const target = event.target.closest(itemSelector);
|
|
22
|
+
if (!target || !node.contains(target) || target.tabIndex === 0)
|
|
23
|
+
return;
|
|
24
|
+
setTabstop(target);
|
|
25
|
+
};
|
|
26
|
+
sync();
|
|
27
|
+
node.addEventListener('focusin', onFocusIn);
|
|
28
|
+
const observer = new MutationObserver(sync);
|
|
29
|
+
observer.observe(node, {
|
|
30
|
+
attributes: true,
|
|
31
|
+
attributeFilter: [currentAttr],
|
|
32
|
+
subtree: true,
|
|
33
|
+
childList: true,
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
node.removeEventListener('focusin', onFocusIn);
|
|
37
|
+
observer.disconnect();
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export const arrowKeyNav = (itemSelector, orientation = 'vertical') => (event) => {
|
|
42
|
+
const [prev, next] = orientation === 'vertical'
|
|
43
|
+
? ['ArrowUp', 'ArrowDown']
|
|
44
|
+
: ['ArrowLeft', 'ArrowRight'];
|
|
45
|
+
const { key } = event;
|
|
46
|
+
if (key !== prev && key !== next && key !== 'Home' && key !== 'End')
|
|
47
|
+
return;
|
|
48
|
+
const items = Array.from(event.currentTarget.querySelectorAll(itemSelector));
|
|
49
|
+
if (items.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
const focused = event.currentTarget.querySelector(`${itemSelector}:focus`);
|
|
52
|
+
if (!focused)
|
|
53
|
+
return;
|
|
54
|
+
const currentIndex = items.indexOf(focused);
|
|
55
|
+
let target;
|
|
56
|
+
if (key === 'Home') {
|
|
57
|
+
target = items[0];
|
|
58
|
+
}
|
|
59
|
+
else if (key === 'End') {
|
|
60
|
+
target = items[items.length - 1];
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const delta = key === next ? 1 : -1;
|
|
64
|
+
const index = currentIndex + delta;
|
|
65
|
+
target = index < 0 ? items[items.length - 1] : index >= items.length ? items[0] : items[index];
|
|
66
|
+
}
|
|
67
|
+
target.focus();
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
2
3
|
import type { NavigationDrawerProps } from './types.ts'
|
|
3
4
|
let {
|
|
4
5
|
modal = false,
|
|
@@ -7,12 +8,22 @@
|
|
|
7
8
|
direction = 'ltr',
|
|
8
9
|
popover,
|
|
9
10
|
children,
|
|
11
|
+
onkeydown: userKeydown,
|
|
10
12
|
...attributes
|
|
11
13
|
}: NavigationDrawerProps = $props()
|
|
14
|
+
|
|
15
|
+
const attach = rovingTabindex('.np-navigation-drawer-item')
|
|
16
|
+
const arrowHandler = arrowKeyNav('.np-navigation-drawer-item')
|
|
17
|
+
|
|
18
|
+
const handleKeydown = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
|
|
19
|
+
userKeydown?.(event)
|
|
20
|
+
if (!event.defaultPrevented) arrowHandler(event)
|
|
21
|
+
}
|
|
12
22
|
</script>
|
|
13
23
|
|
|
14
24
|
<nav
|
|
15
25
|
{...attributes}
|
|
26
|
+
{@attach attach}
|
|
16
27
|
bind:this={element}
|
|
17
28
|
popover={modal ? popover || 'auto' : undefined}
|
|
18
29
|
style="--np-navigation-drawer-start: {direction === 'ltr'
|
|
@@ -24,6 +35,7 @@
|
|
|
24
35
|
backdrop && 'np-navigation-drawer-backdrop',
|
|
25
36
|
attributes.class,
|
|
26
37
|
]}
|
|
38
|
+
onkeydown={handleKeydown}
|
|
27
39
|
>
|
|
28
40
|
{#if backdrop}
|
|
29
41
|
<div
|
|
@@ -28,12 +28,14 @@
|
|
|
28
28
|
{#if 'href' in attributes}
|
|
29
29
|
<a
|
|
30
30
|
{...attributes}
|
|
31
|
+
href={attributes.href}
|
|
31
32
|
class={[
|
|
32
33
|
'np-navigation-drawer-item',
|
|
33
34
|
selected && 'np-navigation-drawer-item-selected',
|
|
34
35
|
attributes.class,
|
|
35
36
|
]}
|
|
36
37
|
aria-current={selected ? 'page' : undefined}
|
|
38
|
+
tabindex={selected ? 0 : -1}
|
|
37
39
|
>
|
|
38
40
|
{@render content()}
|
|
39
41
|
</a>
|
|
@@ -46,7 +48,8 @@
|
|
|
46
48
|
attributes.class,
|
|
47
49
|
]}
|
|
48
50
|
type="button"
|
|
49
|
-
aria-
|
|
51
|
+
aria-current={selected ? 'page' : undefined}
|
|
52
|
+
tabindex={selected ? 0 : -1}
|
|
50
53
|
>
|
|
51
54
|
{@render content()}
|
|
52
55
|
</button>
|
|
@@ -97,7 +100,11 @@
|
|
|
97
100
|
outline-color: var(--np-color-secondary);
|
|
98
101
|
outline-width: 3px;
|
|
99
102
|
outline-offset: -3px;
|
|
100
|
-
|
|
103
|
+
}
|
|
104
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
105
|
+
.np-navigation-drawer-item:focus-visible {
|
|
106
|
+
animation: focusAnimation 0.3s ease forwards;
|
|
107
|
+
}
|
|
101
108
|
}
|
|
102
109
|
@keyframes focusAnimation {
|
|
103
110
|
0% {
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
2
3
|
import type { NavigationRailProps } from './types.ts'
|
|
3
4
|
|
|
4
|
-
let { children, ...attributes }: NavigationRailProps = $props()
|
|
5
|
+
let { children, onkeydown: userKeydown, ...attributes }: NavigationRailProps = $props()
|
|
6
|
+
|
|
7
|
+
const attach = rovingTabindex('.np-navigation-action')
|
|
8
|
+
const arrowHandler = arrowKeyNav('.np-navigation-action')
|
|
9
|
+
|
|
10
|
+
const handleKeydown = (event: KeyboardEvent & { currentTarget: EventTarget & HTMLElement }) => {
|
|
11
|
+
userKeydown?.(event)
|
|
12
|
+
if (!event.defaultPrevented) arrowHandler(event)
|
|
13
|
+
}
|
|
5
14
|
</script>
|
|
6
15
|
|
|
7
|
-
<nav
|
|
16
|
+
<nav
|
|
17
|
+
{...attributes}
|
|
18
|
+
{@attach attach}
|
|
19
|
+
class="navigation-rail {attributes.class}"
|
|
20
|
+
onkeydown={handleKeydown}
|
|
21
|
+
>
|
|
8
22
|
{#if children}
|
|
9
23
|
{@render children()}
|
|
10
24
|
{/if}
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
{#if 'href' in attributes}
|
|
20
20
|
<a
|
|
21
21
|
{...attributes}
|
|
22
|
+
href={attributes.href}
|
|
22
23
|
class={['np-navigation-action', selected && 'np-navigation-action-selected', attributes.class]}
|
|
23
24
|
aria-current={selected ? 'page' : undefined}
|
|
25
|
+
tabindex={selected ? 0 : -1}
|
|
24
26
|
>
|
|
25
27
|
{@render content()}
|
|
26
28
|
</a>
|
|
@@ -28,7 +30,8 @@
|
|
|
28
30
|
<button
|
|
29
31
|
{...attributes as HTMLButtonAttributes}
|
|
30
32
|
class={['np-navigation-action', selected && 'np-navigation-action-selected', attributes.class]}
|
|
31
|
-
aria-
|
|
33
|
+
aria-current={selected ? 'page' : undefined}
|
|
34
|
+
tabindex={selected ? 0 : -1}
|
|
32
35
|
>
|
|
33
36
|
{@render content()}
|
|
34
37
|
</button>
|
|
@@ -54,7 +57,11 @@
|
|
|
54
57
|
outline-width: 3px;
|
|
55
58
|
outline-offset: 2px;
|
|
56
59
|
border-radius: 1rem;
|
|
57
|
-
|
|
60
|
+
}
|
|
61
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
62
|
+
.np-navigation-action:focus-visible {
|
|
63
|
+
animation: focusAnimation 0.3s ease forwards;
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
@keyframes focusAnimation {
|
|
60
67
|
0% {
|
package/dist/tabs/Tabs.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Divider from '../divider/Divider.svelte'
|
|
3
|
+
import { arrowKeyNav, rovingTabindex } from '../keyboard-nav.js'
|
|
3
4
|
import { setTabsContext } from './context.js'
|
|
4
5
|
import type { TabsContext, TabsProps } from './types.ts'
|
|
5
6
|
|
|
@@ -31,31 +32,18 @@
|
|
|
31
32
|
tabsContext.variant === 'secondary' ? '--np-indicator-radius: 0;--_indicator-gap: 0' : '',
|
|
32
33
|
)
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
currentTarget: EventTarget & HTMLDivElement
|
|
37
|
-
},
|
|
38
|
-
) => {
|
|
39
|
-
const tabs = Array.from(event.currentTarget.querySelectorAll<HTMLElement>('.np-tab'))
|
|
40
|
-
if (tabs && tabs.length > 0 && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
|
|
41
|
-
const focusedTab = event.currentTarget.querySelector<HTMLElement>('.np-tab:focus')
|
|
42
|
-
const currentIndex = focusedTab ? tabs.indexOf(focusedTab) : 0
|
|
43
|
-
const index = currentIndex + (event.key === 'ArrowRight' ? 1 : -1)
|
|
44
|
-
const newTab =
|
|
45
|
-
index < 0 ? tabs[tabs.length - 1] : index >= tabs.length ? tabs[0] : tabs[index]
|
|
46
|
-
newTab.focus()
|
|
47
|
-
event.preventDefault()
|
|
48
|
-
}
|
|
49
|
-
}
|
|
35
|
+
const attach = rovingTabindex('.np-tab', { currentAttr: 'aria-selected', currentValue: 'true' })
|
|
36
|
+
const onkeydown = arrowKeyNav('.np-tab', 'horizontal')
|
|
50
37
|
</script>
|
|
51
38
|
|
|
52
39
|
<nav {...attributes} bind:this={element} style={secondaryStyle}>
|
|
53
40
|
<div
|
|
41
|
+
{@attach attach}
|
|
54
42
|
class={['np-tabs']}
|
|
55
43
|
role="tablist"
|
|
56
44
|
aria-orientation="horizontal"
|
|
57
45
|
tabindex="-1"
|
|
58
|
-
onkeydown
|
|
46
|
+
{onkeydown}
|
|
59
47
|
>
|
|
60
48
|
{@render children?.()}
|
|
61
49
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "noph-ui",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://noph.dev",
|
|
6
6
|
"repository": {
|
|
@@ -70,25 +70,25 @@
|
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@eslint/js": "^10.0.1",
|
|
72
72
|
"@materialx/material-color-utilities": "^0.4.8",
|
|
73
|
-
"@playwright/test": "^1.
|
|
73
|
+
"@playwright/test": "^1.59.1",
|
|
74
74
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
75
|
-
"@sveltejs/kit": "^2.
|
|
75
|
+
"@sveltejs/kit": "^2.57.1",
|
|
76
76
|
"@sveltejs/package": "^2.5.7",
|
|
77
77
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
78
78
|
"@types/eslint": "^9.6.1",
|
|
79
|
-
"eslint": "^10.1
|
|
79
|
+
"eslint": "^10.2.1",
|
|
80
80
|
"eslint-config-prettier": "^10.1.8",
|
|
81
|
-
"eslint-plugin-svelte": "^3.
|
|
82
|
-
"globals": "^17.
|
|
83
|
-
"prettier": "^3.8.
|
|
81
|
+
"eslint-plugin-svelte": "^3.17.1",
|
|
82
|
+
"globals": "^17.5.0",
|
|
83
|
+
"prettier": "^3.8.3",
|
|
84
84
|
"prettier-plugin-svelte": "^3.5.1",
|
|
85
85
|
"publint": "^0.3.18",
|
|
86
|
-
"svelte": "^5.55.
|
|
87
|
-
"svelte-check": "^4.4.
|
|
88
|
-
"typescript": "^6.0.
|
|
89
|
-
"typescript-eslint": "^8.
|
|
90
|
-
"vite": "8.0.
|
|
91
|
-
"vitest": "^4.1.
|
|
86
|
+
"svelte": "^5.55.4",
|
|
87
|
+
"svelte-check": "^4.4.6",
|
|
88
|
+
"typescript": "^6.0.3",
|
|
89
|
+
"typescript-eslint": "^8.59.0",
|
|
90
|
+
"vite": "8.0.10",
|
|
91
|
+
"vitest": "^4.1.5"
|
|
92
92
|
},
|
|
93
93
|
"svelte": "./dist/index.js",
|
|
94
94
|
"types": "./dist/index.d.ts",
|