@rakeyshgidwani/roger-ui-bank-theme-stan-design 0.2.9 → 0.2.10
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/components/ui/accessibility-demo.esm.js +30 -24
- package/dist/components/ui/accessibility-demo.js +30 -24
- package/dist/components/ui/advanced-component-architecture-demo.esm.js +235 -179
- package/dist/components/ui/advanced-component-architecture-demo.js +235 -179
- package/dist/components/ui/advanced-transition-system-demo.esm.js +110 -64
- package/dist/components/ui/advanced-transition-system-demo.js +110 -64
- package/dist/components/ui/advanced-transition-system.esm.js +166 -122
- package/dist/components/ui/advanced-transition-system.js +166 -122
- package/dist/components/ui/animation/animated-container.esm.js +52 -29
- package/dist/components/ui/animation/animated-container.js +52 -29
- package/dist/components/ui/animation/staggered-container.esm.js +18 -9
- package/dist/components/ui/animation/staggered-container.js +18 -9
- package/dist/components/ui/animation-demo.esm.js +67 -35
- package/dist/components/ui/animation-demo.js +67 -35
- package/dist/components/ui/badge.esm.js +9 -6
- package/dist/components/ui/badge.js +9 -6
- package/dist/components/ui/battery-conscious-animation-demo.esm.js +122 -87
- package/dist/components/ui/battery-conscious-animation-demo.js +122 -87
- package/dist/components/ui/border-radius-shadow-demo.esm.js +23 -12
- package/dist/components/ui/border-radius-shadow-demo.js +23 -12
- package/dist/components/ui/button.esm.js +8 -2
- package/dist/components/ui/button.js +8 -2
- package/dist/components/ui/card.esm.js +33 -8
- package/dist/components/ui/card.js +33 -8
- package/dist/components/ui/checkbox.esm.js +3 -3
- package/dist/components/ui/checkbox.js +3 -3
- package/dist/components/ui/color-preview.esm.js +68 -45
- package/dist/components/ui/color-preview.js +68 -45
- package/dist/components/ui/data-display/chart.esm.js +112 -84
- package/dist/components/ui/data-display/chart.js +112 -84
- package/dist/components/ui/data-display/data-grid-simple.esm.js +1 -1
- package/dist/components/ui/data-display/data-grid-simple.js +1 -1
- package/dist/components/ui/data-display/data-grid.esm.js +80 -67
- package/dist/components/ui/data-display/data-grid.js +80 -67
- package/dist/components/ui/data-display/list.esm.js +53 -45
- package/dist/components/ui/data-display/list.js +53 -45
- package/dist/components/ui/data-display/table.esm.js +62 -54
- package/dist/components/ui/data-display/table.js +62 -54
- package/dist/components/ui/data-display/timeline.esm.js +39 -34
- package/dist/components/ui/data-display/timeline.js +39 -34
- package/dist/components/ui/data-display/tree.esm.js +116 -84
- package/dist/components/ui/data-display/tree.js +116 -84
- package/dist/components/ui/data-display/types.esm.js +389 -364
- package/dist/components/ui/data-display/types.js +389 -364
- package/dist/components/ui/enterprise-mobile-experience-demo.esm.js +120 -70
- package/dist/components/ui/enterprise-mobile-experience-demo.js +120 -70
- package/dist/components/ui/enterprise-mobile-experience.esm.js +124 -73
- package/dist/components/ui/enterprise-mobile-experience.js +124 -73
- package/dist/components/ui/feedback/alert.esm.js +22 -15
- package/dist/components/ui/feedback/alert.js +22 -15
- package/dist/components/ui/feedback/progress.esm.js +47 -24
- package/dist/components/ui/feedback/progress.js +47 -24
- package/dist/components/ui/feedback/skeleton.esm.js +39 -29
- package/dist/components/ui/feedback/skeleton.js +39 -29
- package/dist/components/ui/feedback/toast.esm.js +62 -38
- package/dist/components/ui/feedback/toast.js +62 -38
- package/dist/components/ui/feedback/types.esm.js +83 -83
- package/dist/components/ui/feedback/types.js +83 -83
- package/dist/components/ui/font-preview.esm.js +41 -39
- package/dist/components/ui/font-preview.js +41 -39
- package/dist/components/ui/form-demo.esm.js +150 -113
- package/dist/components/ui/form-demo.js +150 -113
- package/dist/components/ui/hardware-acceleration-demo.esm.js +137 -87
- package/dist/components/ui/hardware-acceleration-demo.js +137 -87
- package/dist/components/ui/input.esm.js +4 -1
- package/dist/components/ui/input.js +4 -1
- package/dist/components/ui/layout-demo.esm.js +81 -56
- package/dist/components/ui/layout-demo.js +81 -56
- package/dist/components/ui/layouts/adaptive-layout.esm.js +27 -8
- package/dist/components/ui/layouts/adaptive-layout.js +27 -8
- package/dist/components/ui/layouts/desktop-layout.esm.js +39 -19
- package/dist/components/ui/layouts/desktop-layout.js +39 -19
- package/dist/components/ui/layouts/mobile-layout.esm.js +19 -9
- package/dist/components/ui/layouts/mobile-layout.js +19 -9
- package/dist/components/ui/layouts/tablet-layout.esm.js +28 -14
- package/dist/components/ui/layouts/tablet-layout.js +28 -14
- package/dist/components/ui/mobile-form-validation.esm.js +120 -87
- package/dist/components/ui/mobile-form-validation.js +120 -87
- package/dist/components/ui/mobile-input-demo.esm.js +19 -13
- package/dist/components/ui/mobile-input-demo.js +19 -13
- package/dist/components/ui/mobile-input.esm.js +185 -120
- package/dist/components/ui/mobile-input.js +185 -120
- package/dist/components/ui/mobile-skeleton-loading-demo.esm.js +128 -111
- package/dist/components/ui/mobile-skeleton-loading-demo.js +128 -111
- package/dist/components/ui/navigation/breadcrumb.esm.js +17 -14
- package/dist/components/ui/navigation/breadcrumb.js +17 -14
- package/dist/components/ui/navigation/index.esm.js +23 -1
- package/dist/components/ui/navigation/index.js +23 -1
- package/dist/components/ui/navigation/menu.esm.js +49 -35
- package/dist/components/ui/navigation/menu.js +49 -35
- package/dist/components/ui/navigation/navigation-demo.esm.js +81 -74
- package/dist/components/ui/navigation/navigation-demo.js +81 -74
- package/dist/components/ui/navigation/pagination.esm.js +62 -50
- package/dist/components/ui/navigation/pagination.js +62 -50
- package/dist/components/ui/navigation/sidebar.esm.js +56 -42
- package/dist/components/ui/navigation/sidebar.js +56 -42
- package/dist/components/ui/navigation/stepper.esm.js +34 -23
- package/dist/components/ui/navigation/stepper.js +34 -23
- package/dist/components/ui/navigation/tabs.esm.js +32 -21
- package/dist/components/ui/navigation/tabs.js +32 -21
- package/dist/components/ui/navigation/types.esm.js +196 -195
- package/dist/components/ui/navigation/types.js +196 -195
- package/dist/components/ui/overlay/backdrop.esm.js +17 -16
- package/dist/components/ui/overlay/backdrop.js +17 -16
- package/dist/components/ui/overlay/focus-manager.esm.js +21 -19
- package/dist/components/ui/overlay/focus-manager.js +21 -19
- package/dist/components/ui/overlay/index.esm.js +22 -2
- package/dist/components/ui/overlay/index.js +22 -2
- package/dist/components/ui/overlay/modal.esm.js +38 -34
- package/dist/components/ui/overlay/modal.js +38 -34
- package/dist/components/ui/overlay/overlay-manager.esm.js +25 -20
- package/dist/components/ui/overlay/overlay-manager.js +25 -20
- package/dist/components/ui/overlay/popover.esm.js +74 -58
- package/dist/components/ui/overlay/popover.js +74 -58
- package/dist/components/ui/overlay/portal.esm.js +7 -7
- package/dist/components/ui/overlay/portal.js +7 -7
- package/dist/components/ui/overlay/tooltip.esm.js +54 -39
- package/dist/components/ui/overlay/tooltip.js +54 -39
- package/dist/components/ui/overlay/types.esm.js +132 -131
- package/dist/components/ui/overlay/types.js +132 -131
- package/dist/components/ui/performance-demo.esm.js +135 -88
- package/dist/components/ui/performance-demo.js +135 -88
- package/dist/components/ui/semantic-input-system-demo.esm.js +117 -80
- package/dist/components/ui/semantic-input-system-demo.js +117 -80
- package/dist/components/ui/theme-customizer.esm.js +84 -52
- package/dist/components/ui/theme-customizer.js +84 -52
- package/dist/components/ui/theme-preview.esm.js +95 -43
- package/dist/components/ui/theme-preview.js +95 -43
- package/dist/components/ui/theme-switcher.esm.js +70 -44
- package/dist/components/ui/theme-switcher.js +70 -44
- package/dist/components/ui/theme-toggle.esm.js +3 -3
- package/dist/components/ui/theme-toggle.js +3 -3
- package/dist/components/ui/token-demo.esm.js +33 -21
- package/dist/components/ui/token-demo.js +33 -21
- package/dist/components/ui/touch-demo.esm.js +102 -73
- package/dist/components/ui/touch-demo.js +102 -73
- package/dist/components/ui/touch-friendly-interface-demo.esm.js +102 -64
- package/dist/components/ui/touch-friendly-interface-demo.js +102 -64
- package/dist/components/ui/touch-friendly-interface.esm.js +85 -61
- package/dist/components/ui/touch-friendly-interface.js +85 -61
- package/dist/hooks/use-accessibility-support.esm.js +115 -85
- package/dist/hooks/use-accessibility-support.js +115 -85
- package/dist/hooks/use-adaptive-layout.esm.js +56 -33
- package/dist/hooks/use-adaptive-layout.js +56 -33
- package/dist/hooks/use-advanced-patterns.esm.js +57 -42
- package/dist/hooks/use-advanced-patterns.js +57 -42
- package/dist/hooks/use-advanced-transition-system.esm.js +112 -71
- package/dist/hooks/use-advanced-transition-system.js +112 -71
- package/dist/hooks/use-animation-profile.esm.js +63 -34
- package/dist/hooks/use-animation-profile.js +63 -34
- package/dist/hooks/use-battery-animations.esm.js +80 -55
- package/dist/hooks/use-battery-animations.js +80 -55
- package/dist/hooks/use-battery-conscious-loading.esm.js +166 -123
- package/dist/hooks/use-battery-conscious-loading.js +166 -123
- package/dist/hooks/use-battery-optimization.esm.js +78 -55
- package/dist/hooks/use-battery-optimization.js +78 -55
- package/dist/hooks/use-battery-status.esm.js +73 -51
- package/dist/hooks/use-battery-status.js +73 -51
- package/dist/hooks/use-component-performance.esm.js +62 -47
- package/dist/hooks/use-component-performance.js +62 -47
- package/dist/hooks/use-device-loading-states.esm.js +152 -109
- package/dist/hooks/use-device-loading-states.js +152 -109
- package/dist/hooks/use-device.esm.js +25 -14
- package/dist/hooks/use-device.js +25 -14
- package/dist/hooks/use-enterprise-mobile-experience.esm.js +137 -88
- package/dist/hooks/use-enterprise-mobile-experience.js +137 -88
- package/dist/hooks/use-form-feedback.esm.js +124 -81
- package/dist/hooks/use-form-feedback.js +124 -81
- package/dist/hooks/use-form-performance.esm.js +127 -92
- package/dist/hooks/use-form-performance.js +127 -92
- package/dist/hooks/use-frame-rate.esm.js +56 -37
- package/dist/hooks/use-frame-rate.js +56 -37
- package/dist/hooks/use-gestures.esm.js +96 -72
- package/dist/hooks/use-gestures.js +96 -72
- package/dist/hooks/use-hardware-acceleration.esm.js +65 -37
- package/dist/hooks/use-hardware-acceleration.js +65 -37
- package/dist/hooks/use-input-accessibility.esm.js +157 -119
- package/dist/hooks/use-input-accessibility.js +157 -119
- package/dist/hooks/use-input-performance.esm.js +139 -104
- package/dist/hooks/use-input-performance.js +139 -104
- package/dist/hooks/use-layout-performance.esm.js +50 -29
- package/dist/hooks/use-layout-performance.js +50 -29
- package/dist/hooks/use-loading-accessibility.esm.js +209 -169
- package/dist/hooks/use-loading-accessibility.js +209 -169
- package/dist/hooks/use-loading-performance.esm.js +117 -93
- package/dist/hooks/use-loading-performance.js +117 -93
- package/dist/hooks/use-memory-usage.esm.js +57 -38
- package/dist/hooks/use-memory-usage.js +57 -38
- package/dist/hooks/use-mobile-form-layout.esm.js +111 -74
- package/dist/hooks/use-mobile-form-layout.js +111 -74
- package/dist/hooks/use-mobile-form-validation.esm.js +211 -144
- package/dist/hooks/use-mobile-form-validation.js +211 -144
- package/dist/hooks/use-mobile-keyboard-optimization.esm.js +154 -113
- package/dist/hooks/use-mobile-keyboard-optimization.js +154 -113
- package/dist/hooks/use-mobile-layout.esm.js +73 -51
- package/dist/hooks/use-mobile-layout.js +73 -51
- package/dist/hooks/use-mobile-optimization.esm.js +72 -44
- package/dist/hooks/use-mobile-optimization.js +72 -44
- package/dist/hooks/use-mobile-skeleton.esm.js +97 -64
- package/dist/hooks/use-mobile-skeleton.js +97 -64
- package/dist/hooks/use-mobile-touch.esm.js +128 -93
- package/dist/hooks/use-mobile-touch.js +128 -93
- package/dist/hooks/use-performance-throttling.esm.js +72 -48
- package/dist/hooks/use-performance-throttling.js +72 -48
- package/dist/hooks/use-performance.esm.js +90 -52
- package/dist/hooks/use-performance.js +90 -52
- package/dist/hooks/use-reusable-architecture.esm.js +94 -65
- package/dist/hooks/use-reusable-architecture.js +94 -65
- package/dist/hooks/use-semantic-input-types.esm.js +166 -124
- package/dist/hooks/use-semantic-input-types.js +166 -124
- package/dist/hooks/use-semantic-input.esm.js +178 -126
- package/dist/hooks/use-semantic-input.js +178 -126
- package/dist/hooks/use-tablet-layout.esm.js +67 -38
- package/dist/hooks/use-tablet-layout.js +67 -38
- package/dist/hooks/use-touch-friendly-input.esm.js +193 -149
- package/dist/hooks/use-touch-friendly-input.js +193 -149
- package/dist/hooks/use-touch-friendly-interface.esm.js +99 -67
- package/dist/hooks/use-touch-friendly-interface.js +99 -67
- package/dist/hooks/use-touch-optimization.esm.js +99 -72
- package/dist/hooks/use-touch-optimization.js +99 -72
- package/dist/index.esm.js +289 -280
- package/dist/index.js +289 -280
- package/dist/lib/utils.esm.js +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/plugins/theme-css-generator.esm.js +104 -55
- package/dist/plugins/theme-css-generator.js +104 -55
- package/dist/provider.esm.js +4 -4
- package/dist/provider.js +4 -4
- package/dist/styles.css +1 -1
- package/dist/theme.esm.js +633 -468
- package/dist/theme.js +633 -468
- package/dist/themes/ThemeContext.esm.js +15 -15
- package/dist/themes/ThemeContext.js +15 -15
- package/dist/themes/ThemeProvider.esm.js +25 -22
- package/dist/themes/ThemeProvider.js +25 -22
- package/dist/themes/accessibility.esm.js +147 -108
- package/dist/themes/accessibility.js +147 -108
- package/dist/themes/aria-patterns.esm.js +198 -162
- package/dist/themes/aria-patterns.js +198 -162
- package/dist/themes/base-themes.esm.js +14 -11
- package/dist/themes/base-themes.js +14 -11
- package/dist/themes/colorManager.esm.js +101 -83
- package/dist/themes/colorManager.js +101 -83
- package/dist/themes/examples/dark-theme.esm.js +133 -103
- package/dist/themes/examples/dark-theme.js +133 -103
- package/dist/themes/examples/minimal-theme.esm.js +83 -61
- package/dist/themes/examples/minimal-theme.js +83 -61
- package/dist/themes/focus-management.esm.js +202 -143
- package/dist/themes/focus-management.js +202 -143
- package/dist/themes/fontLoader.esm.js +28 -19
- package/dist/themes/fontLoader.js +28 -19
- package/dist/themes/high-contrast.esm.js +152 -104
- package/dist/themes/high-contrast.js +152 -104
- package/dist/themes/inheritance.esm.js +35 -27
- package/dist/themes/inheritance.js +35 -27
- package/dist/themes/keyboard-navigation.esm.js +152 -123
- package/dist/themes/keyboard-navigation.js +152 -123
- package/dist/themes/motion-reduction.esm.js +193 -133
- package/dist/themes/motion-reduction.js +193 -133
- package/dist/themes/navigation.esm.js +146 -146
- package/dist/themes/navigation.js +146 -146
- package/dist/themes/screen-reader.esm.js +159 -94
- package/dist/themes/screen-reader.js +159 -94
- package/dist/themes/systemThemeDetector.esm.js +42 -34
- package/dist/themes/systemThemeDetector.js +42 -34
- package/dist/themes/themeCSSUpdater.esm.js +21 -9
- package/dist/themes/themeCSSUpdater.js +21 -9
- package/dist/themes/themePersistence.esm.js +68 -47
- package/dist/themes/themePersistence.js +68 -47
- package/dist/themes/themes/stan-design.esm.js +633 -468
- package/dist/themes/themes/stan-design.js +633 -468
- package/dist/themes/types.esm.js +301 -287
- package/dist/themes/types.js +301 -287
- package/dist/themes/useSystemTheme.esm.js +4 -4
- package/dist/themes/useSystemTheme.js +4 -4
- package/dist/themes/useTheme.esm.js +4 -4
- package/dist/themes/useTheme.js +4 -4
- package/dist/themes/validation.esm.js +128 -77
- package/dist/themes/validation.js +128 -77
- package/dist/tokens/index.esm.js +15 -4
- package/dist/tokens/index.js +15 -4
- package/dist/tokens/tokenExporter.esm.js +87 -61
- package/dist/tokens/tokenExporter.js +87 -61
- package/dist/tokens/tokenGenerator.esm.js +86 -77
- package/dist/tokens/tokenGenerator.js +86 -77
- package/dist/tokens/tokenManager.esm.js +64 -51
- package/dist/tokens/tokenManager.js +64 -51
- package/dist/tokens/tokenValidator.esm.js +193 -147
- package/dist/tokens/tokenValidator.js +193 -147
- package/dist/tokens/types.esm.js +49 -35
- package/dist/tokens/types.js +49 -35
- package/dist/utils/bundle-analyzer.esm.js +83 -65
- package/dist/utils/bundle-analyzer.js +83 -65
- package/dist/utils/bundle-splitting.esm.js +142 -117
- package/dist/utils/bundle-splitting.js +142 -117
- package/dist/utils/lazy-loading.esm.js +132 -106
- package/dist/utils/lazy-loading.js +132 -106
- package/dist/utils/performance-monitor.esm.js +170 -129
- package/dist/utils/performance-monitor.js +170 -129
- package/dist/utils/tree-shaking.esm.js +69 -61
- package/dist/utils/tree-shaking.js +69 -61
- package/package.json +1 -1
- package/src/index.ts +146 -146
|
@@ -2,21 +2,21 @@ import { screenReaderOptimizer } from './screen-reader';
|
|
|
2
2
|
|
|
3
3
|
// Focus management utilities
|
|
4
4
|
export class AccessibilityFocusManager {
|
|
5
|
-
private focusableSelectors];
|
|
6
|
-
private focusableElements];
|
|
7
|
-
private lastFocusedElement;
|
|
8
|
-
private focusTraps, FocusTrap>;
|
|
9
|
-
private focusHistory];
|
|
10
|
-
private maxHistorySize;
|
|
5
|
+
private focusableSelectors: string[];
|
|
6
|
+
private focusableElements: HTMLElement[];
|
|
7
|
+
private lastFocusedElement: HTMLElement | null;
|
|
8
|
+
private focusTraps: Map<string, FocusTrap>;
|
|
9
|
+
private focusHistory: HTMLElement[];
|
|
10
|
+
private maxHistorySize: number;
|
|
11
11
|
|
|
12
12
|
constructor() {
|
|
13
13
|
this.focusableSelectors = [
|
|
14
14
|
'a[href]',
|
|
15
|
-
'button])',
|
|
16
|
-
'input])',
|
|
17
|
-
'select])',
|
|
18
|
-
'textarea])',
|
|
19
|
-
'[tabindex]="-1"])',
|
|
15
|
+
'button:not([disabled])',
|
|
16
|
+
'input:not([disabled])',
|
|
17
|
+
'select:not([disabled])',
|
|
18
|
+
'textarea:not([disabled])',
|
|
19
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
20
20
|
'[contenteditable="true"]',
|
|
21
21
|
'audio[controls]',
|
|
22
22
|
'video[controls]',
|
|
@@ -33,18 +33,22 @@ export class AccessibilityFocusManager {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Initialize focus management
|
|
36
|
-
private initialize()
|
|
36
|
+
private initialize(): void {
|
|
37
|
+
this.updateFocusableElements();
|
|
37
38
|
this.setupEventListeners();
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// Update list of focusable elements
|
|
41
|
-
private updateFocusableElements()
|
|
42
|
+
private updateFocusableElements(): void {
|
|
43
|
+
this.focusableElements = Array.from(
|
|
42
44
|
document.querySelectorAll(this.focusableSelectors.join(','))
|
|
43
45
|
) as HTMLElement[];
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
// Setup event listeners
|
|
47
|
-
private setupEventListeners()
|
|
49
|
+
private setupEventListeners(): void {
|
|
50
|
+
// Track focus changes
|
|
51
|
+
document.addEventListener('focusin', this.handleFocusIn.bind(this));
|
|
48
52
|
document.addEventListener('focusout', this.handleFocusOut.bind(this));
|
|
49
53
|
|
|
50
54
|
// Handle tab key navigation
|
|
@@ -56,12 +60,14 @@ export class AccessibilityFocusManager {
|
|
|
56
60
|
});
|
|
57
61
|
|
|
58
62
|
observer.observe(document.body, {
|
|
59
|
-
childList,
|
|
60
|
-
subtree
|
|
63
|
+
childList: true,
|
|
64
|
+
subtree: true
|
|
65
|
+
});
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
// Handle focus in events
|
|
64
|
-
private handleFocusIn(event)
|
|
69
|
+
private handleFocusIn(event: FocusEvent): void {
|
|
70
|
+
const target = event.target as HTMLElement;
|
|
65
71
|
|
|
66
72
|
if (target && target !== this.lastFocusedElement) {
|
|
67
73
|
// Add to focus history
|
|
@@ -81,16 +87,20 @@ export class AccessibilityFocusManager {
|
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
// Handle focus out events
|
|
84
|
-
private handleFocusOut(_event)
|
|
90
|
+
private handleFocusOut(_event: FocusEvent): void {
|
|
91
|
+
// Focus out handling if needed
|
|
92
|
+
}
|
|
85
93
|
|
|
86
94
|
// Handle key down events
|
|
87
|
-
private handleKeyDown(event
|
|
95
|
+
private handleKeyDown(event: KeyboardEvent): void {
|
|
96
|
+
if (event.key === 'Tab') {
|
|
88
97
|
this.handleTabNavigation(event);
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
// Handle tab navigation
|
|
93
|
-
private handleTabNavigation(event)
|
|
102
|
+
private handleTabNavigation(event: KeyboardEvent): void {
|
|
103
|
+
const isShiftTab = event.shiftKey;
|
|
94
104
|
const currentElement = event.target as HTMLElement;
|
|
95
105
|
|
|
96
106
|
// Check if we're in a focus trap
|
|
@@ -106,7 +116,8 @@ export class AccessibilityFocusManager {
|
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
// Get active focus trap for element
|
|
109
|
-
private getActiveFocusTrap(element)
|
|
119
|
+
private getActiveFocusTrap(element: HTMLElement): FocusTrap | null {
|
|
120
|
+
for (const trap of this.focusTraps.values()) {
|
|
110
121
|
if (trap.isActive && trap.container.contains(element)) {
|
|
111
122
|
return trap;
|
|
112
123
|
}
|
|
@@ -115,36 +126,40 @@ export class AccessibilityFocusManager {
|
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
// Navigate within a focus trap
|
|
118
|
-
private navigateInFocusTrap(trap, isShiftTab)
|
|
129
|
+
private navigateInFocusTrap(trap: FocusTrap, isShiftTab: boolean): void {
|
|
130
|
+
const focusableElements = trap.getFocusableElements();
|
|
119
131
|
if (focusableElements.length === 0) return;
|
|
120
132
|
|
|
121
133
|
const currentIndex = focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
122
|
-
let nextIndex;
|
|
134
|
+
let nextIndex: number;
|
|
123
135
|
|
|
124
136
|
if (isShiftTab) {
|
|
125
|
-
nextIndex = currentIndex > 0 ? currentIndex - 1 ;
|
|
137
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1;
|
|
126
138
|
} else {
|
|
127
|
-
nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 ;
|
|
139
|
+
nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
focusableElements[nextIndex]?.focus();
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
// Navigate focusable elements
|
|
134
|
-
private navigateFocusableElements(isShiftTab)
|
|
135
|
-
|
|
146
|
+
private navigateFocusableElements(isShiftTab: boolean): void {
|
|
147
|
+
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
148
|
+
let nextIndex: number;
|
|
136
149
|
|
|
137
150
|
if (isShiftTab) {
|
|
138
|
-
nextIndex = currentIndex > 0 ? currentIndex - 1 ;
|
|
151
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : this.focusableElements.length - 1;
|
|
139
152
|
} else {
|
|
140
|
-
nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 ;
|
|
153
|
+
nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 : 0;
|
|
141
154
|
}
|
|
142
155
|
|
|
143
156
|
this.focusableElements[nextIndex]?.focus();
|
|
144
157
|
}
|
|
145
158
|
|
|
146
159
|
// Add element to focus history
|
|
147
|
-
private addToFocusHistory(element)
|
|
160
|
+
private addToFocusHistory(element: HTMLElement): void {
|
|
161
|
+
// Remove if already exists
|
|
162
|
+
this.focusHistory = this.focusHistory.filter(el => el !== element);
|
|
148
163
|
|
|
149
164
|
// Add to beginning
|
|
150
165
|
this.focusHistory.unshift(element);
|
|
@@ -156,7 +171,8 @@ export class AccessibilityFocusManager {
|
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
// Get element description for screen readers
|
|
159
|
-
private getElementDescription(element)
|
|
174
|
+
private getElementDescription(element: HTMLElement): string {
|
|
175
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
160
176
|
if (ariaLabel) return ariaLabel;
|
|
161
177
|
|
|
162
178
|
const ariaLabelledBy = element.getAttribute('aria-labelledby');
|
|
@@ -175,7 +191,9 @@ export class AccessibilityFocusManager {
|
|
|
175
191
|
}
|
|
176
192
|
|
|
177
193
|
// Get element context for screen readers
|
|
178
|
-
private getElementContext(element)
|
|
194
|
+
private getElementContext(element: HTMLElement): string | undefined {
|
|
195
|
+
// Find nearest landmark or section
|
|
196
|
+
const landmark = element.closest('[role], section, article, aside, nav, header, footer, main');
|
|
179
197
|
if (landmark) {
|
|
180
198
|
const role = landmark.getAttribute('role');
|
|
181
199
|
const ariaLabel = landmark.getAttribute('aria-label');
|
|
@@ -185,13 +203,15 @@ export class AccessibilityFocusManager {
|
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
// Create focus trap
|
|
188
|
-
createFocusTrap(container, options= {})
|
|
206
|
+
createFocusTrap(container: HTMLElement, options: FocusTrapOptions = {}): FocusTrap {
|
|
207
|
+
const trap = new FocusTrap(container, options);
|
|
189
208
|
this.focusTraps.set(trap.id, trap);
|
|
190
209
|
return trap;
|
|
191
210
|
}
|
|
192
211
|
|
|
193
212
|
// Remove focus trap
|
|
194
|
-
removeFocusTrap(trapId)
|
|
213
|
+
removeFocusTrap(trapId: string): void {
|
|
214
|
+
const trap = this.focusTraps.get(trapId);
|
|
195
215
|
if (trap) {
|
|
196
216
|
trap.deactivate();
|
|
197
217
|
this.focusTraps.delete(trapId);
|
|
@@ -199,104 +219,119 @@ export class AccessibilityFocusManager {
|
|
|
199
219
|
}
|
|
200
220
|
|
|
201
221
|
// Focus first focusable element
|
|
202
|
-
focusFirst()
|
|
222
|
+
focusFirst(): void {
|
|
223
|
+
const firstElement = this.focusableElements[0];
|
|
203
224
|
if (firstElement) {
|
|
204
225
|
firstElement.focus();
|
|
205
226
|
}
|
|
206
227
|
}
|
|
207
228
|
|
|
208
229
|
// Focus last focusable element
|
|
209
|
-
focusLast()
|
|
230
|
+
focusLast(): void {
|
|
231
|
+
const lastElement = this.focusableElements[this.focusableElements.length - 1];
|
|
210
232
|
if (lastElement) {
|
|
211
233
|
lastElement.focus();
|
|
212
234
|
}
|
|
213
235
|
}
|
|
214
236
|
|
|
215
237
|
// Focus next element
|
|
216
|
-
focusNext()
|
|
217
|
-
const
|
|
238
|
+
focusNext(): void {
|
|
239
|
+
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
240
|
+
const nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 : 0;
|
|
218
241
|
this.focusableElements[nextIndex]?.focus();
|
|
219
242
|
}
|
|
220
243
|
|
|
221
244
|
// Focus previous element
|
|
222
|
-
focusPrevious()
|
|
223
|
-
const
|
|
245
|
+
focusPrevious(): void {
|
|
246
|
+
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
247
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : this.focusableElements.length - 1;
|
|
224
248
|
this.focusableElements[prevIndex]?.focus();
|
|
225
249
|
}
|
|
226
250
|
|
|
227
251
|
// Focus element by index
|
|
228
|
-
focusByIndex(index)
|
|
252
|
+
focusByIndex(index: number): void {
|
|
253
|
+
if (index >= 0 && index < this.focusableElements.length) {
|
|
229
254
|
this.focusableElements[index].focus();
|
|
230
255
|
}
|
|
231
256
|
}
|
|
232
257
|
|
|
233
258
|
// Focus element by ID
|
|
234
|
-
focusById(id)
|
|
259
|
+
focusById(id: string): void {
|
|
260
|
+
const element = document.getElementById(id);
|
|
235
261
|
if (element && this.focusableElements.includes(element)) {
|
|
236
262
|
element.focus();
|
|
237
263
|
}
|
|
238
264
|
}
|
|
239
265
|
|
|
240
266
|
// Focus element by data attribute
|
|
241
|
-
focusByDataAttribute(attribute, value
|
|
267
|
+
focusByDataAttribute(attribute: string, value: string): void {
|
|
268
|
+
const element = document.querySelector(`[data-${attribute}="${value}"]`) as HTMLElement;
|
|
242
269
|
if (element && this.focusableElements.includes(element)) {
|
|
243
270
|
element.focus();
|
|
244
271
|
}
|
|
245
272
|
}
|
|
246
273
|
|
|
247
274
|
// Restore previous focus
|
|
248
|
-
restoreFocus()
|
|
275
|
+
restoreFocus(): void {
|
|
276
|
+
if (this.lastFocusedElement && this.lastFocusedElement.offsetParent !== null) {
|
|
249
277
|
this.lastFocusedElement.focus();
|
|
250
278
|
}
|
|
251
279
|
}
|
|
252
280
|
|
|
253
281
|
// Get focus history
|
|
254
|
-
getFocusHistory()] {
|
|
282
|
+
getFocusHistory(): HTMLElement[] {
|
|
255
283
|
return [...this.focusHistory];
|
|
256
284
|
}
|
|
257
285
|
|
|
258
286
|
// Clear focus history
|
|
259
|
-
clearFocusHistory()
|
|
287
|
+
clearFocusHistory(): void {
|
|
288
|
+
this.focusHistory = [];
|
|
260
289
|
}
|
|
261
290
|
|
|
262
291
|
// Get current focus information
|
|
263
|
-
getCurrentFocusInfo()
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
292
|
+
getCurrentFocusInfo(): {
|
|
293
|
+
element: HTMLElement | null;
|
|
294
|
+
index: number;
|
|
295
|
+
total: number;
|
|
296
|
+
context: string | undefined;
|
|
267
297
|
} {
|
|
268
298
|
const currentElement = document.activeElement as HTMLElement;
|
|
269
299
|
const index = this.focusableElements.indexOf(currentElement);
|
|
270
300
|
|
|
271
301
|
return {
|
|
272
|
-
element,
|
|
273
|
-
index
|
|
274
|
-
total,
|
|
275
|
-
context)
|
|
302
|
+
element: currentElement,
|
|
303
|
+
index: index >= 0 ? index : -1,
|
|
304
|
+
total: this.focusableElements.length,
|
|
305
|
+
context: currentElement ? this.getElementContext(currentElement) : undefined
|
|
306
|
+
};
|
|
276
307
|
}
|
|
277
308
|
|
|
278
309
|
// Check if element is focusable
|
|
279
|
-
isFocusable(element)
|
|
310
|
+
isFocusable(element: HTMLElement): boolean {
|
|
311
|
+
return this.focusableElements.includes(element);
|
|
280
312
|
}
|
|
281
313
|
|
|
282
314
|
// Get all focusable elements
|
|
283
|
-
getAllFocusableElements()] {
|
|
315
|
+
getAllFocusableElements(): HTMLElement[] {
|
|
284
316
|
return [...this.focusableElements];
|
|
285
317
|
}
|
|
286
318
|
|
|
287
319
|
// Get focusable elements within container
|
|
288
|
-
getFocusableElementsInContainer(container)] {
|
|
320
|
+
getFocusableElementsInContainer(container: HTMLElement): HTMLElement[] {
|
|
289
321
|
return this.focusableElements.filter(element =>
|
|
290
322
|
container.contains(element)
|
|
291
323
|
);
|
|
292
324
|
}
|
|
293
325
|
|
|
294
326
|
// Update focusable elements manually
|
|
295
|
-
refreshFocusableElements()
|
|
327
|
+
refreshFocusableElements(): void {
|
|
328
|
+
this.updateFocusableElements();
|
|
296
329
|
}
|
|
297
330
|
|
|
298
331
|
// Destroy focus manager
|
|
299
|
-
destroy()
|
|
332
|
+
destroy(): void {
|
|
333
|
+
// Remove all focus traps
|
|
334
|
+
this.focusTraps.forEach(trap => trap.deactivate());
|
|
300
335
|
this.focusTraps.clear();
|
|
301
336
|
|
|
302
337
|
// Clear focus history
|
|
@@ -311,41 +346,42 @@ export class AccessibilityFocusManager {
|
|
|
311
346
|
|
|
312
347
|
// Focus trap options
|
|
313
348
|
export interface FocusTrapOptions {
|
|
314
|
-
autoFocus
|
|
315
|
-
returnFocus
|
|
316
|
-
escapeDeactivates
|
|
317
|
-
clickOutsideDeactivates
|
|
318
|
-
allowOutsideClick
|
|
319
|
-
fallbackFocus
|
|
349
|
+
autoFocus?: boolean;
|
|
350
|
+
returnFocus?: boolean;
|
|
351
|
+
escapeDeactivates?: boolean;
|
|
352
|
+
clickOutsideDeactivates?: boolean;
|
|
353
|
+
allowOutsideClick?: boolean;
|
|
354
|
+
fallbackFocus?: HTMLElement;
|
|
320
355
|
}
|
|
321
356
|
|
|
322
357
|
// Focus trap class
|
|
323
358
|
export class FocusTrap {
|
|
324
|
-
public readonly id;
|
|
325
|
-
public readonly container;
|
|
326
|
-
public readonly options;
|
|
327
|
-
public isActive= false;
|
|
359
|
+
public readonly id: string;
|
|
360
|
+
public readonly container: HTMLElement;
|
|
361
|
+
public readonly options: FocusTrapOptions;
|
|
362
|
+
public isActive: boolean = false;
|
|
328
363
|
|
|
329
|
-
private returnFocusElement= null;
|
|
330
|
-
private focusableElements] = [];
|
|
331
|
-
private eventListeners; handler) => void }> = [];
|
|
364
|
+
private returnFocusElement: HTMLElement | null = null;
|
|
365
|
+
private focusableElements: HTMLElement[] = [];
|
|
366
|
+
private eventListeners: Array<{ event: string; handler: (event: any) => void }> = [];
|
|
332
367
|
|
|
333
|
-
constructor(container, options= {}) {
|
|
368
|
+
constructor(container: HTMLElement, options: FocusTrapOptions = {}) {
|
|
334
369
|
this.id = `focus-trap-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
335
370
|
this.container = container;
|
|
336
371
|
this.options = {
|
|
337
|
-
autoFocus,
|
|
338
|
-
returnFocus,
|
|
339
|
-
escapeDeactivates,
|
|
340
|
-
clickOutsideDeactivates,
|
|
341
|
-
allowOutsideClick,
|
|
342
|
-
fallbackFocus,
|
|
372
|
+
autoFocus: true,
|
|
373
|
+
returnFocus: true,
|
|
374
|
+
escapeDeactivates: true,
|
|
375
|
+
clickOutsideDeactivates: false,
|
|
376
|
+
allowOutsideClick: false,
|
|
377
|
+
fallbackFocus: container,
|
|
343
378
|
...options
|
|
344
379
|
};
|
|
345
380
|
}
|
|
346
381
|
|
|
347
382
|
// Activate focus trap
|
|
348
|
-
activate()
|
|
383
|
+
activate(): void {
|
|
384
|
+
if (this.isActive) return;
|
|
349
385
|
|
|
350
386
|
// Store current focus for return
|
|
351
387
|
if (this.options.returnFocus) {
|
|
@@ -369,7 +405,8 @@ export class FocusTrap {
|
|
|
369
405
|
}
|
|
370
406
|
|
|
371
407
|
// Deactivate focus trap
|
|
372
|
-
deactivate()
|
|
408
|
+
deactivate(): void {
|
|
409
|
+
if (!this.isActive) return;
|
|
373
410
|
|
|
374
411
|
// Remove event listeners
|
|
375
412
|
this.removeEventListeners();
|
|
@@ -383,14 +420,14 @@ export class FocusTrap {
|
|
|
383
420
|
}
|
|
384
421
|
|
|
385
422
|
// Get focusable elements within container
|
|
386
|
-
getFocusableElements()] {
|
|
423
|
+
getFocusableElements(): HTMLElement[] {
|
|
387
424
|
const focusableSelectors = [
|
|
388
425
|
'a[href]',
|
|
389
|
-
'button])',
|
|
390
|
-
'input])',
|
|
391
|
-
'select])',
|
|
392
|
-
'textarea])',
|
|
393
|
-
'[tabindex]="-1"])',
|
|
426
|
+
'button:not([disabled])',
|
|
427
|
+
'input:not([disabled])',
|
|
428
|
+
'select:not([disabled])',
|
|
429
|
+
'textarea:not([disabled])',
|
|
430
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
394
431
|
'[contenteditable="true"]'
|
|
395
432
|
];
|
|
396
433
|
|
|
@@ -400,31 +437,33 @@ export class FocusTrap {
|
|
|
400
437
|
}
|
|
401
438
|
|
|
402
439
|
// Setup event listeners
|
|
403
|
-
private setupEventListeners()
|
|
404
|
-
|
|
440
|
+
private setupEventListeners(): void {
|
|
441
|
+
// Handle escape key
|
|
442
|
+
if (this.options.escapeDeactivates) {
|
|
443
|
+
const escapeHandler = (event: KeyboardEvent) => {
|
|
405
444
|
if (event.key === 'Escape') {
|
|
406
445
|
this.deactivate();
|
|
407
446
|
}
|
|
408
447
|
};
|
|
409
448
|
|
|
410
449
|
document.addEventListener('keydown', escapeHandler);
|
|
411
|
-
this.eventListeners.push({ event, handler});
|
|
450
|
+
this.eventListeners.push({ event: 'keydown', handler: escapeHandler });
|
|
412
451
|
}
|
|
413
452
|
|
|
414
453
|
// Handle click outside
|
|
415
454
|
if (this.options.clickOutsideDeactivates) {
|
|
416
|
-
const clickHandler = (event) => {
|
|
455
|
+
const clickHandler = (event: MouseEvent) => {
|
|
417
456
|
if (!this.container.contains(event.target as Node)) {
|
|
418
457
|
this.deactivate();
|
|
419
458
|
}
|
|
420
459
|
};
|
|
421
460
|
|
|
422
461
|
document.addEventListener('click', clickHandler);
|
|
423
|
-
this.eventListeners.push({ event, handler});
|
|
462
|
+
this.eventListeners.push({ event: 'click', handler: clickHandler });
|
|
424
463
|
}
|
|
425
464
|
|
|
426
465
|
// Handle tab key within trap
|
|
427
|
-
const tabHandler = (event) => {
|
|
466
|
+
const tabHandler = (event: KeyboardEvent) => {
|
|
428
467
|
if (event.key === 'Tab') {
|
|
429
468
|
event.preventDefault();
|
|
430
469
|
this.handleTabNavigation(event.shiftKey);
|
|
@@ -432,11 +471,12 @@ export class FocusTrap {
|
|
|
432
471
|
};
|
|
433
472
|
|
|
434
473
|
this.container.addEventListener('keydown', tabHandler);
|
|
435
|
-
this.eventListeners.push({ event, handler});
|
|
474
|
+
this.eventListeners.push({ event: 'keydown', handler: tabHandler });
|
|
436
475
|
}
|
|
437
476
|
|
|
438
477
|
// Remove event listeners
|
|
439
|
-
private removeEventListeners()
|
|
478
|
+
private removeEventListeners(): void {
|
|
479
|
+
this.eventListeners.forEach(({ event, handler }) => {
|
|
440
480
|
if (event === 'keydown' && handler.toString().includes('tabHandler')) {
|
|
441
481
|
this.container.removeEventListener(event, handler);
|
|
442
482
|
} else {
|
|
@@ -448,73 +488,81 @@ export class FocusTrap {
|
|
|
448
488
|
}
|
|
449
489
|
|
|
450
490
|
// Handle tab navigation within trap
|
|
451
|
-
private handleTabNavigation(isShiftTab
|
|
491
|
+
private handleTabNavigation(isShiftTab: boolean): void {
|
|
492
|
+
if (this.focusableElements.length === 0) return;
|
|
452
493
|
|
|
453
494
|
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
454
|
-
let nextIndex;
|
|
495
|
+
let nextIndex: number;
|
|
455
496
|
|
|
456
497
|
if (isShiftTab) {
|
|
457
|
-
nextIndex = currentIndex > 0 ? currentIndex - 1 ;
|
|
498
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : this.focusableElements.length - 1;
|
|
458
499
|
} else {
|
|
459
|
-
nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 ;
|
|
500
|
+
nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 : 0;
|
|
460
501
|
}
|
|
461
502
|
|
|
462
503
|
this.focusableElements[nextIndex]?.focus();
|
|
463
504
|
}
|
|
464
505
|
|
|
465
506
|
// Focus first element in trap
|
|
466
|
-
focusFirst()
|
|
507
|
+
focusFirst(): void {
|
|
508
|
+
if (this.focusableElements.length > 0) {
|
|
467
509
|
this.focusableElements[0].focus();
|
|
468
510
|
}
|
|
469
511
|
}
|
|
470
512
|
|
|
471
513
|
// Focus last element in trap
|
|
472
|
-
focusLast()
|
|
514
|
+
focusLast(): void {
|
|
515
|
+
if (this.focusableElements.length > 0) {
|
|
473
516
|
this.focusableElements[this.focusableElements.length - 1].focus();
|
|
474
517
|
}
|
|
475
518
|
}
|
|
476
519
|
|
|
477
520
|
// Focus next element in trap
|
|
478
|
-
focusNext()
|
|
521
|
+
focusNext(): void {
|
|
522
|
+
if (this.focusableElements.length === 0) return;
|
|
479
523
|
|
|
480
524
|
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
481
|
-
const nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 ;
|
|
525
|
+
const nextIndex = currentIndex < this.focusableElements.length - 1 ? currentIndex + 1 : 0;
|
|
482
526
|
this.focusableElements[nextIndex]?.focus();
|
|
483
527
|
}
|
|
484
528
|
|
|
485
529
|
// Focus previous element in trap
|
|
486
|
-
focusPrevious()
|
|
530
|
+
focusPrevious(): void {
|
|
531
|
+
if (this.focusableElements.length === 0) return;
|
|
487
532
|
|
|
488
533
|
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
489
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 ;
|
|
534
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : this.focusableElements.length - 1;
|
|
490
535
|
this.focusableElements[prevIndex]?.focus();
|
|
491
536
|
}
|
|
492
537
|
|
|
493
538
|
// Get trap information
|
|
494
|
-
getInfo()
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
539
|
+
getInfo(): {
|
|
540
|
+
id: string;
|
|
541
|
+
isActive: boolean;
|
|
542
|
+
focusableElementsCount: number;
|
|
543
|
+
currentFocusIndex: number;
|
|
544
|
+
container: HTMLElement;
|
|
499
545
|
} {
|
|
500
546
|
const currentIndex = this.focusableElements.indexOf(document.activeElement as HTMLElement);
|
|
501
547
|
|
|
502
548
|
return {
|
|
503
|
-
id,
|
|
504
|
-
isActive,
|
|
505
|
-
focusableElementsCount,
|
|
506
|
-
currentFocusIndex
|
|
507
|
-
container
|
|
549
|
+
id: this.id,
|
|
550
|
+
isActive: this.isActive,
|
|
551
|
+
focusableElementsCount: this.focusableElements.length,
|
|
552
|
+
currentFocusIndex: currentIndex >= 0 ? currentIndex : -1,
|
|
553
|
+
container: this.container
|
|
554
|
+
};
|
|
508
555
|
}
|
|
509
556
|
}
|
|
510
557
|
|
|
511
558
|
// Focus restoration utilities
|
|
512
559
|
export class FocusRestoration {
|
|
513
|
-
private focusStack] = [];
|
|
514
|
-
private maxStackSize= 20;
|
|
560
|
+
private focusStack: HTMLElement[] = [];
|
|
561
|
+
private maxStackSize: number = 20;
|
|
515
562
|
|
|
516
563
|
// Push current focus to stack
|
|
517
|
-
pushFocus()
|
|
564
|
+
pushFocus(): void {
|
|
565
|
+
const currentElement = document.activeElement as HTMLElement;
|
|
518
566
|
if (currentElement && currentElement !== document.body) {
|
|
519
567
|
this.focusStack.push(currentElement);
|
|
520
568
|
|
|
@@ -526,7 +574,8 @@ export class FocusRestoration {
|
|
|
526
574
|
}
|
|
527
575
|
|
|
528
576
|
// Pop and restore focus from stack
|
|
529
|
-
popFocus()
|
|
577
|
+
popFocus(): boolean {
|
|
578
|
+
const element = this.focusStack.pop();
|
|
530
579
|
if (element && element.offsetParent !== null) {
|
|
531
580
|
element.focus();
|
|
532
581
|
return true;
|
|
@@ -535,28 +584,33 @@ export class FocusRestoration {
|
|
|
535
584
|
}
|
|
536
585
|
|
|
537
586
|
// Peek at top of focus stack
|
|
538
|
-
peekFocus()
|
|
587
|
+
peekFocus(): HTMLElement | undefined {
|
|
588
|
+
return this.focusStack[this.focusStack.length - 1];
|
|
539
589
|
}
|
|
540
590
|
|
|
541
591
|
// Clear focus stack
|
|
542
|
-
clear()
|
|
592
|
+
clear(): void {
|
|
593
|
+
this.focusStack = [];
|
|
543
594
|
}
|
|
544
595
|
|
|
545
596
|
// Get stack size
|
|
546
|
-
getSize()
|
|
597
|
+
getSize(): number {
|
|
598
|
+
return this.focusStack.length;
|
|
547
599
|
}
|
|
548
600
|
|
|
549
601
|
// Check if stack is empty
|
|
550
|
-
isEmpty()
|
|
602
|
+
isEmpty(): boolean {
|
|
603
|
+
return this.focusStack.length === 0;
|
|
551
604
|
}
|
|
552
605
|
}
|
|
553
606
|
|
|
554
607
|
// Focus navigation utilities
|
|
555
608
|
export class FocusNavigation {
|
|
556
609
|
// Navigate to next heading
|
|
557
|
-
static focusNextHeading()
|
|
610
|
+
static focusNextHeading(): void {
|
|
611
|
+
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
558
612
|
const currentIndex = headings.indexOf(document.activeElement as HTMLElement);
|
|
559
|
-
const nextIndex = currentIndex < headings.length - 1 ? currentIndex + 1 ;
|
|
613
|
+
const nextIndex = currentIndex < headings.length - 1 ? currentIndex + 1 : 0;
|
|
560
614
|
|
|
561
615
|
if (headings[nextIndex]) {
|
|
562
616
|
(headings[nextIndex] as HTMLElement).focus();
|
|
@@ -564,9 +618,10 @@ export class FocusNavigation {
|
|
|
564
618
|
}
|
|
565
619
|
|
|
566
620
|
// Navigate to previous heading
|
|
567
|
-
static focusPreviousHeading()
|
|
621
|
+
static focusPreviousHeading(): void {
|
|
622
|
+
const headings = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
|
|
568
623
|
const currentIndex = headings.indexOf(document.activeElement as HTMLElement);
|
|
569
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 ;
|
|
624
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : headings.length - 1;
|
|
570
625
|
|
|
571
626
|
if (headings[prevIndex]) {
|
|
572
627
|
(headings[prevIndex] as HTMLElement).focus();
|
|
@@ -574,9 +629,10 @@ export class FocusNavigation {
|
|
|
574
629
|
}
|
|
575
630
|
|
|
576
631
|
// Navigate to next landmark
|
|
577
|
-
static focusNextLandmark()
|
|
632
|
+
static focusNextLandmark(): void {
|
|
633
|
+
const landmarks = Array.from(document.querySelectorAll('[role="banner"], [role="main"], [role="navigation"], [role="complementary"], [role="contentinfo"]'));
|
|
578
634
|
const currentIndex = landmarks.indexOf(document.activeElement as HTMLElement);
|
|
579
|
-
const nextIndex = currentIndex < landmarks.length - 1 ? currentIndex + 1 ;
|
|
635
|
+
const nextIndex = currentIndex < landmarks.length - 1 ? currentIndex + 1 : 0;
|
|
580
636
|
|
|
581
637
|
if (landmarks[nextIndex]) {
|
|
582
638
|
(landmarks[nextIndex] as HTMLElement).focus();
|
|
@@ -584,9 +640,10 @@ export class FocusNavigation {
|
|
|
584
640
|
}
|
|
585
641
|
|
|
586
642
|
// Navigate to previous landmark
|
|
587
|
-
static focusPreviousLandmark()
|
|
643
|
+
static focusPreviousLandmark(): void {
|
|
644
|
+
const landmarks = Array.from(document.querySelectorAll('[role="banner"], [role="main"], [role="navigation"], [role="complementary"], [role="contentinfo"]'));
|
|
588
645
|
const currentIndex = landmarks.indexOf(document.activeElement as HTMLElement);
|
|
589
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 ;
|
|
646
|
+
const prevIndex = currentIndex > 0 ? currentIndex - 1 : landmarks.length - 1;
|
|
590
647
|
|
|
591
648
|
if (landmarks[prevIndex]) {
|
|
592
649
|
(landmarks[prevIndex] as HTMLElement).focus();
|
|
@@ -594,13 +651,14 @@ export class FocusNavigation {
|
|
|
594
651
|
}
|
|
595
652
|
|
|
596
653
|
// Focus first focusable element in container
|
|
597
|
-
static focusFirstInContainer(container)
|
|
654
|
+
static focusFirstInContainer(container: HTMLElement): void {
|
|
655
|
+
const focusableSelectors = [
|
|
598
656
|
'a[href]',
|
|
599
|
-
'button])',
|
|
600
|
-
'input])',
|
|
601
|
-
'select])',
|
|
602
|
-
'textarea])',
|
|
603
|
-
'[tabindex]="-1"])'
|
|
657
|
+
'button:not([disabled])',
|
|
658
|
+
'input:not([disabled])',
|
|
659
|
+
'select:not([disabled])',
|
|
660
|
+
'textarea:not([disabled])',
|
|
661
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
604
662
|
];
|
|
605
663
|
|
|
606
664
|
const firstElement = container.querySelector(focusableSelectors.join(','));
|
|
@@ -610,13 +668,14 @@ export class FocusNavigation {
|
|
|
610
668
|
}
|
|
611
669
|
|
|
612
670
|
// Focus last focusable element in container
|
|
613
|
-
static focusLastInContainer(container)
|
|
671
|
+
static focusLastInContainer(container: HTMLElement): void {
|
|
672
|
+
const focusableSelectors = [
|
|
614
673
|
'a[href]',
|
|
615
|
-
'button])',
|
|
616
|
-
'input])',
|
|
617
|
-
'select])',
|
|
618
|
-
'textarea])',
|
|
619
|
-
'[tabindex]="-1"])'
|
|
674
|
+
'button:not([disabled])',
|
|
675
|
+
'input:not([disabled])',
|
|
676
|
+
'select:not([disabled])',
|
|
677
|
+
'textarea:not([disabled])',
|
|
678
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
620
679
|
];
|
|
621
680
|
|
|
622
681
|
const elements = Array.from(container.querySelectorAll(focusableSelectors.join(',')));
|