@sc4rfurryx/proteusjs 1.0.0 → 1.1.1
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/LICENSE +1 -1
- package/README.md +331 -77
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/react.d.ts +140 -0
- package/dist/adapters/react.esm.js +849 -0
- package/dist/adapters/react.esm.js.map +1 -0
- package/dist/adapters/svelte.d.ts +181 -0
- package/dist/adapters/svelte.esm.js +909 -0
- package/dist/adapters/svelte.esm.js.map +1 -0
- package/dist/adapters/vue.d.ts +205 -0
- package/dist/adapters/vue.esm.js +873 -0
- package/dist/adapters/vue.esm.js.map +1 -0
- package/dist/modules/a11y-audit.d.ts +31 -0
- package/dist/modules/a11y-audit.esm.js +64 -0
- package/dist/modules/a11y-audit.esm.js.map +1 -0
- package/dist/modules/a11y-primitives.d.ts +36 -0
- package/dist/modules/a11y-primitives.esm.js +114 -0
- package/dist/modules/a11y-primitives.esm.js.map +1 -0
- package/dist/modules/anchor.d.ts +30 -0
- package/dist/modules/anchor.esm.js +219 -0
- package/dist/modules/anchor.esm.js.map +1 -0
- package/dist/modules/container.d.ts +60 -0
- package/dist/modules/container.esm.js +194 -0
- package/dist/modules/container.esm.js.map +1 -0
- package/dist/modules/perf.d.ts +82 -0
- package/dist/modules/perf.esm.js +257 -0
- package/dist/modules/perf.esm.js.map +1 -0
- package/dist/modules/popover.d.ts +33 -0
- package/dist/modules/popover.esm.js +191 -0
- package/dist/modules/popover.esm.js.map +1 -0
- package/dist/modules/scroll.d.ts +43 -0
- package/dist/modules/scroll.esm.js +195 -0
- package/dist/modules/scroll.esm.js.map +1 -0
- package/dist/modules/transitions.d.ts +35 -0
- package/dist/modules/transitions.esm.js +120 -0
- package/dist/modules/transitions.esm.js.map +1 -0
- package/dist/modules/typography.d.ts +72 -0
- package/dist/modules/typography.esm.js +168 -0
- package/dist/modules/typography.esm.js.map +1 -0
- package/dist/proteus.cjs.js +1554 -12
- package/dist/proteus.cjs.js.map +1 -1
- package/dist/proteus.d.ts +516 -12
- package/dist/proteus.esm.js +1545 -12
- package/dist/proteus.esm.js.map +1 -1
- package/dist/proteus.esm.min.js +3 -3
- package/dist/proteus.esm.min.js.map +1 -1
- package/dist/proteus.js +1554 -12
- package/dist/proteus.js.map +1 -1
- package/dist/proteus.min.js +3 -3
- package/dist/proteus.min.js.map +1 -1
- package/package.json +69 -7
- package/src/adapters/react.ts +264 -0
- package/src/adapters/svelte.ts +321 -0
- package/src/adapters/vue.ts +268 -0
- package/src/index.ts +33 -6
- package/src/modules/a11y-audit/index.ts +84 -0
- package/src/modules/a11y-primitives/index.ts +152 -0
- package/src/modules/anchor/index.ts +259 -0
- package/src/modules/container/index.ts +230 -0
- package/src/modules/perf/index.ts +291 -0
- package/src/modules/popover/index.ts +238 -0
- package/src/modules/scroll/index.ts +251 -0
- package/src/modules/transitions/index.ts +145 -0
- package/src/modules/typography/index.ts +239 -0
- package/src/utils/version.ts +1 -1
@@ -0,0 +1,268 @@
|
|
1
|
+
/**
|
2
|
+
* @sc4rfurryx/proteusjs/adapters/vue
|
3
|
+
* Vue composables and directives for ProteusJS
|
4
|
+
*
|
5
|
+
* @version 1.1.0
|
6
|
+
* @author sc4rfurry
|
7
|
+
* @license MIT
|
8
|
+
*/
|
9
|
+
|
10
|
+
import { ref, onMounted, onUnmounted, Ref } from 'vue';
|
11
|
+
import { transition, TransitionOptions } from '../modules/transitions';
|
12
|
+
import { scrollAnimate, ScrollAnimateOptions } from '../modules/scroll';
|
13
|
+
import { attach as attachPopover, PopoverOptions, PopoverController } from '../modules/popover';
|
14
|
+
import { tether, TetherOptions, TetherController } from '../modules/anchor';
|
15
|
+
import { defineContainer, ContainerOptions } from '../modules/container';
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Composable for view transitions
|
19
|
+
*/
|
20
|
+
export function useTransition() {
|
21
|
+
return {
|
22
|
+
transition: async (run: () => Promise<any> | any, opts?: TransitionOptions) => {
|
23
|
+
return transition(run, opts);
|
24
|
+
}
|
25
|
+
};
|
26
|
+
}
|
27
|
+
|
28
|
+
/**
|
29
|
+
* Composable for scroll animations
|
30
|
+
*/
|
31
|
+
export function useScrollAnimate(
|
32
|
+
elementRef: Ref<HTMLElement | undefined>,
|
33
|
+
opts: ScrollAnimateOptions
|
34
|
+
) {
|
35
|
+
onMounted(() => {
|
36
|
+
if (elementRef.value) {
|
37
|
+
scrollAnimate(elementRef.value, opts);
|
38
|
+
}
|
39
|
+
});
|
40
|
+
|
41
|
+
return {
|
42
|
+
elementRef
|
43
|
+
};
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Composable for popover functionality
|
48
|
+
*/
|
49
|
+
export function usePopover(
|
50
|
+
triggerRef: Ref<HTMLElement | undefined>,
|
51
|
+
panelRef: Ref<HTMLElement | undefined>,
|
52
|
+
opts?: PopoverOptions
|
53
|
+
) {
|
54
|
+
const controller = ref<PopoverController | null>(null);
|
55
|
+
const isOpen = ref(false);
|
56
|
+
|
57
|
+
onMounted(() => {
|
58
|
+
if (triggerRef.value && panelRef.value) {
|
59
|
+
controller.value = attachPopover(triggerRef.value, panelRef.value, {
|
60
|
+
...opts,
|
61
|
+
onOpen: () => {
|
62
|
+
isOpen.value = true;
|
63
|
+
opts?.onOpen?.();
|
64
|
+
},
|
65
|
+
onClose: () => {
|
66
|
+
isOpen.value = false;
|
67
|
+
opts?.onClose?.();
|
68
|
+
}
|
69
|
+
});
|
70
|
+
}
|
71
|
+
});
|
72
|
+
|
73
|
+
onUnmounted(() => {
|
74
|
+
if (controller.value) {
|
75
|
+
controller.value.destroy();
|
76
|
+
}
|
77
|
+
});
|
78
|
+
|
79
|
+
const open = () => controller.value?.open();
|
80
|
+
const close = () => controller.value?.close();
|
81
|
+
const toggle = () => controller.value?.toggle();
|
82
|
+
|
83
|
+
return {
|
84
|
+
isOpen,
|
85
|
+
open,
|
86
|
+
close,
|
87
|
+
toggle
|
88
|
+
};
|
89
|
+
}
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Composable for anchor positioning
|
93
|
+
*/
|
94
|
+
export function useAnchor(
|
95
|
+
floatingRef: Ref<HTMLElement | undefined>,
|
96
|
+
anchorRef: Ref<HTMLElement | undefined>,
|
97
|
+
opts?: Omit<TetherOptions, 'anchor'>
|
98
|
+
) {
|
99
|
+
const controller = ref<TetherController | null>(null);
|
100
|
+
|
101
|
+
onMounted(() => {
|
102
|
+
if (floatingRef.value && anchorRef.value) {
|
103
|
+
controller.value = tether(floatingRef.value, {
|
104
|
+
anchor: anchorRef.value,
|
105
|
+
...opts
|
106
|
+
});
|
107
|
+
}
|
108
|
+
});
|
109
|
+
|
110
|
+
onUnmounted(() => {
|
111
|
+
if (controller.value) {
|
112
|
+
controller.value.destroy();
|
113
|
+
}
|
114
|
+
});
|
115
|
+
|
116
|
+
return {
|
117
|
+
controller
|
118
|
+
};
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Composable for container queries
|
123
|
+
*/
|
124
|
+
export function useContainer(
|
125
|
+
elementRef: Ref<HTMLElement | undefined>,
|
126
|
+
name?: string,
|
127
|
+
opts?: ContainerOptions
|
128
|
+
) {
|
129
|
+
onMounted(() => {
|
130
|
+
if (elementRef.value) {
|
131
|
+
defineContainer(elementRef.value, name, opts);
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
135
|
+
return {
|
136
|
+
elementRef
|
137
|
+
};
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Vue directive for scroll animations
|
142
|
+
*/
|
143
|
+
export const vProteusScroll = {
|
144
|
+
mounted(el: HTMLElement, binding: { value: ScrollAnimateOptions }) {
|
145
|
+
scrollAnimate(el, binding.value);
|
146
|
+
}
|
147
|
+
};
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Vue directive for container queries
|
151
|
+
*/
|
152
|
+
export const vProteusContainer = {
|
153
|
+
mounted(el: HTMLElement, binding: { value?: { name?: string; options?: ContainerOptions } }) {
|
154
|
+
const { name, options } = binding.value || {};
|
155
|
+
defineContainer(el, name, options);
|
156
|
+
}
|
157
|
+
};
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Vue directive for performance optimizations
|
161
|
+
*/
|
162
|
+
export const vProteusPerf = {
|
163
|
+
mounted(el: HTMLElement) {
|
164
|
+
// Apply content visibility optimization
|
165
|
+
const observer = new IntersectionObserver(
|
166
|
+
(entries) => {
|
167
|
+
entries.forEach(entry => {
|
168
|
+
if (entry.isIntersecting) {
|
169
|
+
el.style.contentVisibility = 'visible';
|
170
|
+
} else {
|
171
|
+
el.style.contentVisibility = 'auto';
|
172
|
+
}
|
173
|
+
});
|
174
|
+
},
|
175
|
+
{ rootMargin: '50px' }
|
176
|
+
);
|
177
|
+
|
178
|
+
observer.observe(el);
|
179
|
+
|
180
|
+
// Store cleanup function
|
181
|
+
(el as any)._proteusCleanup = () => {
|
182
|
+
observer.disconnect();
|
183
|
+
};
|
184
|
+
},
|
185
|
+
unmounted(el: HTMLElement) {
|
186
|
+
if ((el as any)._proteusCleanup) {
|
187
|
+
(el as any)._proteusCleanup();
|
188
|
+
}
|
189
|
+
}
|
190
|
+
};
|
191
|
+
|
192
|
+
/**
|
193
|
+
* Vue directive for accessibility enhancements
|
194
|
+
*/
|
195
|
+
export const vProteusA11y = {
|
196
|
+
mounted(el: HTMLElement, binding: { value?: { announceChanges?: boolean } }) {
|
197
|
+
const { announceChanges = false } = binding.value || {};
|
198
|
+
|
199
|
+
// Enhance focus indicators
|
200
|
+
const focusableElements = el.querySelectorAll(
|
201
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
202
|
+
);
|
203
|
+
|
204
|
+
focusableElements.forEach(element => {
|
205
|
+
const htmlEl = element as HTMLElement;
|
206
|
+
htmlEl.addEventListener('focus', () => {
|
207
|
+
htmlEl.style.outline = '2px solid #0066cc';
|
208
|
+
htmlEl.style.outlineOffset = '2px';
|
209
|
+
});
|
210
|
+
|
211
|
+
htmlEl.addEventListener('blur', () => {
|
212
|
+
htmlEl.style.outline = 'none';
|
213
|
+
});
|
214
|
+
});
|
215
|
+
|
216
|
+
if (announceChanges) {
|
217
|
+
const observer = new MutationObserver(() => {
|
218
|
+
const announcement = document.createElement('div');
|
219
|
+
announcement.setAttribute('aria-live', 'polite');
|
220
|
+
announcement.style.position = 'absolute';
|
221
|
+
announcement.style.left = '-10000px';
|
222
|
+
announcement.textContent = 'Content updated';
|
223
|
+
document.body.appendChild(announcement);
|
224
|
+
|
225
|
+
setTimeout(() => {
|
226
|
+
document.body.removeChild(announcement);
|
227
|
+
}, 1000);
|
228
|
+
});
|
229
|
+
|
230
|
+
observer.observe(el, { childList: true, subtree: true });
|
231
|
+
|
232
|
+
(el as any)._proteusA11yCleanup = () => {
|
233
|
+
observer.disconnect();
|
234
|
+
};
|
235
|
+
}
|
236
|
+
},
|
237
|
+
unmounted(el: HTMLElement) {
|
238
|
+
if ((el as any)._proteusA11yCleanup) {
|
239
|
+
(el as any)._proteusA11yCleanup();
|
240
|
+
}
|
241
|
+
}
|
242
|
+
};
|
243
|
+
|
244
|
+
/**
|
245
|
+
* Plugin for Vue 3
|
246
|
+
*/
|
247
|
+
export const ProteusPlugin = {
|
248
|
+
install(app: any) {
|
249
|
+
app.directive('proteus-scroll', vProteusScroll);
|
250
|
+
app.directive('proteus-container', vProteusContainer);
|
251
|
+
app.directive('proteus-perf', vProteusPerf);
|
252
|
+
app.directive('proteus-a11y', vProteusA11y);
|
253
|
+
}
|
254
|
+
};
|
255
|
+
|
256
|
+
// Export all composables and directives
|
257
|
+
export default {
|
258
|
+
useTransition,
|
259
|
+
useScrollAnimate,
|
260
|
+
usePopover,
|
261
|
+
useAnchor,
|
262
|
+
useContainer,
|
263
|
+
vProteusScroll,
|
264
|
+
vProteusContainer,
|
265
|
+
vProteusPerf,
|
266
|
+
vProteusA11y,
|
267
|
+
ProteusPlugin
|
268
|
+
};
|
package/src/index.ts
CHANGED
@@ -1,16 +1,32 @@
|
|
1
1
|
/**
|
2
|
-
* ProteusJS -
|
2
|
+
* ProteusJS - Native-first Web Development Primitives
|
3
3
|
* Shape-shifting responsive design that adapts like the sea god himself
|
4
|
-
*
|
5
|
-
* @version 1.
|
6
|
-
* @author
|
4
|
+
*
|
5
|
+
* @version 1.1.0
|
6
|
+
* @author sc4rfurry
|
7
7
|
* @license MIT
|
8
8
|
*/
|
9
9
|
|
10
|
-
// Core exports
|
10
|
+
// Core exports (legacy compatibility)
|
11
11
|
export { ProteusJS as default } from './core/ProteusJS';
|
12
12
|
export { ProteusJS } from './core/ProteusJS';
|
13
13
|
|
14
|
+
// New modular exports
|
15
|
+
export * as transitions from './modules/transitions';
|
16
|
+
export * as scroll from './modules/scroll';
|
17
|
+
export * as anchor from './modules/anchor';
|
18
|
+
export * as popover from './modules/popover';
|
19
|
+
export * as container from './modules/container';
|
20
|
+
export * as typography from './modules/typography';
|
21
|
+
export * as a11yAudit from './modules/a11y-audit';
|
22
|
+
export * as a11yPrimitives from './modules/a11y-primitives';
|
23
|
+
export * as perf from './modules/perf';
|
24
|
+
|
25
|
+
// Framework adapters are available as separate subpath exports:
|
26
|
+
// import { ... } from '@sc4rfurryx/proteusjs/adapters/react'
|
27
|
+
// import { ... } from '@sc4rfurryx/proteusjs/adapters/vue'
|
28
|
+
// import { ... } from '@sc4rfurryx/proteusjs/adapters/svelte'
|
29
|
+
|
14
30
|
// Type exports
|
15
31
|
export type {
|
16
32
|
ProteusConfig,
|
@@ -23,6 +39,17 @@ export type {
|
|
23
39
|
PerformanceConfig
|
24
40
|
} from './types';
|
25
41
|
|
42
|
+
// Module-specific type exports
|
43
|
+
export type { TransitionOptions, NavigateOptions } from './modules/transitions';
|
44
|
+
export type { ScrollAnimateOptions } from './modules/scroll';
|
45
|
+
export type { TetherOptions, TetherController } from './modules/anchor';
|
46
|
+
export type { PopoverOptions, PopoverController } from './modules/popover';
|
47
|
+
export type { ContainerOptions } from './modules/container';
|
48
|
+
export type { FluidTypeOptions, FluidTypeResult } from './modules/typography';
|
49
|
+
export type { AuditOptions, AuditReport, AuditViolation } from './modules/a11y-audit';
|
50
|
+
export type { Controller, DialogOptions, TooltipOptions, FocusTrapController } from './modules/a11y-primitives';
|
51
|
+
export type { SpeculationOptions, ContentVisibilityOptions } from './modules/perf';
|
52
|
+
|
26
53
|
// Utility exports
|
27
54
|
export { version } from './utils/version';
|
28
55
|
export { isSupported } from './utils/support';
|
@@ -31,5 +58,5 @@ export { isSupported } from './utils/support';
|
|
31
58
|
export type { ProteusPlugin } from './core/PluginSystem';
|
32
59
|
|
33
60
|
// Constants
|
34
|
-
export const VERSION = '1.
|
61
|
+
export const VERSION = '1.1.0';
|
35
62
|
export const LIBRARY_NAME = 'ProteusJS';
|
@@ -0,0 +1,84 @@
|
|
1
|
+
/**
|
2
|
+
* @sc4rfurryx/proteusjs/a11y-audit
|
3
|
+
* Lightweight accessibility audits for development
|
4
|
+
*
|
5
|
+
* @version 1.1.0
|
6
|
+
* @author sc4rfurry
|
7
|
+
* @license MIT
|
8
|
+
*/
|
9
|
+
|
10
|
+
export interface AuditOptions {
|
11
|
+
rules?: string[];
|
12
|
+
format?: 'console' | 'json';
|
13
|
+
}
|
14
|
+
|
15
|
+
export interface AuditViolation {
|
16
|
+
id: string;
|
17
|
+
impact: 'minor' | 'moderate' | 'serious' | 'critical';
|
18
|
+
nodes: number;
|
19
|
+
help: string;
|
20
|
+
}
|
21
|
+
|
22
|
+
export interface AuditReport {
|
23
|
+
violations: AuditViolation[];
|
24
|
+
passes: number;
|
25
|
+
timestamp: number;
|
26
|
+
url: string;
|
27
|
+
}
|
28
|
+
|
29
|
+
export async function audit(
|
30
|
+
target: Document | Element = document,
|
31
|
+
options: AuditOptions = {}
|
32
|
+
): Promise<AuditReport> {
|
33
|
+
if (typeof window === 'undefined' || process.env['NODE_ENV'] === 'production') {
|
34
|
+
return { violations: [], passes: 0, timestamp: Date.now(), url: '' };
|
35
|
+
}
|
36
|
+
|
37
|
+
const { rules = ['images', 'headings', 'forms'], format = 'console' } = options;
|
38
|
+
const violations: AuditViolation[] = [];
|
39
|
+
let passes = 0;
|
40
|
+
|
41
|
+
if (rules.includes('images')) {
|
42
|
+
const imgs = target.querySelectorAll('img:not([alt])');
|
43
|
+
if (imgs.length > 0) {
|
44
|
+
violations.push({
|
45
|
+
id: 'image-alt', impact: 'critical', nodes: imgs.length, help: 'Images need alt text'
|
46
|
+
});
|
47
|
+
}
|
48
|
+
passes += target.querySelectorAll('img[alt]').length;
|
49
|
+
}
|
50
|
+
|
51
|
+
if (rules.includes('headings')) {
|
52
|
+
const h1s = target.querySelectorAll('h1');
|
53
|
+
if (h1s.length !== 1) {
|
54
|
+
violations.push({
|
55
|
+
id: 'heading-structure', impact: 'moderate', nodes: h1s.length, help: 'Page should have exactly one h1'
|
56
|
+
});
|
57
|
+
} else passes++;
|
58
|
+
}
|
59
|
+
|
60
|
+
if (rules.includes('forms')) {
|
61
|
+
const unlabeled = target.querySelectorAll('input:not([aria-label]):not([aria-labelledby])');
|
62
|
+
if (unlabeled.length > 0) {
|
63
|
+
violations.push({
|
64
|
+
id: 'form-labels', impact: 'critical', nodes: unlabeled.length, help: 'Form inputs need labels'
|
65
|
+
});
|
66
|
+
}
|
67
|
+
passes += target.querySelectorAll('input[aria-label], input[aria-labelledby]').length;
|
68
|
+
}
|
69
|
+
|
70
|
+
const report: AuditReport = {
|
71
|
+
violations, passes, timestamp: Date.now(),
|
72
|
+
url: typeof window !== 'undefined' ? window.location.href : ''
|
73
|
+
};
|
74
|
+
|
75
|
+
if (format === 'console' && violations.length > 0) {
|
76
|
+
console.group('🔍 A11y Audit Results');
|
77
|
+
violations.forEach(v => console.warn(`${v.impact}: ${v.help}`));
|
78
|
+
console.groupEnd();
|
79
|
+
}
|
80
|
+
|
81
|
+
return report;
|
82
|
+
}
|
83
|
+
|
84
|
+
export default { audit };
|
@@ -0,0 +1,152 @@
|
|
1
|
+
/**
|
2
|
+
* @sc4rfurryx/proteusjs/a11y-primitives
|
3
|
+
* Lightweight accessibility patterns
|
4
|
+
*
|
5
|
+
* @version 1.1.0
|
6
|
+
* @author sc4rfurry
|
7
|
+
* @license MIT
|
8
|
+
*/
|
9
|
+
|
10
|
+
export interface Controller {
|
11
|
+
destroy(): void;
|
12
|
+
}
|
13
|
+
|
14
|
+
export interface DialogOptions {
|
15
|
+
modal?: boolean;
|
16
|
+
restoreFocus?: boolean;
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface TooltipOptions {
|
20
|
+
delay?: number;
|
21
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
22
|
+
}
|
23
|
+
|
24
|
+
export interface FocusTrapController {
|
25
|
+
activate(): void;
|
26
|
+
deactivate(): void;
|
27
|
+
}
|
28
|
+
|
29
|
+
export function dialog(root: Element | string, opts: DialogOptions = {}): Controller {
|
30
|
+
const el = typeof root === 'string' ? document.querySelector(root) : root;
|
31
|
+
if (!el) throw new Error('Dialog element not found');
|
32
|
+
|
33
|
+
const { modal = true, restoreFocus = true } = opts;
|
34
|
+
let prevFocus: Element | null = null;
|
35
|
+
|
36
|
+
const open = () => {
|
37
|
+
if (restoreFocus) prevFocus = document.activeElement;
|
38
|
+
el.setAttribute('role', 'dialog');
|
39
|
+
if (modal) el.setAttribute('aria-modal', 'true');
|
40
|
+
(el as HTMLElement).focus();
|
41
|
+
};
|
42
|
+
|
43
|
+
const close = () => {
|
44
|
+
if (restoreFocus && prevFocus) (prevFocus as HTMLElement).focus();
|
45
|
+
};
|
46
|
+
|
47
|
+
return { destroy: () => close() };
|
48
|
+
}
|
49
|
+
|
50
|
+
export function tooltip(trigger: Element, content: Element, opts: TooltipOptions = {}): Controller {
|
51
|
+
const { delay = 300 } = opts;
|
52
|
+
let timeout: number;
|
53
|
+
|
54
|
+
const show = () => {
|
55
|
+
clearTimeout(timeout);
|
56
|
+
timeout = window.setTimeout(() => {
|
57
|
+
content.setAttribute('role', 'tooltip');
|
58
|
+
trigger.setAttribute('aria-describedby', content.id || 'tooltip');
|
59
|
+
content.style.display = 'block';
|
60
|
+
}, delay);
|
61
|
+
};
|
62
|
+
|
63
|
+
const hide = () => {
|
64
|
+
clearTimeout(timeout);
|
65
|
+
content.style.display = 'none';
|
66
|
+
trigger.removeAttribute('aria-describedby');
|
67
|
+
};
|
68
|
+
|
69
|
+
trigger.addEventListener('mouseenter', show);
|
70
|
+
trigger.addEventListener('mouseleave', hide);
|
71
|
+
trigger.addEventListener('focus', show);
|
72
|
+
trigger.addEventListener('blur', hide);
|
73
|
+
|
74
|
+
return {
|
75
|
+
destroy: () => {
|
76
|
+
clearTimeout(timeout);
|
77
|
+
trigger.removeEventListener('mouseenter', show);
|
78
|
+
trigger.removeEventListener('mouseleave', hide);
|
79
|
+
trigger.removeEventListener('focus', show);
|
80
|
+
trigger.removeEventListener('blur', hide);
|
81
|
+
}
|
82
|
+
};
|
83
|
+
}
|
84
|
+
|
85
|
+
export function focusTrap(container: Element): FocusTrapController {
|
86
|
+
const focusable = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
87
|
+
|
88
|
+
const activate = () => {
|
89
|
+
const elements = container.querySelectorAll(focusable);
|
90
|
+
if (elements.length === 0) return;
|
91
|
+
|
92
|
+
const first = elements[0] as HTMLElement;
|
93
|
+
const last = elements[elements.length - 1] as HTMLElement;
|
94
|
+
|
95
|
+
const handleTab = (e: KeyboardEvent) => {
|
96
|
+
if (e.key !== 'Tab') return;
|
97
|
+
|
98
|
+
if (e.shiftKey && document.activeElement === first) {
|
99
|
+
e.preventDefault();
|
100
|
+
last.focus();
|
101
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
102
|
+
e.preventDefault();
|
103
|
+
first.focus();
|
104
|
+
}
|
105
|
+
};
|
106
|
+
|
107
|
+
container.addEventListener('keydown', handleTab);
|
108
|
+
first.focus();
|
109
|
+
|
110
|
+
return () => container.removeEventListener('keydown', handleTab);
|
111
|
+
};
|
112
|
+
|
113
|
+
let deactivate = () => {};
|
114
|
+
|
115
|
+
return {
|
116
|
+
activate: () => { deactivate = activate() || (() => {}); },
|
117
|
+
deactivate: () => deactivate()
|
118
|
+
};
|
119
|
+
}
|
120
|
+
|
121
|
+
export function menu(container: Element): Controller {
|
122
|
+
const items = container.querySelectorAll('[role="menuitem"]');
|
123
|
+
let currentIndex = 0;
|
124
|
+
|
125
|
+
const navigate = (e: KeyboardEvent) => {
|
126
|
+
switch (e.key) {
|
127
|
+
case 'ArrowDown':
|
128
|
+
e.preventDefault();
|
129
|
+
currentIndex = (currentIndex + 1) % items.length;
|
130
|
+
(items[currentIndex] as HTMLElement).focus();
|
131
|
+
break;
|
132
|
+
case 'ArrowUp':
|
133
|
+
e.preventDefault();
|
134
|
+
currentIndex = currentIndex === 0 ? items.length - 1 : currentIndex - 1;
|
135
|
+
(items[currentIndex] as HTMLElement).focus();
|
136
|
+
break;
|
137
|
+
case 'Escape':
|
138
|
+
e.preventDefault();
|
139
|
+
container.dispatchEvent(new CustomEvent('menu:close'));
|
140
|
+
break;
|
141
|
+
}
|
142
|
+
};
|
143
|
+
|
144
|
+
container.setAttribute('role', 'menu');
|
145
|
+
container.addEventListener('keydown', navigate);
|
146
|
+
|
147
|
+
return {
|
148
|
+
destroy: () => container.removeEventListener('keydown', navigate)
|
149
|
+
};
|
150
|
+
}
|
151
|
+
|
152
|
+
export default { dialog, tooltip, focusTrap, menu };
|