@data-slot/navigation-menu 0.1.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/README.md +223 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +1 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @data-slot/navigation-menu
|
|
2
|
+
|
|
3
|
+
Headless navigation menu (mega menu) component for vanilla JavaScript. Accessible, unstyled, tiny.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @data-slot/navigation-menu
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<nav data-slot="navigation-menu">
|
|
15
|
+
<ul data-slot="navigation-menu-list">
|
|
16
|
+
<li data-slot="navigation-menu-item" data-value="products">
|
|
17
|
+
<button data-slot="navigation-menu-trigger">Products</button>
|
|
18
|
+
<div data-slot="navigation-menu-content">
|
|
19
|
+
<a href="/product-a">Product A</a>
|
|
20
|
+
<a href="/product-b">Product B</a>
|
|
21
|
+
</div>
|
|
22
|
+
</li>
|
|
23
|
+
<li data-slot="navigation-menu-item" data-value="company">
|
|
24
|
+
<button data-slot="navigation-menu-trigger">Company</button>
|
|
25
|
+
<div data-slot="navigation-menu-content">
|
|
26
|
+
<a href="/about">About</a>
|
|
27
|
+
<a href="/careers">Careers</a>
|
|
28
|
+
</div>
|
|
29
|
+
</li>
|
|
30
|
+
<div data-slot="navigation-menu-indicator"></div>
|
|
31
|
+
</ul>
|
|
32
|
+
<div data-slot="navigation-menu-viewport"></div>
|
|
33
|
+
</nav>
|
|
34
|
+
|
|
35
|
+
<script type="module">
|
|
36
|
+
import { create } from "@data-slot/navigation-menu";
|
|
37
|
+
|
|
38
|
+
const controllers = create();
|
|
39
|
+
</script>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
### `create(scope?)`
|
|
45
|
+
|
|
46
|
+
Auto-discover and bind all navigation menu instances in a scope (defaults to `document`).
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { create } from "@data-slot/navigation-menu";
|
|
50
|
+
|
|
51
|
+
const controllers = create(); // Returns NavigationMenuController[]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `createNavigationMenu(root, options?)`
|
|
55
|
+
|
|
56
|
+
Create a controller for a specific element.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { createNavigationMenu } from "@data-slot/navigation-menu";
|
|
60
|
+
|
|
61
|
+
const menu = createNavigationMenu(element, {
|
|
62
|
+
delayOpen: 200,
|
|
63
|
+
delayClose: 150,
|
|
64
|
+
onValueChange: (value) => console.log(value),
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Options
|
|
69
|
+
|
|
70
|
+
| Option | Type | Default | Description |
|
|
71
|
+
|--------|------|---------|-------------|
|
|
72
|
+
| `delayOpen` | `number` | `200` | Delay before opening on hover (ms) |
|
|
73
|
+
| `delayClose` | `number` | `150` | Delay before closing on mouse leave (ms) |
|
|
74
|
+
| `onValueChange` | `(value: string \| null) => void` | `undefined` | Callback when active item changes |
|
|
75
|
+
|
|
76
|
+
### Controller
|
|
77
|
+
|
|
78
|
+
| Method/Property | Description |
|
|
79
|
+
|-----------------|-------------|
|
|
80
|
+
| `open(value)` | Open a specific item |
|
|
81
|
+
| `close()` | Close the menu |
|
|
82
|
+
| `value` | Currently active item value (readonly `string \| null`) |
|
|
83
|
+
| `destroy()` | Cleanup all event listeners |
|
|
84
|
+
|
|
85
|
+
## Markup Structure
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<nav data-slot="navigation-menu">
|
|
89
|
+
<ul data-slot="navigation-menu-list">
|
|
90
|
+
<li data-slot="navigation-menu-item" data-value="unique-id">
|
|
91
|
+
<button data-slot="navigation-menu-trigger">Label</button>
|
|
92
|
+
<div data-slot="navigation-menu-content">
|
|
93
|
+
<!-- Links, content -->
|
|
94
|
+
</div>
|
|
95
|
+
</li>
|
|
96
|
+
<!-- Optional hover indicator -->
|
|
97
|
+
<div data-slot="navigation-menu-indicator"></div>
|
|
98
|
+
</ul>
|
|
99
|
+
<!-- Optional viewport for animated content switching -->
|
|
100
|
+
<div data-slot="navigation-menu-viewport"></div>
|
|
101
|
+
</nav>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Optional Slots
|
|
105
|
+
|
|
106
|
+
- `navigation-menu-indicator` - Animated highlight that follows the hovered trigger
|
|
107
|
+
- `navigation-menu-viewport` - Container for content with size transitions
|
|
108
|
+
|
|
109
|
+
## Styling
|
|
110
|
+
|
|
111
|
+
### Basic Styling
|
|
112
|
+
|
|
113
|
+
```css
|
|
114
|
+
/* Hidden by default */
|
|
115
|
+
[data-slot="navigation-menu-content"] {
|
|
116
|
+
display: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
[data-slot="navigation-menu-content"][data-state="active"] {
|
|
120
|
+
display: block;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Viewport sizing */
|
|
124
|
+
[data-slot="navigation-menu-viewport"] {
|
|
125
|
+
width: var(--viewport-width);
|
|
126
|
+
height: var(--viewport-height);
|
|
127
|
+
transition: width 0.3s, height 0.3s;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Skip animation on initial open */
|
|
131
|
+
[data-slot="navigation-menu-viewport"][data-instant] {
|
|
132
|
+
transition: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Indicator positioning */
|
|
136
|
+
[data-slot="navigation-menu-indicator"] {
|
|
137
|
+
position: absolute;
|
|
138
|
+
left: var(--indicator-left);
|
|
139
|
+
width: var(--indicator-width);
|
|
140
|
+
transition: left 0.2s, width 0.2s;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Motion Animations
|
|
145
|
+
|
|
146
|
+
Content panels receive `data-motion` attributes for enter/exit animations:
|
|
147
|
+
|
|
148
|
+
```css
|
|
149
|
+
/* Entering from right */
|
|
150
|
+
[data-slot="navigation-menu-content"][data-motion="from-right"] {
|
|
151
|
+
animation: slideFromRight 0.2s;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Exiting to left */
|
|
155
|
+
[data-slot="navigation-menu-content"][data-motion="to-left"] {
|
|
156
|
+
animation: slideToLeft 0.2s;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@keyframes slideFromRight {
|
|
160
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
161
|
+
to { transform: translateX(0); opacity: 1; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@keyframes slideToLeft {
|
|
165
|
+
from { transform: translateX(0); opacity: 1; }
|
|
166
|
+
to { transform: translateX(-100%); opacity: 0; }
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### CSS Variables
|
|
171
|
+
|
|
172
|
+
| Variable | Element | Description |
|
|
173
|
+
|----------|---------|-------------|
|
|
174
|
+
| `--viewport-width` | viewport | Width of active content |
|
|
175
|
+
| `--viewport-height` | viewport | Height of active content |
|
|
176
|
+
| `--indicator-left` | indicator | Left offset from list |
|
|
177
|
+
| `--indicator-width` | indicator | Width of hovered trigger |
|
|
178
|
+
| `--indicator-top` | indicator | Top offset from list |
|
|
179
|
+
| `--indicator-height` | indicator | Height of hovered trigger |
|
|
180
|
+
| `--motion-direction` | viewport | `1` (right) or `-1` (left) |
|
|
181
|
+
|
|
182
|
+
## Keyboard Navigation
|
|
183
|
+
|
|
184
|
+
### Within Trigger List
|
|
185
|
+
|
|
186
|
+
| Key | Action |
|
|
187
|
+
|-----|--------|
|
|
188
|
+
| `ArrowLeft` | Move focus to previous trigger |
|
|
189
|
+
| `ArrowRight` | Move focus to next trigger |
|
|
190
|
+
| `ArrowDown` | Move focus into content panel |
|
|
191
|
+
| `Home` | Move focus to first trigger |
|
|
192
|
+
| `End` | Move focus to last trigger |
|
|
193
|
+
| `Escape` | Close menu |
|
|
194
|
+
|
|
195
|
+
### Within Content Panel
|
|
196
|
+
|
|
197
|
+
| Key | Action |
|
|
198
|
+
|-----|--------|
|
|
199
|
+
| `ArrowDown` / `ArrowRight` | Move to next focusable element |
|
|
200
|
+
| `ArrowUp` / `ArrowLeft` | Move to previous element (returns to trigger at start) |
|
|
201
|
+
| `Escape` | Close menu and return focus to trigger |
|
|
202
|
+
|
|
203
|
+
## Behavior
|
|
204
|
+
|
|
205
|
+
- **Hover**: Opens after `delayOpen` ms, closes after `delayClose` ms
|
|
206
|
+
- **Click**: Locks menu open until clicking outside or same trigger
|
|
207
|
+
- **Focus**: Opens immediately on keyboard focus
|
|
208
|
+
- **Switching**: Instant transition between items (no delay)
|
|
209
|
+
|
|
210
|
+
## Events
|
|
211
|
+
|
|
212
|
+
Listen for changes via custom events:
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
element.addEventListener("navigation-menu:change", (e) => {
|
|
216
|
+
console.log("Active item:", e.detail.value);
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
|
223
|
+
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`@data-slot/core`));function l(e,t={}){let{delayOpen:n=200,delayClose:r=150,openOnFocus:i=!0,onValueChange:a}=t,o=e=>e.replace(/[^a-z0-9\-_:.]/gi,`-`),s=(e,t)=>{`inert`in e&&(e.inert=t)},l=(0,c.getPart)(e,`navigation-menu-list`),u=(0,c.getParts)(e,`navigation-menu-item`),d=(0,c.getPart)(e,`navigation-menu-viewport`),f=(0,c.getPart)(e,`navigation-menu-indicator`);if(!l||u.length===0)throw Error(`NavigationMenu requires navigation-menu-list and at least one navigation-menu-item`);let p=null,m=null,h=-1,g=null,_=null,v=null,y=!1,b=!1,x=!1,S=[],C=null,w=e=>{C?.disconnect(),C=null,!(!d||!e)&&(C=new ResizeObserver(()=>P(e)),C.observe(e))};S.push(()=>C?.disconnect());let T=new Map,E=0;u.forEach(e=>{let t=e.dataset.value;if(!t)return;let n=(0,c.getPart)(e,`navigation-menu-trigger`),r=(0,c.getPart)(e,`navigation-menu-content`);if(n&&r){T.set(t,{item:e,trigger:n,content:r,index:E++});let i=o(t),a=(0,c.ensureId)(n,`nav-menu-trigger-${i}`),s=(0,c.ensureId)(r,`nav-menu-content-${i}`);n.setAttribute(`aria-haspopup`,`true`),n.setAttribute(`aria-controls`,s),r.setAttribute(`role`,`region`),r.setAttribute(`aria-labelledby`,a)}});let D=Array.from(T.values()).map(e=>e.trigger),O=new Map;for(let[e,t]of T)O.set(t.trigger,e);let k=`a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])`,A=e=>Array.from(e.querySelectorAll(k)).filter(e=>!e.hidden&&!e.closest(`[hidden]`)),j=()=>{g&&(clearTimeout(g),g=null),_&&(clearTimeout(_),_=null)},M=null,N=()=>(M||(M=document.createElement(`div`),M.setAttribute(`data-slot`,`navigation-menu-bridge`),M.style.cssText=`position: absolute; left: 0; right: 0; top: 0; pointer-events: auto; z-index: -1;`,d&&d.insertBefore(M,d.firstChild),S.push((0,c.on)(M,`pointerenter`,()=>{j()}))),M),P=e=>{d&&requestAnimationFrame(()=>{let t=e.getBoundingClientRect(),n=getComputedStyle(e),r=parseFloat(n.marginTop)||0,i=getComputedStyle(d),a=parseFloat(i.marginTop)||0,o=parseFloat(i.marginBottom)||0;d.style.setProperty(`--viewport-width`,`${t.width}px`),d.style.setProperty(`--viewport-height`,`${t.height+a+o}px`);let s=r+a;if(s>0){let e=N();e.style.height=`${s}px`,e.style.transform=`translateY(-${s}px)`}else M&&(M.style.height=`0`)})},F=e=>h===-1||e>h?`right`:`left`,I=e=>{if(!f)return;if(v=e,!e){f.setAttribute(`data-state`,`hidden`);return}let t=l.getBoundingClientRect(),n=e.getBoundingClientRect();f.style.setProperty(`--indicator-left`,`${n.left-t.left}px`),f.style.setProperty(`--indicator-width`,`${n.width}px`),f.style.setProperty(`--indicator-top`,`${n.top-t.top}px`),f.style.setProperty(`--indicator-height`,`${n.height}px`),f.setAttribute(`data-state`,`visible`)},L=(t,i=!1)=>{if(t===p){j();return}if(t!==null&&t===m){j();return}j(),m=t===null?null:t;let o=()=>{let n=p,r=t?T.get(t):null,i=n!==null&&t!==null&&n!==t,o=i&&r?F(r.index):null,l=document.activeElement;if(t===null&&l&&e.contains(l)){let e=n?T.get(n)?.trigger:null;e&&e.focus()}if(T.forEach(({trigger:e,content:r,item:i},a)=>{let l=a===t,u=a===n;if((0,c.setAria)(e,`expanded`,l),e.setAttribute(`data-state`,l?`open`:`closed`),l||u&&t===null?e.tabIndex=0:e.tabIndex=-1,i.setAttribute(`data-state`,l?`open`:`closed`),!l)if(r.setAttribute(`data-state`,`inactive`),r.setAttribute(`aria-hidden`,`true`),s(r,!0),r.hidden=!0,u&&o){let e=o===`right`?`to-left`:`to-right`;r.setAttribute(`data-motion`,e)}else r.removeAttribute(`data-motion`)}),r){if(o){let e=o===`right`?`from-right`:`from-left`;r.content.setAttribute(`data-motion`,e)}else r.content.removeAttribute(`data-motion`);r.content.setAttribute(`data-state`,`active`),r.content.removeAttribute(`aria-hidden`),s(r.content,!1),r.content.hidden=!1,h=r.index,P(r.content),w(r.content),I(r.trigger)}else w(null);let u=t!==null;e.setAttribute(`data-state`,u?`open`:`closed`),o?e.setAttribute(`data-motion`,o===`right`?`from-right`:`from-left`):e.removeAttribute(`data-motion`),d&&(d.setAttribute(`data-state`,u?`open`:`closed`),u&&!i?d.setAttribute(`data-instant`,``):i&&d.removeAttribute(`data-instant`),o&&d.style.setProperty(`--motion-direction`,o===`right`?`1`:`-1`)),p=t,m=null,t===null&&I(null),(0,c.emit)(e,`navigation-menu:change`,{value:t}),a?.(t)};i?o():t!==null&&p===null?g=setTimeout(o,n):t!==null&&p!==null?o():_=setTimeout(o,r)};e.setAttribute(`data-state`,`closed`),d&&d.setAttribute(`data-state`,`closed`),f&&f.setAttribute(`data-state`,`hidden`),T.forEach(({trigger:e,content:t,item:n})=>{e.tagName===`BUTTON`&&!e.hasAttribute(`type`)&&(e.type=`button`),(0,c.setAria)(e,`expanded`,!1),e.setAttribute(`data-state`,`closed`),e.tabIndex=e===D[0]?0:-1,n.setAttribute(`data-state`,`closed`),t.setAttribute(`data-state`,`inactive`),t.setAttribute(`aria-hidden`,`true`),t.tabIndex=-1,s(t,!0),t.hidden=!0}),T.forEach(({item:e,trigger:t},n)=>{S.push((0,c.on)(t,`pointerenter`,()=>{y||I(t)})),S.push((0,c.on)(e,`pointerenter`,()=>{y||L(n)})),S.push((0,c.on)(e,`pointerleave`,()=>{m===n&&p===null&&(j(),m=null)})),S.push((0,c.on)(t,`focus`,()=>{b||(i&&L(n,!0),I(t))})),S.push((0,c.on)(t,`pointerdown`,()=>{b=!0})),S.push((0,c.on)(t,`click`,()=>{j(),p===n&&y?(y=!1,L(null,!0),I(null)):p===n&&!y?(y=!0,I(t)):(y=!0,L(n,!0),I(t)),b=!1}))}),S.push((0,c.on)(e,`pointerenter`,()=>{x=!0}),(0,c.on)(e,`pointerleave`,()=>{x=!1,y||(L(null),I(null))}),(0,c.on)(e,`pointerdown`,j)),d&&S.push((0,c.on)(d,`pointerenter`,()=>{j()}),(0,c.on)(d,`transitionend`,e=>{if(e.target!==d)return;let t=p?T.get(p):null;t&&P(t.content)})),T.forEach(({content:e})=>{S.push((0,c.on)(e,`pointerenter`,()=>{j()}))}),S.push((0,c.on)(l,`keydown`,e=>{let t=e.target,n=D.indexOf(t);if(n===-1)return;let r=O.get(t)??null,i=n;switch(e.key){case`ArrowLeft`:i=n-1,i<0&&(i=D.length-1);break;case`ArrowRight`:i=n+1,i>=D.length&&(i=0);break;case`ArrowDown`:e.preventDefault(),r&&(y=!0,L(r,!0),requestAnimationFrame(()=>{let e=T.get(r);if(!e)return;let t=A(e.content),n=t[0];n?n.focus():e.content.focus()}));return;case`Home`:i=0;break;case`End`:i=D.length-1;break;case`Escape`:y=!1,L(null,!0),I(null);return;default:return}e.preventDefault();let a=D[i];a&&(D.forEach(e=>e.tabIndex=e===a?0:-1),a.focus(),I(a))})),T.forEach(({content:e,trigger:t})=>{S.push((0,c.on)(e,`keydown`,n=>{let r=n.target,i=A(e),a=i.indexOf(r);if(a!==-1)switch(n.key){case`ArrowDown`:case`ArrowRight`:{n.preventDefault();let e=a+1;e<i.length&&i[e]?.focus();break}case`ArrowUp`:case`ArrowLeft`:n.preventDefault(),a===0?t.focus():i[a-1]?.focus();break;case`Escape`:n.preventDefault(),y=!1,L(null,!0),I(null),t.focus();break}}))});let R=()=>e.contains(document.activeElement)||x||y;S.push((0,c.on)(document,`pointerup`,()=>{b=!1},{capture:!0}),(0,c.on)(document,`pointercancel`,()=>{b=!1},{capture:!0})),S.push((0,c.on)(document,`focusin`,t=>{p!==null&&(e.contains(t.target)||(y=!1,L(null,!0),I(null)))})),S.push((0,c.on)(document,`pointerdown`,t=>{p!==null&&R()&&(e.contains(t.target)||(y=!1,L(null,!0),I(null)))})),S.push((0,c.on)(document,`keydown`,e=>{e.key!==`Escape`||p===null||R()&&(y=!1,L(null,!0),I(null))})),S.push((0,c.on)(window,`resize`,()=>{v&&requestAnimationFrame(()=>I(v))}),(0,c.on)(l,`scroll`,()=>{v&&requestAnimationFrame(()=>I(v))}));let z={get value(){return p},open:e=>L(e,!0),close:()=>L(null,!0),destroy:()=>{j(),S.forEach(e=>e()),S.length=0}};return z}const u=new WeakSet;function d(e=document){let t=[];for(let n of(0,c.getRoots)(e,`navigation-menu`)){if(u.has(n))continue;u.add(n),t.push(l(n))}return t}exports.create=d,exports.createNavigationMenu=l;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface NavigationMenuOptions {
|
|
3
|
+
/** Delay before opening on hover (ms) */
|
|
4
|
+
delayOpen?: number;
|
|
5
|
+
/** Delay before closing on mouse leave (ms) */
|
|
6
|
+
delayClose?: number;
|
|
7
|
+
/** Whether focusing a trigger opens its content (default: true) */
|
|
8
|
+
openOnFocus?: boolean;
|
|
9
|
+
/** Callback when active item changes */
|
|
10
|
+
onValueChange?: (value: string | null) => void;
|
|
11
|
+
}
|
|
12
|
+
interface NavigationMenuController {
|
|
13
|
+
/** Currently active item value */
|
|
14
|
+
readonly value: string | null;
|
|
15
|
+
/** Open a specific item */
|
|
16
|
+
open(value: string): void;
|
|
17
|
+
/** Close the menu */
|
|
18
|
+
close(): void;
|
|
19
|
+
/** Cleanup all event listeners */
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a navigation menu controller for a root element
|
|
24
|
+
*
|
|
25
|
+
* Expected markup:
|
|
26
|
+
* ```html
|
|
27
|
+
* <nav data-slot="navigation-menu">
|
|
28
|
+
* <ul data-slot="navigation-menu-list">
|
|
29
|
+
* <li data-slot="navigation-menu-item" data-value="products">
|
|
30
|
+
* <button data-slot="navigation-menu-trigger">Products</button>
|
|
31
|
+
* <div data-slot="navigation-menu-content">...</div>
|
|
32
|
+
* </li>
|
|
33
|
+
* <!-- Optional hover indicator -->
|
|
34
|
+
* <div data-slot="navigation-menu-indicator"></div>
|
|
35
|
+
* </ul>
|
|
36
|
+
* <div data-slot="navigation-menu-viewport"></div>
|
|
37
|
+
* </nav>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function createNavigationMenu(root: Element, options?: NavigationMenuOptions): NavigationMenuController;
|
|
41
|
+
/**
|
|
42
|
+
* Find and bind all navigation menu components in a scope
|
|
43
|
+
* Returns array of controllers for programmatic access
|
|
44
|
+
*/
|
|
45
|
+
declare function create(scope?: ParentNode): NavigationMenuController[];
|
|
46
|
+
//#endregion
|
|
47
|
+
export { NavigationMenuController, NavigationMenuOptions, create, createNavigationMenu };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
interface NavigationMenuOptions {
|
|
3
|
+
/** Delay before opening on hover (ms) */
|
|
4
|
+
delayOpen?: number;
|
|
5
|
+
/** Delay before closing on mouse leave (ms) */
|
|
6
|
+
delayClose?: number;
|
|
7
|
+
/** Whether focusing a trigger opens its content (default: true) */
|
|
8
|
+
openOnFocus?: boolean;
|
|
9
|
+
/** Callback when active item changes */
|
|
10
|
+
onValueChange?: (value: string | null) => void;
|
|
11
|
+
}
|
|
12
|
+
interface NavigationMenuController {
|
|
13
|
+
/** Currently active item value */
|
|
14
|
+
readonly value: string | null;
|
|
15
|
+
/** Open a specific item */
|
|
16
|
+
open(value: string): void;
|
|
17
|
+
/** Close the menu */
|
|
18
|
+
close(): void;
|
|
19
|
+
/** Cleanup all event listeners */
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a navigation menu controller for a root element
|
|
24
|
+
*
|
|
25
|
+
* Expected markup:
|
|
26
|
+
* ```html
|
|
27
|
+
* <nav data-slot="navigation-menu">
|
|
28
|
+
* <ul data-slot="navigation-menu-list">
|
|
29
|
+
* <li data-slot="navigation-menu-item" data-value="products">
|
|
30
|
+
* <button data-slot="navigation-menu-trigger">Products</button>
|
|
31
|
+
* <div data-slot="navigation-menu-content">...</div>
|
|
32
|
+
* </li>
|
|
33
|
+
* <!-- Optional hover indicator -->
|
|
34
|
+
* <div data-slot="navigation-menu-indicator"></div>
|
|
35
|
+
* </ul>
|
|
36
|
+
* <div data-slot="navigation-menu-viewport"></div>
|
|
37
|
+
* </nav>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
declare function createNavigationMenu(root: Element, options?: NavigationMenuOptions): NavigationMenuController;
|
|
41
|
+
/**
|
|
42
|
+
* Find and bind all navigation menu components in a scope
|
|
43
|
+
* Returns array of controllers for programmatic access
|
|
44
|
+
*/
|
|
45
|
+
declare function create(scope?: ParentNode): NavigationMenuController[];
|
|
46
|
+
//#endregion
|
|
47
|
+
export { NavigationMenuController, NavigationMenuOptions, create, createNavigationMenu };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{emit as e,ensureId as t,getPart as n,getParts as r,getRoots as i,on as a,setAria as o}from"@data-slot/core";function s(i,s={}){let{delayOpen:c=200,delayClose:l=150,openOnFocus:u=!0,onValueChange:d}=s,f=e=>e.replace(/[^a-z0-9\-_:.]/gi,`-`),p=(e,t)=>{`inert`in e&&(e.inert=t)},m=n(i,`navigation-menu-list`),h=r(i,`navigation-menu-item`),g=n(i,`navigation-menu-viewport`),_=n(i,`navigation-menu-indicator`);if(!m||h.length===0)throw Error(`NavigationMenu requires navigation-menu-list and at least one navigation-menu-item`);let v=null,y=null,b=-1,x=null,S=null,C=null,w=!1,T=!1,E=!1,D=[],O=null,k=e=>{O?.disconnect(),O=null,!(!g||!e)&&(O=new ResizeObserver(()=>z(e)),O.observe(e))};D.push(()=>O?.disconnect());let A=new Map,j=0;h.forEach(e=>{let r=e.dataset.value;if(!r)return;let i=n(e,`navigation-menu-trigger`),a=n(e,`navigation-menu-content`);if(i&&a){A.set(r,{item:e,trigger:i,content:a,index:j++});let n=f(r),o=t(i,`nav-menu-trigger-${n}`),s=t(a,`nav-menu-content-${n}`);i.setAttribute(`aria-haspopup`,`true`),i.setAttribute(`aria-controls`,s),a.setAttribute(`role`,`region`),a.setAttribute(`aria-labelledby`,o)}});let M=Array.from(A.values()).map(e=>e.trigger),N=new Map;for(let[e,t]of A)N.set(t.trigger,e);let P=`a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])`,F=e=>Array.from(e.querySelectorAll(P)).filter(e=>!e.hidden&&!e.closest(`[hidden]`)),I=()=>{x&&(clearTimeout(x),x=null),S&&(clearTimeout(S),S=null)},L=null,R=()=>(L||(L=document.createElement(`div`),L.setAttribute(`data-slot`,`navigation-menu-bridge`),L.style.cssText=`position: absolute; left: 0; right: 0; top: 0; pointer-events: auto; z-index: -1;`,g&&g.insertBefore(L,g.firstChild),D.push(a(L,`pointerenter`,()=>{I()}))),L),z=e=>{g&&requestAnimationFrame(()=>{let t=e.getBoundingClientRect(),n=getComputedStyle(e),r=parseFloat(n.marginTop)||0,i=getComputedStyle(g),a=parseFloat(i.marginTop)||0,o=parseFloat(i.marginBottom)||0;g.style.setProperty(`--viewport-width`,`${t.width}px`),g.style.setProperty(`--viewport-height`,`${t.height+a+o}px`);let s=r+a;if(s>0){let e=R();e.style.height=`${s}px`,e.style.transform=`translateY(-${s}px)`}else L&&(L.style.height=`0`)})},B=e=>b===-1||e>b?`right`:`left`,V=e=>{if(!_)return;if(C=e,!e){_.setAttribute(`data-state`,`hidden`);return}let t=m.getBoundingClientRect(),n=e.getBoundingClientRect();_.style.setProperty(`--indicator-left`,`${n.left-t.left}px`),_.style.setProperty(`--indicator-width`,`${n.width}px`),_.style.setProperty(`--indicator-top`,`${n.top-t.top}px`),_.style.setProperty(`--indicator-height`,`${n.height}px`),_.setAttribute(`data-state`,`visible`)},H=(t,n=!1)=>{if(t===v){I();return}if(t!==null&&t===y){I();return}I(),y=t===null?null:t;let r=()=>{let n=v,r=t?A.get(t):null,a=n!==null&&t!==null&&n!==t,s=a&&r?B(r.index):null,c=document.activeElement;if(t===null&&c&&i.contains(c)){let e=n?A.get(n)?.trigger:null;e&&e.focus()}if(A.forEach(({trigger:e,content:r,item:i},a)=>{let c=a===t,l=a===n;if(o(e,`expanded`,c),e.setAttribute(`data-state`,c?`open`:`closed`),c||l&&t===null?e.tabIndex=0:e.tabIndex=-1,i.setAttribute(`data-state`,c?`open`:`closed`),!c)if(r.setAttribute(`data-state`,`inactive`),r.setAttribute(`aria-hidden`,`true`),p(r,!0),r.hidden=!0,l&&s){let e=s===`right`?`to-left`:`to-right`;r.setAttribute(`data-motion`,e)}else r.removeAttribute(`data-motion`)}),r){if(s){let e=s===`right`?`from-right`:`from-left`;r.content.setAttribute(`data-motion`,e)}else r.content.removeAttribute(`data-motion`);r.content.setAttribute(`data-state`,`active`),r.content.removeAttribute(`aria-hidden`),p(r.content,!1),r.content.hidden=!1,b=r.index,z(r.content),k(r.content),V(r.trigger)}else k(null);let l=t!==null;i.setAttribute(`data-state`,l?`open`:`closed`),s?i.setAttribute(`data-motion`,s===`right`?`from-right`:`from-left`):i.removeAttribute(`data-motion`),g&&(g.setAttribute(`data-state`,l?`open`:`closed`),l&&!a?g.setAttribute(`data-instant`,``):a&&g.removeAttribute(`data-instant`),s&&g.style.setProperty(`--motion-direction`,s===`right`?`1`:`-1`)),v=t,y=null,t===null&&V(null),e(i,`navigation-menu:change`,{value:t}),d?.(t)};n?r():t!==null&&v===null?x=setTimeout(r,c):t!==null&&v!==null?r():S=setTimeout(r,l)};i.setAttribute(`data-state`,`closed`),g&&g.setAttribute(`data-state`,`closed`),_&&_.setAttribute(`data-state`,`hidden`),A.forEach(({trigger:e,content:t,item:n})=>{e.tagName===`BUTTON`&&!e.hasAttribute(`type`)&&(e.type=`button`),o(e,`expanded`,!1),e.setAttribute(`data-state`,`closed`),e.tabIndex=e===M[0]?0:-1,n.setAttribute(`data-state`,`closed`),t.setAttribute(`data-state`,`inactive`),t.setAttribute(`aria-hidden`,`true`),t.tabIndex=-1,p(t,!0),t.hidden=!0}),A.forEach(({item:e,trigger:t},n)=>{D.push(a(t,`pointerenter`,()=>{w||V(t)})),D.push(a(e,`pointerenter`,()=>{w||H(n)})),D.push(a(e,`pointerleave`,()=>{y===n&&v===null&&(I(),y=null)})),D.push(a(t,`focus`,()=>{T||(u&&H(n,!0),V(t))})),D.push(a(t,`pointerdown`,()=>{T=!0})),D.push(a(t,`click`,()=>{I(),v===n&&w?(w=!1,H(null,!0),V(null)):v===n&&!w?(w=!0,V(t)):(w=!0,H(n,!0),V(t)),T=!1}))}),D.push(a(i,`pointerenter`,()=>{E=!0}),a(i,`pointerleave`,()=>{E=!1,w||(H(null),V(null))}),a(i,`pointerdown`,I)),g&&D.push(a(g,`pointerenter`,()=>{I()}),a(g,`transitionend`,e=>{if(e.target!==g)return;let t=v?A.get(v):null;t&&z(t.content)})),A.forEach(({content:e})=>{D.push(a(e,`pointerenter`,()=>{I()}))}),D.push(a(m,`keydown`,e=>{let t=e.target,n=M.indexOf(t);if(n===-1)return;let r=N.get(t)??null,i=n;switch(e.key){case`ArrowLeft`:i=n-1,i<0&&(i=M.length-1);break;case`ArrowRight`:i=n+1,i>=M.length&&(i=0);break;case`ArrowDown`:e.preventDefault(),r&&(w=!0,H(r,!0),requestAnimationFrame(()=>{let e=A.get(r);if(!e)return;let t=F(e.content),n=t[0];n?n.focus():e.content.focus()}));return;case`Home`:i=0;break;case`End`:i=M.length-1;break;case`Escape`:w=!1,H(null,!0),V(null);return;default:return}e.preventDefault();let a=M[i];a&&(M.forEach(e=>e.tabIndex=e===a?0:-1),a.focus(),V(a))})),A.forEach(({content:e,trigger:t})=>{D.push(a(e,`keydown`,n=>{let r=n.target,i=F(e),a=i.indexOf(r);if(a!==-1)switch(n.key){case`ArrowDown`:case`ArrowRight`:{n.preventDefault();let e=a+1;e<i.length&&i[e]?.focus();break}case`ArrowUp`:case`ArrowLeft`:n.preventDefault(),a===0?t.focus():i[a-1]?.focus();break;case`Escape`:n.preventDefault(),w=!1,H(null,!0),V(null),t.focus();break}}))});let U=()=>i.contains(document.activeElement)||E||w;D.push(a(document,`pointerup`,()=>{T=!1},{capture:!0}),a(document,`pointercancel`,()=>{T=!1},{capture:!0})),D.push(a(document,`focusin`,e=>{v!==null&&(i.contains(e.target)||(w=!1,H(null,!0),V(null)))})),D.push(a(document,`pointerdown`,e=>{v!==null&&U()&&(i.contains(e.target)||(w=!1,H(null,!0),V(null)))})),D.push(a(document,`keydown`,e=>{e.key!==`Escape`||v===null||U()&&(w=!1,H(null,!0),V(null))})),D.push(a(window,`resize`,()=>{C&&requestAnimationFrame(()=>V(C))}),a(m,`scroll`,()=>{C&&requestAnimationFrame(()=>V(C))}));let W={get value(){return v},open:e=>H(e,!0),close:()=>H(null,!0),destroy:()=>{I(),D.forEach(e=>e()),D.length=0}};return W}const c=new WeakSet;function l(e=document){let t=[];for(let n of i(e,`navigation-menu`)){if(c.has(n))continue;c.add(n),t.push(s(n))}return t}export{l as create,s as createNavigationMenu};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@data-slot/navigation-menu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsdown"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@data-slot/core": "workspace:*"
|
|
27
|
+
},
|
|
28
|
+
"keywords": ["headless", "ui", "navigation-menu", "mega-menu", "vanilla", "data-slot"],
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|