@rakeyshgidwani/roger-ui-bank-theme-stan-design 0.1.4 → 0.1.5
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/CHANGELOG.md +1 -1
- package/dist/index.d.ts +131 -131
- package/dist/index.esm.js +148 -148
- package/dist/index.js +148 -148
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/ui/accessibility-demo.tsx +271 -0
- package/src/components/ui/advanced-component-architecture-demo.tsx +916 -0
- package/src/components/ui/advanced-transition-system-demo.tsx +670 -0
- package/src/components/ui/advanced-transition-system.tsx +395 -0
- package/src/components/ui/animation/animated-container.tsx +166 -0
- package/src/components/ui/animation/index.ts +19 -0
- package/src/components/ui/animation/staggered-container.tsx +68 -0
- package/src/components/ui/animation-demo.tsx +250 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/battery-conscious-animation-demo.tsx +568 -0
- package/src/components/ui/border-radius-shadow-demo.tsx +187 -0
- package/src/components/ui/button.tsx +36 -0
- package/src/components/ui/card.tsx +207 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/color-preview.tsx +411 -0
- package/src/components/ui/data-display/chart.tsx +653 -0
- package/src/components/ui/data-display/data-grid-simple.tsx +76 -0
- package/src/components/ui/data-display/data-grid.tsx +680 -0
- package/src/components/ui/data-display/list.tsx +456 -0
- package/src/components/ui/data-display/table.tsx +482 -0
- package/src/components/ui/data-display/timeline.tsx +441 -0
- package/src/components/ui/data-display/tree.tsx +602 -0
- package/src/components/ui/data-display/types.ts +536 -0
- package/src/components/ui/enterprise-mobile-experience-demo.tsx +749 -0
- package/src/components/ui/enterprise-mobile-experience.tsx +464 -0
- package/src/components/ui/feedback/alert.tsx +157 -0
- package/src/components/ui/feedback/progress.tsx +292 -0
- package/src/components/ui/feedback/skeleton.tsx +185 -0
- package/src/components/ui/feedback/toast.tsx +280 -0
- package/src/components/ui/feedback/types.ts +125 -0
- package/src/components/ui/font-preview.tsx +288 -0
- package/src/components/ui/form-demo.tsx +553 -0
- package/src/components/ui/hardware-acceleration-demo.tsx +547 -0
- package/src/components/ui/input.tsx +35 -0
- package/src/components/ui/label.tsx +16 -0
- package/src/components/ui/layout-demo.tsx +367 -0
- package/src/components/ui/layouts/adaptive-layout.tsx +139 -0
- package/src/components/ui/layouts/desktop-layout.tsx +224 -0
- package/src/components/ui/layouts/index.ts +10 -0
- package/src/components/ui/layouts/mobile-layout.tsx +162 -0
- package/src/components/ui/layouts/tablet-layout.tsx +197 -0
- package/src/components/ui/mobile-form-validation.tsx +451 -0
- package/src/components/ui/mobile-input-demo.tsx +201 -0
- package/src/components/ui/mobile-input.tsx +281 -0
- package/src/components/ui/mobile-skeleton-loading-demo.tsx +638 -0
- package/src/components/ui/navigation/breadcrumb.tsx +158 -0
- package/src/components/ui/navigation/index.ts +36 -0
- package/src/components/ui/navigation/menu.tsx +374 -0
- package/src/components/ui/navigation/navigation-demo.tsx +324 -0
- package/src/components/ui/navigation/pagination.tsx +272 -0
- package/src/components/ui/navigation/sidebar.tsx +383 -0
- package/src/components/ui/navigation/stepper.tsx +303 -0
- package/src/components/ui/navigation/tabs.tsx +205 -0
- package/src/components/ui/navigation/types.ts +299 -0
- package/src/components/ui/overlay/backdrop.tsx +81 -0
- package/src/components/ui/overlay/focus-manager.tsx +143 -0
- package/src/components/ui/overlay/index.ts +36 -0
- package/src/components/ui/overlay/modal.tsx +270 -0
- package/src/components/ui/overlay/overlay-manager.tsx +110 -0
- package/src/components/ui/overlay/popover.tsx +462 -0
- package/src/components/ui/overlay/portal.tsx +79 -0
- package/src/components/ui/overlay/tooltip.tsx +303 -0
- package/src/components/ui/overlay/types.ts +196 -0
- package/src/components/ui/performance-demo.tsx +596 -0
- package/src/components/ui/semantic-input-system-demo.tsx +502 -0
- package/src/components/ui/semantic-input-system-demo.tsx.disabled +873 -0
- package/src/components/ui/tablet-layout.tsx +192 -0
- package/src/components/ui/theme-customizer.tsx +386 -0
- package/src/components/ui/theme-preview.tsx +310 -0
- package/src/components/ui/theme-switcher.tsx +264 -0
- package/src/components/ui/theme-toggle.tsx +38 -0
- package/src/components/ui/token-demo.tsx +195 -0
- package/src/components/ui/touch-demo.tsx +462 -0
- package/src/components/ui/touch-friendly-interface-demo.tsx +519 -0
- package/src/components/ui/touch-friendly-interface.tsx +296 -0
- package/src/hooks/index.ts +190 -0
- package/src/hooks/use-accessibility-support.ts +518 -0
- package/src/hooks/use-adaptive-layout.ts +289 -0
- package/src/hooks/use-advanced-patterns.ts +294 -0
- package/src/hooks/use-advanced-transition-system.ts +393 -0
- package/src/hooks/use-animation-profile.ts +288 -0
- package/src/hooks/use-battery-animations.ts +384 -0
- package/src/hooks/use-battery-conscious-loading.ts +475 -0
- package/src/hooks/use-battery-optimization.ts +330 -0
- package/src/hooks/use-battery-status.ts +299 -0
- package/src/hooks/use-component-performance.ts +344 -0
- package/src/hooks/use-device-loading-states.ts +459 -0
- package/src/hooks/use-device.tsx +110 -0
- package/src/hooks/use-enterprise-mobile-experience.ts +488 -0
- package/src/hooks/use-form-feedback.ts +403 -0
- package/src/hooks/use-form-performance.ts +513 -0
- package/src/hooks/use-frame-rate.ts +251 -0
- package/src/hooks/use-gestures.ts +338 -0
- package/src/hooks/use-hardware-acceleration.ts +341 -0
- package/src/hooks/use-input-accessibility.ts +455 -0
- package/src/hooks/use-input-performance.ts +506 -0
- package/src/hooks/use-layout-performance.ts +319 -0
- package/src/hooks/use-loading-accessibility.ts +535 -0
- package/src/hooks/use-loading-performance.ts +473 -0
- package/src/hooks/use-memory-usage.ts +287 -0
- package/src/hooks/use-mobile-form-layout.ts +464 -0
- package/src/hooks/use-mobile-form-validation.ts +518 -0
- package/src/hooks/use-mobile-keyboard-optimization.ts +472 -0
- package/src/hooks/use-mobile-layout.ts +302 -0
- package/src/hooks/use-mobile-optimization.ts +406 -0
- package/src/hooks/use-mobile-skeleton.ts +402 -0
- package/src/hooks/use-mobile-touch.ts +414 -0
- package/src/hooks/use-performance-throttling.ts +348 -0
- package/src/hooks/use-performance.ts +316 -0
- package/src/hooks/use-reusable-architecture.ts +414 -0
- package/src/hooks/use-semantic-input-types.ts +357 -0
- package/src/hooks/use-semantic-input.ts +565 -0
- package/src/hooks/use-tablet-layout.ts +384 -0
- package/src/hooks/use-touch-friendly-input.ts +524 -0
- package/src/hooks/use-touch-friendly-interface.ts +331 -0
- package/src/hooks/use-touch-optimization.ts +375 -0
- package/src/index.ts +279 -279
- package/src/lib/utils.ts +6 -0
- package/src/themes/README.md +272 -0
- package/src/themes/ThemeContext.tsx +31 -0
- package/src/themes/ThemeProvider.tsx +232 -0
- package/src/themes/accessibility/index.ts +27 -0
- package/src/themes/accessibility.ts +259 -0
- package/src/themes/aria-patterns.ts +420 -0
- package/src/themes/base-themes.ts +55 -0
- package/src/themes/colorManager.ts +380 -0
- package/src/themes/examples/dark-theme.ts +154 -0
- package/src/themes/examples/minimal-theme.ts +108 -0
- package/src/themes/focus-management.ts +701 -0
- package/src/themes/fontLoader.ts +201 -0
- package/src/themes/high-contrast.ts +621 -0
- package/src/themes/index.ts +19 -0
- package/src/themes/inheritance.ts +227 -0
- package/src/themes/keyboard-navigation.ts +550 -0
- package/src/themes/motion-reduction.ts +662 -0
- package/src/themes/navigation.ts +238 -0
- package/src/themes/screen-reader.ts +645 -0
- package/src/themes/systemThemeDetector.ts +182 -0
- package/src/themes/themeCSSUpdater.ts +262 -0
- package/src/themes/themePersistence.ts +238 -0
- package/src/themes/themes/default.ts +586 -0
- package/src/themes/themes/harvey.ts +554 -0
- package/src/themes/themes/stan-design.ts +683 -0
- package/src/themes/types.ts +460 -0
- package/src/themes/useSystemTheme.ts +48 -0
- package/src/themes/useTheme.ts +87 -0
- package/src/themes/validation.ts +462 -0
- package/src/tokens/index.ts +34 -0
- package/src/tokens/tokenExporter.ts +397 -0
- package/src/tokens/tokenGenerator.ts +276 -0
- package/src/tokens/tokenManager.ts +248 -0
- package/src/tokens/tokenValidator.ts +543 -0
- package/src/tokens/types.ts +78 -0
- package/src/utils/bundle-analyzer.ts +260 -0
- package/src/utils/bundle-splitting.ts +483 -0
- package/src/utils/lazy-loading.ts +441 -0
- package/src/utils/performance-monitor.ts +513 -0
- package/src/utils/tree-shaking.ts +274 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
// Keyboard key constants
|
|
2
|
+
export const KEY_CODES = {
|
|
3
|
+
// Navigation keys
|
|
4
|
+
TAB: 'Tab',
|
|
5
|
+
SHIFT_TAB: 'Shift+Tab',
|
|
6
|
+
ARROW_UP: 'ArrowUp',
|
|
7
|
+
ARROW_DOWN: 'ArrowDown',
|
|
8
|
+
ARROW_LEFT: 'ArrowLeft',
|
|
9
|
+
ARROW_RIGHT: 'ArrowRight',
|
|
10
|
+
HOME: 'Home',
|
|
11
|
+
END: 'End',
|
|
12
|
+
PAGE_UP: 'PageUp',
|
|
13
|
+
PAGE_DOWN: 'PageDown',
|
|
14
|
+
|
|
15
|
+
// Action keys
|
|
16
|
+
ENTER: 'Enter',
|
|
17
|
+
SPACE: ' ',
|
|
18
|
+
ESCAPE: 'Escape',
|
|
19
|
+
BACKSPACE: 'Backspace',
|
|
20
|
+
DELETE: 'Delete',
|
|
21
|
+
|
|
22
|
+
// Modifier keys
|
|
23
|
+
SHIFT: 'Shift',
|
|
24
|
+
CTRL: 'Control',
|
|
25
|
+
ALT: 'Alt',
|
|
26
|
+
META: 'Meta',
|
|
27
|
+
|
|
28
|
+
// Function keys
|
|
29
|
+
F1: 'F1',
|
|
30
|
+
F2: 'F2',
|
|
31
|
+
F3: 'F3',
|
|
32
|
+
F4: 'F4',
|
|
33
|
+
F5: 'F5',
|
|
34
|
+
F6: 'F6',
|
|
35
|
+
F7: 'F7',
|
|
36
|
+
F8: 'F8',
|
|
37
|
+
F9: 'F9',
|
|
38
|
+
F10: 'F10',
|
|
39
|
+
F11: 'F11',
|
|
40
|
+
F12: 'F12'
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
// Keyboard navigation patterns
|
|
44
|
+
export const NAVIGATION_PATTERNS = {
|
|
45
|
+
// List navigation
|
|
46
|
+
LIST: {
|
|
47
|
+
NEXT: [KEY_CODES.ARROW_DOWN, KEY_CODES.ARROW_RIGHT],
|
|
48
|
+
PREVIOUS: [KEY_CODES.ARROW_UP, KEY_CODES.ARROW_LEFT],
|
|
49
|
+
FIRST: [KEY_CODES.HOME],
|
|
50
|
+
LAST: [KEY_CODES.END],
|
|
51
|
+
SELECT: [KEY_CODES.ENTER, KEY_CODES.SPACE]
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Grid navigation
|
|
55
|
+
GRID: {
|
|
56
|
+
NEXT_ROW: [KEY_CODES.ARROW_DOWN],
|
|
57
|
+
PREVIOUS_ROW: [KEY_CODES.ARROW_UP],
|
|
58
|
+
NEXT_COLUMN: [KEY_CODES.ARROW_RIGHT],
|
|
59
|
+
PREVIOUS_COLUMN: [KEY_CODES.ARROW_LEFT],
|
|
60
|
+
FIRST_ROW: [KEY_CODES.HOME],
|
|
61
|
+
LAST_ROW: [KEY_CODES.END],
|
|
62
|
+
FIRST_COLUMN: [KEY_CODES.CTRL + '+' + KEY_CODES.HOME],
|
|
63
|
+
LAST_COLUMN: [KEY_CODES.CTRL + '+' + KEY_CODES.END]
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Tree navigation
|
|
67
|
+
TREE: {
|
|
68
|
+
EXPAND: [KEY_CODES.ARROW_RIGHT],
|
|
69
|
+
COLLAPSE: [KEY_CODES.ARROW_LEFT],
|
|
70
|
+
NEXT_ITEM: [KEY_CODES.ARROW_DOWN],
|
|
71
|
+
PREVIOUS_ITEM: [KEY_CODES.ARROW_UP],
|
|
72
|
+
FIRST_ITEM: [KEY_CODES.HOME],
|
|
73
|
+
LAST_ITEM: [KEY_CODES.END],
|
|
74
|
+
SELECT: [KEY_CODES.ENTER, KEY_CODES.SPACE]
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// Tab navigation
|
|
78
|
+
TABS: {
|
|
79
|
+
NEXT_TAB: [KEY_CODES.ARROW_RIGHT],
|
|
80
|
+
PREVIOUS_TAB: [KEY_CODES.ARROW_LEFT],
|
|
81
|
+
FIRST_TAB: [KEY_CODES.HOME],
|
|
82
|
+
LAST_TAB: [KEY_CODES.END],
|
|
83
|
+
SELECT_TAB: [KEY_CODES.ENTER, KEY_CODES.SPACE]
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Menu navigation
|
|
87
|
+
MENU: {
|
|
88
|
+
OPEN: [KEY_CODES.ENTER, KEY_CODES.SPACE, KEY_CODES.ARROW_DOWN],
|
|
89
|
+
CLOSE: [KEY_CODES.ESCAPE],
|
|
90
|
+
NEXT_ITEM: [KEY_CODES.ARROW_DOWN],
|
|
91
|
+
PREVIOUS_ITEM: [KEY_CODES.ARROW_UP],
|
|
92
|
+
FIRST_ITEM: [KEY_CODES.HOME],
|
|
93
|
+
LAST_ITEM: [KEY_CODES.END],
|
|
94
|
+
SELECT: [KEY_CODES.ENTER, KEY_CODES.SPACE]
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
// Modal navigation
|
|
98
|
+
MODAL: {
|
|
99
|
+
CLOSE: [KEY_CODES.ESCAPE],
|
|
100
|
+
NEXT_FOCUS: [KEY_CODES.TAB],
|
|
101
|
+
PREVIOUS_FOCUS: [KEY_CODES.SHIFT + '+' + KEY_CODES.TAB]
|
|
102
|
+
}
|
|
103
|
+
} as const;
|
|
104
|
+
|
|
105
|
+
// Keyboard navigation manager
|
|
106
|
+
export class KeyboardNavigationManager {
|
|
107
|
+
private focusableSelectors: string[];
|
|
108
|
+
private currentFocusIndex: number;
|
|
109
|
+
private focusableElements: HTMLElement[];
|
|
110
|
+
private container: HTMLElement | null;
|
|
111
|
+
private onFocusChange: ((element: HTMLElement, index: number) => void) | null;
|
|
112
|
+
|
|
113
|
+
constructor(
|
|
114
|
+
container?: HTMLElement,
|
|
115
|
+
focusableSelectors: string[] = [
|
|
116
|
+
'button:not([disabled])',
|
|
117
|
+
'input:not([disabled])',
|
|
118
|
+
'select:not([disabled])',
|
|
119
|
+
'textarea:not([disabled])',
|
|
120
|
+
'a[href]',
|
|
121
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
122
|
+
'[contenteditable="true"]'
|
|
123
|
+
]
|
|
124
|
+
) {
|
|
125
|
+
this.container = container || null;
|
|
126
|
+
this.focusableSelectors = focusableSelectors;
|
|
127
|
+
this.currentFocusIndex = -1;
|
|
128
|
+
this.focusableElements = [];
|
|
129
|
+
this.onFocusChange = null;
|
|
130
|
+
|
|
131
|
+
this.updateFocusableElements();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Set container element
|
|
135
|
+
setContainer(container: HTMLElement) {
|
|
136
|
+
this.container = container;
|
|
137
|
+
this.updateFocusableElements();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Update list of focusable elements
|
|
141
|
+
updateFocusableElements() {
|
|
142
|
+
if (!this.container) return;
|
|
143
|
+
|
|
144
|
+
this.focusableElements = Array.from(
|
|
145
|
+
this.container.querySelectorAll<HTMLElement>(this.focusableSelectors.join(','))
|
|
146
|
+
).filter(element => {
|
|
147
|
+
// Filter out hidden elements
|
|
148
|
+
const style = window.getComputedStyle(element);
|
|
149
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Reset focus index
|
|
153
|
+
this.currentFocusIndex = -1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Set focus change callback
|
|
157
|
+
onFocusChangeCallback(callback: (element: HTMLElement, index: number) => void) {
|
|
158
|
+
this.onFocusChange = callback;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Get current focusable elements
|
|
162
|
+
getFocusableElements(): HTMLElement[] {
|
|
163
|
+
return [...this.focusableElements];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get current focus index
|
|
167
|
+
getCurrentFocusIndex(): number {
|
|
168
|
+
return this.currentFocusIndex;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Focus first element
|
|
172
|
+
focusFirst(): boolean {
|
|
173
|
+
if (this.focusableElements.length === 0) return false;
|
|
174
|
+
|
|
175
|
+
this.currentFocusIndex = 0;
|
|
176
|
+
const element = this.focusableElements[0];
|
|
177
|
+
element.focus();
|
|
178
|
+
|
|
179
|
+
if (this.onFocusChange) {
|
|
180
|
+
this.onFocusChange(element, 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Focus last element
|
|
187
|
+
focusLast(): boolean {
|
|
188
|
+
if (this.focusableElements.length === 0) return false;
|
|
189
|
+
|
|
190
|
+
this.currentFocusIndex = this.focusableElements.length - 1;
|
|
191
|
+
const element = this.focusableElements[this.currentFocusIndex];
|
|
192
|
+
element.focus();
|
|
193
|
+
|
|
194
|
+
if (this.onFocusChange) {
|
|
195
|
+
this.onFocusChange(element, this.currentFocusIndex);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Focus next element
|
|
202
|
+
focusNext(): boolean {
|
|
203
|
+
if (this.focusableElements.length === 0) return false;
|
|
204
|
+
|
|
205
|
+
const nextIndex = (this.currentFocusIndex + 1) % this.focusableElements.length;
|
|
206
|
+
this.currentFocusIndex = nextIndex;
|
|
207
|
+
const element = this.focusableElements[nextIndex];
|
|
208
|
+
element.focus();
|
|
209
|
+
|
|
210
|
+
if (this.onFocusChange) {
|
|
211
|
+
this.onFocusChange(element, nextIndex);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Focus previous element
|
|
218
|
+
focusPrevious(): boolean {
|
|
219
|
+
if (this.focusableElements.length === 0) return false;
|
|
220
|
+
|
|
221
|
+
const prevIndex = this.currentFocusIndex <= 0
|
|
222
|
+
? this.focusableElements.length - 1
|
|
223
|
+
: this.currentFocusIndex - 1;
|
|
224
|
+
this.currentFocusIndex = prevIndex;
|
|
225
|
+
const element = this.focusableElements[prevIndex];
|
|
226
|
+
element.focus();
|
|
227
|
+
|
|
228
|
+
if (this.onFocusChange) {
|
|
229
|
+
this.onFocusChange(element, prevIndex);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Focus element by index
|
|
236
|
+
focusByIndex(index: number): boolean {
|
|
237
|
+
if (index < 0 || index >= this.focusableElements.length) return false;
|
|
238
|
+
|
|
239
|
+
this.currentFocusIndex = index;
|
|
240
|
+
const element = this.focusableElements[index];
|
|
241
|
+
element.focus();
|
|
242
|
+
|
|
243
|
+
if (this.onFocusChange) {
|
|
244
|
+
this.onFocusChange(element, index);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Focus element by ID
|
|
251
|
+
focusById(id: string): boolean {
|
|
252
|
+
const index = this.focusableElements.findIndex(element => element.id === id);
|
|
253
|
+
if (index === -1) return false;
|
|
254
|
+
|
|
255
|
+
return this.focusByIndex(index);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Focus element by data attribute
|
|
259
|
+
focusByDataAttribute(attribute: string, value: string): boolean {
|
|
260
|
+
const index = this.focusableElements.findIndex(
|
|
261
|
+
element => element.getAttribute(`data-${attribute}`) === value
|
|
262
|
+
);
|
|
263
|
+
if (index === -1) return false;
|
|
264
|
+
|
|
265
|
+
return this.focusByIndex(index);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle keyboard navigation
|
|
269
|
+
handleKeyDown(event: KeyboardEvent): boolean {
|
|
270
|
+
const key = event.key;
|
|
271
|
+
const isShift = event.shiftKey;
|
|
272
|
+
const isCtrl = event.ctrlKey;
|
|
273
|
+
|
|
274
|
+
// Handle Tab navigation
|
|
275
|
+
if (key === KEY_CODES.TAB) {
|
|
276
|
+
if (isShift) {
|
|
277
|
+
return this.focusPrevious();
|
|
278
|
+
} else {
|
|
279
|
+
return this.focusNext();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle arrow key navigation
|
|
284
|
+
if (key === KEY_CODES.ARROW_DOWN || key === KEY_CODES.ARROW_RIGHT) {
|
|
285
|
+
event.preventDefault();
|
|
286
|
+
return this.focusNext();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (key === KEY_CODES.ARROW_UP || key === KEY_CODES.ARROW_LEFT) {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
return this.focusPrevious();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle Home/End navigation
|
|
295
|
+
if (key === KEY_CODES.HOME) {
|
|
296
|
+
event.preventDefault();
|
|
297
|
+
if (isCtrl) {
|
|
298
|
+
return this.focusFirst();
|
|
299
|
+
}
|
|
300
|
+
return this.focusFirst();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (key === KEY_CODES.END) {
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
if (isCtrl) {
|
|
306
|
+
return this.focusLast();
|
|
307
|
+
}
|
|
308
|
+
return this.focusLast();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Handle Page Up/Down navigation
|
|
312
|
+
if (key === KEY_CODES.PAGE_UP) {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
// Jump to previous page of elements
|
|
315
|
+
const pageSize = Math.max(1, Math.floor(this.focusableElements.length / 10));
|
|
316
|
+
const targetIndex = Math.max(0, this.currentFocusIndex - pageSize);
|
|
317
|
+
return this.focusByIndex(targetIndex);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (key === KEY_CODES.PAGE_DOWN) {
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
// Jump to next page of elements
|
|
323
|
+
const pageSize = Math.max(1, Math.floor(this.focusableElements.length / 10));
|
|
324
|
+
const targetIndex = Math.min(
|
|
325
|
+
this.focusableElements.length - 1,
|
|
326
|
+
this.currentFocusIndex + pageSize
|
|
327
|
+
);
|
|
328
|
+
return this.focusByIndex(targetIndex);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Handle Enter/Space for selection
|
|
332
|
+
if (key === KEY_CODES.ENTER || key === KEY_CODES.SPACE) {
|
|
333
|
+
if (this.currentFocusIndex >= 0) {
|
|
334
|
+
const element = this.focusableElements[this.currentFocusIndex];
|
|
335
|
+
// Trigger click or submit event
|
|
336
|
+
if (element.tagName === 'BUTTON' || element.tagName === 'A') {
|
|
337
|
+
element.click();
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Handle Escape for closing/canceling
|
|
344
|
+
if (key === KEY_CODES.ESCAPE) {
|
|
345
|
+
// This could trigger a close action depending on context
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Trap focus within container (for modals, etc.)
|
|
353
|
+
trapFocus(): void {
|
|
354
|
+
if (!this.container) return;
|
|
355
|
+
|
|
356
|
+
// Focus first element initially
|
|
357
|
+
this.focusFirst();
|
|
358
|
+
|
|
359
|
+
// Add event listener for focus trapping
|
|
360
|
+
const handleFocus = (event: FocusEvent) => {
|
|
361
|
+
const target = event.target as HTMLElement;
|
|
362
|
+
|
|
363
|
+
// If focus is outside the container, move it back
|
|
364
|
+
if (!this.container?.contains(target)) {
|
|
365
|
+
if (this.focusableElements.length > 0) {
|
|
366
|
+
this.focusFirst();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
this.container.addEventListener('focusin', handleFocus);
|
|
372
|
+
|
|
373
|
+
// Store cleanup function
|
|
374
|
+
(this.container as any)._focusTrapCleanup = () => {
|
|
375
|
+
this.container?.removeEventListener('focusin', handleFocus);
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Release focus trap
|
|
380
|
+
releaseFocusTrap(): void {
|
|
381
|
+
if (this.container && (this.container as any)._focusTrapCleanup) {
|
|
382
|
+
(this.container as any)._focusTrapCleanup();
|
|
383
|
+
delete (this.container as any)._focusTrapCleanup;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Restore focus to previously focused element
|
|
388
|
+
restoreFocus(): void {
|
|
389
|
+
if (this.currentFocusIndex >= 0 && this.currentFocusIndex < this.focusableElements.length) {
|
|
390
|
+
this.focusableElements[this.currentFocusIndex].focus();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Get keyboard shortcuts for accessibility
|
|
395
|
+
getKeyboardShortcuts(): Record<string, string> {
|
|
396
|
+
return {
|
|
397
|
+
'Navigate Next': 'Tab or Arrow Down/Right',
|
|
398
|
+
'Navigate Previous': 'Shift+Tab or Arrow Up/Left',
|
|
399
|
+
'Navigate First': 'Home',
|
|
400
|
+
'Navigate Last': 'End',
|
|
401
|
+
'Navigate Page Up': 'Page Up',
|
|
402
|
+
'Navigate Page Down': 'Page Down',
|
|
403
|
+
'Select/Activate': 'Enter or Space',
|
|
404
|
+
'Close/Cancel': 'Escape'
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Validate keyboard navigation
|
|
409
|
+
validateNavigation(): boolean {
|
|
410
|
+
if (this.focusableElements.length === 0) {
|
|
411
|
+
console.warn('No focusable elements found in container');
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check if all focusable elements have proper tabindex
|
|
416
|
+
const invalidElements = this.focusableElements.filter(element => {
|
|
417
|
+
const tabindex = element.getAttribute('tabindex');
|
|
418
|
+
return tabindex === null || tabindex === '';
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (invalidElements.length > 0) {
|
|
422
|
+
console.warn('Some focusable elements lack proper tabindex:', invalidElements);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Focus management utilities
|
|
431
|
+
export class FocusManager {
|
|
432
|
+
private previousActiveElement: Element | null;
|
|
433
|
+
private focusableSelectors: string[];
|
|
434
|
+
|
|
435
|
+
constructor(focusableSelectors: string[] = [
|
|
436
|
+
'button:not([disabled])',
|
|
437
|
+
'input:not([disabled])',
|
|
438
|
+
'select:not([disabled])',
|
|
439
|
+
'textarea:not([disabled])',
|
|
440
|
+
'a[href]',
|
|
441
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
442
|
+
'[contenteditable="true"]'
|
|
443
|
+
]) {
|
|
444
|
+
this.previousActiveElement = null;
|
|
445
|
+
this.focusableSelectors = focusableSelectors;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Store current active element
|
|
449
|
+
storeActiveElement(): void {
|
|
450
|
+
this.previousActiveElement = document.activeElement;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Restore previously active element
|
|
454
|
+
restoreActiveElement(): boolean {
|
|
455
|
+
if (this.previousActiveElement && this.previousActiveElement instanceof HTMLElement) {
|
|
456
|
+
this.previousActiveElement.focus();
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Get all focusable elements in a container
|
|
463
|
+
getFocusableElements(container: HTMLElement): HTMLElement[] {
|
|
464
|
+
return Array.from(
|
|
465
|
+
container.querySelectorAll<HTMLElement>(this.focusableSelectors.join(','))
|
|
466
|
+
).filter(element => {
|
|
467
|
+
const style = window.getComputedStyle(element);
|
|
468
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Focus first focusable element in container
|
|
473
|
+
focusFirst(container: HTMLElement): boolean {
|
|
474
|
+
const focusableElements = this.getFocusableElements(container);
|
|
475
|
+
if (focusableElements.length === 0) return false;
|
|
476
|
+
|
|
477
|
+
focusableElements[0].focus();
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Focus last focusable element in container
|
|
482
|
+
focusLast(container: HTMLElement): boolean {
|
|
483
|
+
const focusableElements = this.getFocusableElements(container);
|
|
484
|
+
if (focusableElements.length === 0) return false;
|
|
485
|
+
|
|
486
|
+
focusableElements[focusableElements.length - 1].focus();
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Check if element is focusable
|
|
491
|
+
isFocusable(element: HTMLElement): boolean {
|
|
492
|
+
// Check if element matches focusable selectors
|
|
493
|
+
const isSelectorMatch = this.focusableSelectors.some(selector => {
|
|
494
|
+
try {
|
|
495
|
+
return element.matches(selector);
|
|
496
|
+
} catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (!isSelectorMatch) return false;
|
|
502
|
+
|
|
503
|
+
// Check if element is visible
|
|
504
|
+
const style = window.getComputedStyle(element);
|
|
505
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
506
|
+
|
|
507
|
+
// Check if element is not disabled
|
|
508
|
+
if (element.hasAttribute('disabled')) return false;
|
|
509
|
+
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Get next focusable element
|
|
514
|
+
getNextFocusableElement(currentElement: HTMLElement, container: HTMLElement): HTMLElement | null {
|
|
515
|
+
const focusableElements = this.getFocusableElements(container);
|
|
516
|
+
const currentIndex = focusableElements.indexOf(currentElement);
|
|
517
|
+
|
|
518
|
+
if (currentIndex === -1 || currentIndex === focusableElements.length - 1) {
|
|
519
|
+
return focusableElements[0] || null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return focusableElements[currentIndex + 1];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Get previous focusable element
|
|
526
|
+
getPreviousFocusableElement(currentElement: HTMLElement, container: HTMLElement): HTMLElement | null {
|
|
527
|
+
const focusableElements = this.getFocusableElements(container);
|
|
528
|
+
const currentIndex = focusableElements.indexOf(currentElement);
|
|
529
|
+
|
|
530
|
+
if (currentIndex === -1 || currentIndex === 0) {
|
|
531
|
+
return focusableElements[focusableElements.length - 1] || null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return focusableElements[currentIndex - 1];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Export default instances
|
|
539
|
+
export const keyboardNavigationManager = new KeyboardNavigationManager();
|
|
540
|
+
export const focusManager = new FocusManager();
|
|
541
|
+
|
|
542
|
+
// Export default
|
|
543
|
+
export default {
|
|
544
|
+
KeyboardNavigationManager,
|
|
545
|
+
FocusManager,
|
|
546
|
+
KEY_CODES,
|
|
547
|
+
NAVIGATION_PATTERNS,
|
|
548
|
+
keyboardNavigationManager,
|
|
549
|
+
focusManager
|
|
550
|
+
};
|