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.
@@ -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-pressed={selected ? 'true' : undefined}
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
- animation: focusAnimation 0.3s ease forwards;
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 {...attributes} class="navigation-rail {attributes.class}">
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-pressed={selected ? 'true' : undefined}
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
- animation: focusAnimation 0.3s ease forwards;
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% {
@@ -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 handleKeydown = (
35
- event: KeyboardEvent & {
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={handleKeydown}
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.0",
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.58.2",
73
+ "@playwright/test": "^1.59.1",
74
74
  "@sveltejs/adapter-auto": "^7.0.1",
75
- "@sveltejs/kit": "^2.55.0",
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.0",
79
+ "eslint": "^10.2.1",
80
80
  "eslint-config-prettier": "^10.1.8",
81
- "eslint-plugin-svelte": "^3.16.0",
82
- "globals": "^17.4.0",
83
- "prettier": "^3.8.1",
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.1",
87
- "svelte-check": "^4.4.5",
88
- "typescript": "^6.0.2",
89
- "typescript-eslint": "^8.57.2",
90
- "vite": "8.0.3",
91
- "vitest": "^4.1.2"
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",