Skip to content
This repository was archived by the owner on May 20, 2023. It is now read-only.

Commit ff9326d

Browse files
cissyshinshahan
authored andcommitted
Add a focus indicator utility to help improve a11y debugging.
PiperOrigin-RevId: 225080476
1 parent 75ae063 commit ff9326d

File tree

5 files changed

+172
-6
lines changed

5 files changed

+172
-6
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:html';
7+
8+
import 'package:angular/di.dart';
9+
10+
const focusIndicatorProviders = [
11+
FactoryProvider(
12+
FocusIndicatorController, createFocusIndicatorControllerIfNotAvailable)
13+
];
14+
15+
@Injectable()
16+
FocusIndicatorController createFocusIndicatorControllerIfNotAvailable(
17+
@Optional() @SkipSelf() FocusIndicatorController controller) =>
18+
controller ?? FocusIndicatorController();
19+
20+
/// Utility that attaches an a focus indicator to the page when enabled.
21+
///
22+
/// Only used to improve a11y debugging experience. DO NOT USE IN PRODUCTION!
23+
class FocusIndicatorController {
24+
Element _focusIndicator;
25+
int _repositionLoopId;
26+
27+
Element _activeElement;
28+
Element get activeElement => _activeElement;
29+
30+
bool _enabled = false;
31+
bool get enabled => _enabled;
32+
33+
set enabled(bool value) {
34+
if (_enabled == value) return;
35+
_enabled = value;
36+
if (_enabled) {
37+
_turnOnKeyNavMode();
38+
} else {
39+
_turnOffKeyNavMode();
40+
}
41+
}
42+
43+
void _turnOnKeyNavMode() {
44+
window.addEventListener('focus', _onFocus, true);
45+
window.addEventListener('blur', _onBlur, true);
46+
47+
_activeElement = document.activeElement;
48+
49+
_focusIndicator = document.createElement('div');
50+
_focusIndicator.id = 'acx-focus-indicator';
51+
_focusIndicator.style.position = 'fixed';
52+
_focusIndicator.style.zIndex = '9999';
53+
_focusIndicator.style.outline = '2px solid #ff9800';
54+
_focusIndicator.style.pointerEvents = 'none';
55+
document.body.append(_focusIndicator);
56+
57+
_startRepositionLoop();
58+
}
59+
60+
void _turnOffKeyNavMode() {
61+
window.removeEventListener('focus', _onFocus, true);
62+
window.removeEventListener('blur', _onBlur, true);
63+
64+
_activeElement = null;
65+
66+
if (_focusIndicator != null) {
67+
_focusIndicator.remove();
68+
_focusIndicator = null;
69+
}
70+
71+
_cancelRepositionLoop();
72+
}
73+
74+
void _onFocus(Event event) {
75+
_updateActiveElement(event);
76+
}
77+
78+
void _onBlur(Event event) {
79+
Timer.run(() {
80+
_updateActiveElement(event);
81+
});
82+
}
83+
84+
void _updateActiveElement(Event event) {
85+
if (!_enabled || _activeElement == document.activeElement) return;
86+
87+
if (_activeElement != null) {
88+
_activeElement.style.outline = '';
89+
if (_activeElement.getAttribute('style')?.isEmpty == true) {
90+
_activeElement.attributes.remove('style');
91+
}
92+
}
93+
94+
_activeElement = document.activeElement;
95+
96+
window.console.groupCollapsed('Active element '
97+
'[${_activeElement.tagName.toLowerCase()}] '
98+
'after "${event.type}"');
99+
window.console.log(_activeElement);
100+
window.console.log(event);
101+
window.console.groupEnd();
102+
103+
_activeElement.style.outline = 'none';
104+
}
105+
106+
void _startRepositionLoop() {
107+
_repositionLoopId = window.requestAnimationFrame(_reposition);
108+
}
109+
110+
void _cancelRepositionLoop() {
111+
if (_repositionLoopId != null) {
112+
window.cancelAnimationFrame(_repositionLoopId);
113+
_repositionLoopId = null;
114+
}
115+
}
116+
117+
void _reposition(_) {
118+
var rect = _activeElement.getBoundingClientRect();
119+
_focusIndicator.style.top = '${rect.top}px';
120+
_focusIndicator.style.left = '${rect.left}px';
121+
_focusIndicator.style.width = '${rect.width}px';
122+
_focusIndicator.style.height = '${rect.height}px';
123+
124+
_startRepositionLoop();
125+
}
126+
}

angular_gallery/lib/builder/template/gallery.dart.mustache

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import 'package:angular_components/content/deferred_content.dart';
88
import 'package:angular_components/focus/focus.dart';
99
import 'package:angular_components/highlighted_text/highlighted_value.dart';
1010
import 'package:angular_components/material_button/material_button.dart';
11+
import 'package:angular_components/material_checkbox/material_checkbox.dart';
1112
import 'package:angular_components/material_icon/material_icon.dart';
1213
import 'package:angular_components/material_list/material_list.dart';
1314
import 'package:angular_components/material_list/material_list_item.dart';
1415
import 'package:angular_components/material_select/material_select_searchbox.dart';
1516
import 'package:angular_components/model/selection/string_selection_options.dart';
17+
import 'package:angular_components/model/a11y/focus_indicator_controller.dart';
1618
import 'package:angular_components/model/ui/has_renderer.dart';
1719
import 'package:angular_components/model/ui/highlight_assistant.dart';
1820
import 'package:angular_components/model/ui/highlight_provider.dart';
@@ -28,6 +30,7 @@ import 'gallery_route_library.dart';
2830
DeferredContentDirective,
2931
HighlightedValueComponent,
3032
MaterialButtonComponent,
33+
MaterialCheckboxComponent,
3134
MaterialIconComponent,
3235
MaterialListComponent,
3336
MaterialListItemComponent,
@@ -38,6 +41,7 @@ import 'gallery_route_library.dart';
3841
NgModel,
3942
routerDirectives,
4043
],
44+
providers: [focusIndicatorProviders],
4145
viewProviders: [
4246
Provider(HighlightProvider, useExisting: GalleryComponent)
4347
],
@@ -53,10 +57,11 @@ class GalleryComponent implements HighlightProvider {
5357
final ItemRenderer _highlightRenderer =
5458
(item) => item is _Example ? item.displayName : item?.toString();
5559
final List<RouteDefinition> routes = galleryRoutes;
60+
final FocusIndicatorController focusIndicatorController;
5661
StringSelectionOptions<_Example> exampleOptions;
5762
String _breadcrumb = '';
5863
59-
GalleryComponent(Router router) {
64+
GalleryComponent(this.focusIndicatorController, Router router) {
6065
router.stream.listen((newRoute) {
6166
_breadcrumb = breadcrumbs[newRoute.path];
6267
querySelector('material-content').scrollTop = 0;

angular_gallery/lib/builder/template/gallery.html.mustache

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
{{ breadcrumb }}
3939
</span>
4040
</div>
41+
<div class="key-nav">
42+
<material-checkbox [(checked)]="focusIndicatorController.enabled">
43+
Enable focus indicator
44+
</material-checkbox>
45+
</div>
4146
</header>
4247
<router-outlet [routes]="routes"></router-outlet>
4348
</div>

angular_gallery/lib/builder/template/gallery.scss.mustache

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@import 'package:angular_components/css/material/material';
33
@import 'package:angular_components/css/material/mixins';
44
@import 'package:angular_components/css/material/shadow';
5+
@import 'package:angular_components/material_checkbox/mixins';
56
@import 'package:angular_components/material_input/mixins';
67

78
material-drawer {
@@ -61,5 +62,12 @@ material-content {
6162
position: sticky;
6263
top: 0;
6364
z-index: 1;
65+
display: flex;
66+
flex-direction: row;
67+
align-items: center;
68+
justify-content: space-between;
69+
padding-right: $mat-grid * 5;
70+
71+
@include material-checkbox-color($mat-orange-500);
6472
}
6573
}

angular_gallery_section/lib/builder/template/gallery_section.dart.mustache

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:angular/angular.dart';
22
import 'package:angular_components/laminate/popup/module.dart';
3+
import 'package:angular_components/model/a11y/focus_indicator_controller.dart';
34

45
// Import each demo.
56
{{#imports}}
@@ -15,17 +16,28 @@ final _showDocs = Uri.base.queryParameters['showDocs'];
1516

1617
@Component(
1718
selector: 'gallery-section',
18-
providers: [popupBindings],
19+
providers: [focusIndicatorProviders, popupBindings],
1920
template: r'''
21+
<a class="focus-indicator-toggle"
22+
tabindex="0"
23+
(click)="focusIndicatorController.enabled = !focusIndicatorController.enabled">
24+
<template [ngIf]="!focusIndicatorController.enabled">
25+
Enable focus indicator
26+
</template>
27+
<template [ngIf]="focusIndicatorController.enabled">
28+
Disable focus indicator
29+
</template>
30+
</a>
2031
<div *ngIf="!showDocsPage">
21-
<div class="docs-toggle" (click)="toggleDocsPage()">Show Gallery Page</div>
32+
<a class="docs-toggle" tabindex="0"
33+
(click)="toggleDocsPage()">Show Gallery Page</a>
2234
{{#demos}}
2335
<{{selector}}></{{selector}}>
2436
{{/demos}}
2537
</div>
2638
<div *ngIf="showDocsPage">
27-
<div class="docs-toggle" *ngIf="!paramShowDocs"
28-
(click)="toggleDocsPage()">Show Demo Only</div>
39+
<a class="docs-toggle" tabindex="0" *ngIf="!paramShowDocs"
40+
(click)="toggleDocsPage()">Show Demo Only</a>
2941
{{#apis}}
3042
{{#docs}}
3143
<{{selector}}></{{selector}}>
@@ -43,11 +55,21 @@ final _showDocs = Uri.base.queryParameters['showDocs'];
4355
{{/docs}}
4456
{{/apis}}
4557
],
46-
styles: ['.docs-toggle {color: #4285f4; cursor: pointer;}'],
58+
styles: ['''
59+
.focus-indicator-toggle,
60+
.docs-toggle {
61+
color: #4285f4;
62+
cursor: pointer;
63+
}
64+
'''],
4765
)
4866
class GallerySection {
67+
final FocusIndicatorController focusIndicatorController;
68+
4969
bool get paramShowDocs => _showDocs != null;
5070
bool _showDocsPage = _showDocs != null;
5171
bool get showDocsPage => _showDocsPage;
5272
void toggleDocsPage() => _showDocsPage = !showDocsPage;
73+
74+
GallerySection(this.focusIndicatorController);
5375
}

0 commit comments

Comments
 (0)