-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathfocus-ring.ts
More file actions
137 lines (115 loc) · 3.4 KB
/
Copy pathfocus-ring.ts
File metadata and controls
137 lines (115 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {isServer, LitElement, PropertyValues} from 'lit';
import {property} from 'lit/decorators.js';
import {
Attachable,
AttachableController,
} from '../../internal/controller/attachable-controller.js';
/**
* Events that the focus ring listens to.
*/
const EVENTS = ['focusin', 'focusout', 'pointerdown', 'keydown'];
let hadKeyboardEvent = false;
/**
* A focus ring component.
*
* @fires visibility-changed {Event} Fired whenever `visible` changes.
*/
export class FocusRing extends LitElement implements Attachable {
/**
* Makes the focus ring visible.
*/
@property({type: Boolean, reflect: true}) visible = false;
/**
* Makes the focus ring animate inwards instead of outwards.
*/
@property({type: Boolean, reflect: true}) inward = false;
get htmlFor() {
return this.attachableController.htmlFor;
}
set htmlFor(htmlFor: string | null) {
this.attachableController.htmlFor = htmlFor;
}
get control() {
return this.attachableController.control;
}
set control(control: HTMLElement | null) {
this.attachableController.control = control;
}
private readonly attachableController = new AttachableController(
this,
this.onControlChange.bind(this),
);
attach(control: HTMLElement) {
this.attachableController.attach(control);
}
detach() {
this.attachableController.detach();
}
override connectedCallback() {
super.connectedCallback();
// Needed for VoiceOver, which will create a "group" if the element is a
// sibling to other content.
this.setAttribute('aria-hidden', 'true');
}
/** @private */
handleEvent(event: FocusRingEvent) {
if (event[HANDLED_BY_FOCUS_RING]) {
// This ensures the focus ring does not activate when multiple focus rings
// are used within a single component.
return;
}
switch (event.type) {
default:
return;
case 'keydown':
hadKeyboardEvent = true;
break;
case 'focusin':
// Only use hadKeyboardEvent when using safari.
// It works around an issue with focus-visible not working
// properly.
this.visible = this.isSafari() ?
hadKeyboardEvent :
this.control?.matches(':focus-visible') ?? false;
break;
case 'pointerdown':
hadKeyboardEvent = false;
this.visible = false;
break;
case 'focusout':
this.visible = false;
break;
}
event[HANDLED_BY_FOCUS_RING] = true;
}
private onControlChange(prev: HTMLElement | null, next: HTMLElement | null) {
if (isServer) return;
for (const event of EVENTS) {
prev?.removeEventListener(event, this);
next?.addEventListener(event, this);
}
}
override update(changed: PropertyValues<FocusRing>) {
if (changed.has('visible')) {
// This logic can be removed once the `:has` selector has been introduced
// to Firefox. This is necessary to allow correct submenu styles.
this.dispatchEvent(new Event('visibility-changed'));
}
super.update(changed);
}
private isSafari() {
return (
/^((?!chrome|android).)*safari/i.test(navigator.userAgent) ||
/iPad|iPhone|iPod/.test(navigator.userAgent)
);
}
}
const HANDLED_BY_FOCUS_RING = Symbol('handledByFocusRing');
interface FocusRingEvent extends Event {
[HANDLED_BY_FOCUS_RING]: true;
}