mirror of
https://github.com/material-components/material-web.git
synced 2026-03-09 00:09:23 +08:00
118 lines
2.8 KiB
TypeScript
118 lines
2.8 KiB
TypeScript
/**
|
|
* @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.
|
|
*
|
|
* @fires visibility-changed Fired whenever `visible` changes.
|
|
*/
|
|
const EVENTS = ['focusin', 'focusout', 'pointerdown'];
|
|
|
|
/**
|
|
* A focus ring component.
|
|
*/
|
|
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 'focusin':
|
|
this.visible = this.control?.matches(':focus-visible') ?? false;
|
|
break;
|
|
case 'focusout':
|
|
case 'pointerdown':
|
|
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);
|
|
}
|
|
}
|
|
|
|
const HANDLED_BY_FOCUS_RING = Symbol('handledByFocusRing');
|
|
|
|
interface FocusRingEvent extends Event {
|
|
[HANDLED_BY_FOCUS_RING]: true;
|
|
}
|