rework focus-visible logic

pull/513/head
Cory LaViska 2021-08-26 08:35:36 -04:00
rodzic 36d499f253
commit 43f74583ea
14 zmienionych plików z 66 dodań i 125 usunięć

Wyświetl plik

@ -19,6 +19,7 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
- Improved icon contrast in `sl-input`
- Improved contrast of `sl-switch`
- Removed elevation from `sl-color-picker` when rendered inline
- Removed custom `:focus-visible` logic in favor of a directive that outputs `:focus-visible` or `:focus` depending on browser support
- Updated to Lit 2.0.0-rc.3
- Updated to lit-html 2.0.0-rc.4

Wyświetl plik

@ -1,5 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
import { focusVisibleSelector } from '../../internal/focus-visible';
export default css`
${componentStyles}
@ -80,7 +81,7 @@ export default css`
color: rgb(var(--sl-color-primary-700));
}
.button.button--default:focus:not(.button--disabled) {
.button.button--default${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-50));
border-color: rgb(var(--sl-color-primary-500));
color: rgb(var(--sl-color-primary-700));
@ -106,7 +107,7 @@ export default css`
color: rgb(var(--sl-color-neutral-0));
}
.button.button--primary:focus:not(.button--disabled) {
.button.button--primary${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
color: rgb(var(--sl-color-neutral-0));
@ -132,7 +133,7 @@ export default css`
color: rgb(var(--sl-color-neutral-0));
}
.button.button--success:focus:not(.button--disabled) {
.button.button--success${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-success-600));
border-color: rgb(var(--sl-color-success-600));
color: rgb(var(--sl-color-neutral-0));
@ -158,7 +159,7 @@ export default css`
color: rgb(var(--sl-color-neutral-0));
}
.button.button--neutral:focus:not(.button--disabled) {
.button.button--neutral${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-neutral-500));
border-color: rgb(var(--sl-color-neutral-500));
color: rgb(var(--sl-color-neutral-0));
@ -183,7 +184,7 @@ export default css`
color: rgb(var(--sl-color-neutral-0));
}
.button.button--warning:focus:not(.button--disabled) {
.button.button--warning${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-warning-500));
border-color: rgb(var(--sl-color-warning-500));
color: rgb(var(--sl-color-neutral-0));
@ -209,7 +210,7 @@ export default css`
color: rgb(var(--sl-color-neutral-0));
}
.button.button--danger:focus:not(.button--disabled) {
.button.button--danger${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-danger-500));
border-color: rgb(var(--sl-color-danger-500));
color: rgb(var(--sl-color-neutral-0));
@ -238,7 +239,7 @@ export default css`
color: rgb(var(--sl-color-primary-500));
}
.button--text:focus:not(.button--disabled) {
.button--text${focusVisibleSelector}:not(.button--disabled) {
background-color: transparent;
border-color: transparent;
color: rgb(var(--sl-color-primary-500));

Wyświetl plik

@ -1,5 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
import { focusVisibleSelector } from '../../internal/focus-visible';
export default css`
${componentStyles}
@ -31,7 +32,7 @@ export default css`
outline: none;
}
.focus-visible .details__header:focus {
.details__header${focusVisibleSelector} {
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
@ -39,7 +40,7 @@ export default css`
cursor: not-allowed;
}
.details--disabled .details__header:focus {
.details--disabled .details__header${focusVisibleSelector} {
outline: none;
box-shadow: none;
}

Wyświetl plik

@ -5,7 +5,7 @@ import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../intern
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { waitForEvent } from '../../internal/event';
import { focusVisible } from '../../internal/focus-visible';
import { focusVisibleSelector } from '../../internal/focus-visible';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import styles from './details.styles';
@ -55,21 +55,11 @@ export default class SlDetails extends LitElement {
/** Disables the details so it can't be toggled. */
@property({ type: Boolean, reflect: true }) disabled = false;
connectedCallback() {
super.connectedCallback();
this.updateComplete.then(() => focusVisible.observe(this.details));
}
firstUpdated() {
this.body.hidden = !this.open;
this.body.style.height = this.open ? 'auto' : '0';
}
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.details);
}
/** Shows the details. */
async show() {
if (this.open) {

Wyświetl plik

@ -1,5 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
import { focusVisibleSelector } from '../../internal/focus-visible';
export default css`
${componentStyles}
@ -41,7 +42,7 @@ export default css`
cursor: not-allowed;
}
.focus-visible.icon-button:focus {
.icon-button${focusVisibleSelector} {
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
`;

Wyświetl plik

@ -2,7 +2,6 @@ import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { focusVisible } from '../../internal/focus-visible';
import styles from './icon-button.styles';
import '../icon/icon';
@ -48,16 +47,6 @@ export default class SlIconButton extends LitElement {
/** Disables the button. */
@property({ type: Boolean, reflect: true }) disabled = false;
connectedCallback() {
super.connectedCallback();
this.updateComplete.then(() => focusVisible.observe(this.button));
}
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.button);
}
render() {
const isLink = this.href ? true : false;

Wyświetl plik

@ -1,5 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
import { focusVisibleSelector } from '../../internal/focus-visible';
export default css`
${componentStyles}
@ -61,7 +62,7 @@ export default css`
}
:host(:hover:not([aria-disabled='true'])) .menu-item,
:host(.sl-focus-visible:focus:not([aria-disabled='true'])) .menu-item {
:host(${focusVisibleSelector}:not(.sl-focus-invisible):not([aria-disabled='true'])) .menu-item {
outline: none;
background-color: rgb(var(--sl-color-primary-600));
color: rgb(var(--sl-color-neutral-0));

Wyświetl plik

@ -2,7 +2,7 @@ import { LitElement, html } from 'lit';
import { customElement, query } from 'lit/decorators.js';
import { emit } from '../../internal/event';
import { getTextContent } from '../../internal/slot';
import { focusVisible } from '../../internal/focus-visible';
import { hasFocusVisible } from '../../internal/focus-visible';
import type SlMenuItem from '../menu-item/menu-item';
import styles from './menu.styles';
@ -26,19 +26,6 @@ export default class SlMenu extends LitElement {
private typeToSelectString = '';
private typeToSelectTimeout: any;
connectedCallback() {
super.connectedCallback();
focusVisible.observe(this, {
visible: () => this.getAllItems().map(item => item.classList.add('sl-focus-visible')),
notVisible: () => this.getAllItems().map(item => item.classList.remove('sl-focus-visible'))
});
}
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this);
}
getAllItems(options: { includeDisabled: boolean } = { includeDisabled: true }) {
return [...this.defaultSlot.assignedElements({ flatten: true })].filter((el: HTMLElement) => {
if (el.getAttribute('role') !== 'menuitem') {
@ -85,15 +72,18 @@ export default class SlMenu extends LitElement {
this.typeToSelectTimeout = setTimeout(() => (this.typeToSelectString = ''), 750);
this.typeToSelectString += key.toLowerCase();
// The menu may not have focus, so the focus visible logic may not be triggered. Because we know they're using the
// keyboard, we can force the sl-focus-visible class on each item so the selection shows as expected.
this.getAllItems().map(item => item.classList.add('sl-focus-visible'));
// Restore focus in browsers that don't support :focus-visible when using the keyboard
if (!hasFocusVisible) {
items.map(item => item.classList.remove('sl-focus-invisible'));
}
for (const item of items) {
const slot = item.shadowRoot!.querySelector('slot:not([name])') as HTMLSlotElement;
const label = getTextContent(slot).toLowerCase().trim();
if (label.substring(0, this.typeToSelectString.length) === this.typeToSelectString) {
this.setCurrentItem(item);
// Set focus here to force the browser to show :focus-visible styles
item.focus();
break;
}
@ -109,6 +99,14 @@ export default class SlMenu extends LitElement {
}
}
handleKeyUp() {
// Restore focus in browsers that don't support :focus-visible when using the keyboard
if (!hasFocusVisible) {
const items = this.getAllItems();
items.map(item => item.classList.remove('sl-focus-invisible'));
}
}
handleKeyDown(event: KeyboardEvent) {
// Make a selection when pressing enter
if (event.key === 'Enter') {
@ -163,7 +161,11 @@ export default class SlMenu extends LitElement {
if (target.getAttribute('role') === 'menuitem') {
this.setCurrentItem(target as SlMenuItem);
target.focus();
// Hide focus in browsers that don't support :focus-visible when using the mouse
if (!hasFocusVisible) {
target.classList.add('sl-focus-invisible');
}
}
}
@ -184,6 +186,7 @@ export default class SlMenu extends LitElement {
role="menu"
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
@keyup=${this.handleKeyUp}
@mousedown=${this.handleMouseDown}
>
<slot @slotchange=${this.handleSlotChange}></slot>

Wyświetl plik

@ -1,5 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
import { focusVisibleSelector } from '../../internal/focus-visible';
export default css`
${componentStyles}
@ -24,7 +25,7 @@ export default css`
outline: none;
}
.rating.focus-visible:focus {
.rating${focusVisibleSelector} {
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}

Wyświetl plik

@ -5,7 +5,6 @@ import { styleMap } from 'lit-html/directives/style-map';
import { unsafeHTML } from 'lit-html/directives/unsafe-html';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { focusVisible } from '../../internal/focus-visible';
import { clamp } from '../../internal/math';
import styles from './rating.styles';
@ -65,16 +64,6 @@ export default class SlRating extends LitElement {
this.rating.blur();
}
connectedCallback() {
super.connectedCallback();
this.updateComplete.then(() => focusVisible.observe(this.rating));
}
disconnectedCallback() {
super.disconnectedCallback();
focusVisible.unobserve(this.rating);
}
getValueFromMousePosition(event: MouseEvent) {
return this.getValueFromXCoordinate(event.clientX);
}

Wyświetl plik

@ -28,11 +28,6 @@ export default css`
transition: var(--sl-transition-fast) transform ease, var(--sl-transition-fast) width ease;
}
/* Remove the focus ring when the user isn't interacting with a keyboard */
.tab-group:not(.focus-visible) ::slotted(sl-tab) {
--focus-ring: none;
}
.tab-group--has-scroll-controls .tab-group__nav-container {
position: relative;
padding: 0 var(--sl-spacing-x-large);

Wyświetl plik

@ -3,7 +3,6 @@ import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { focusVisible } from '../../internal/focus-visible';
import { getOffset } from '../../internal/offset';
import { scrollIntoView } from '../../internal/scroll';
import type SlTab from '../tab/tab';
@ -88,7 +87,6 @@ export default class SlTabGroup extends LitElement {
this.syncTabsAndPanels();
this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true });
this.resizeObserver.observe(this.nav);
focusVisible.observe(this.tabGroup);
// Set initial tab state when the tabs first become visible
const intersectionObserver = new IntersectionObserver((entries, observer) => {
@ -105,7 +103,6 @@ export default class SlTabGroup extends LitElement {
disconnectedCallback() {
this.mutationObserver.disconnect();
this.resizeObserver.unobserve(this.nav);
focusVisible.unobserve(this.tabGroup);
}
/** Shows the specified tab panel. */

Wyświetl plik

@ -1,12 +1,11 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
import { focusVisibleSelector } from '../../internal/focus-visible';
export default css`
${componentStyles}
:host {
--focus-ring: inset 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
display: inline-block;
}
@ -33,9 +32,9 @@ export default css`
outline: none;
}
.tab:focus:not(.tab--disabled) {
.tab${focusVisibleSelector}:not(.tab--disabled) {
color: rgb(var(--sl-color-primary-600));
box-shadow: var(--focus-ring);
box-shadow: inset 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
.tab.tab--active:not(.tab--disabled) {

Wyświetl plik

@ -1,53 +1,26 @@
import { unsafeCSS } from 'lit';
//
// Simulates :focus-visible behavior on an element by watching for certain keyboard and mouse heuristics and toggling a
// `focus-visible` class. Works at the component level so no global polyfill is necessary.
// Determines if the current browser supports :focus-visible
//
// This will eventually be removed pending better :focus-visible support: https://caniuse.com/#search=focus-visible
export const hasFocusVisible = (() => {
const style = document.createElement('style');
let isSupported;
try {
document.head.appendChild(style);
style.sheet!.insertRule(':focus-visible { color: inherit }');
isSupported = true;
} catch {
isSupported = false;
} finally {
style.remove();
}
return isSupported;
})();
//
const listeners = new WeakMap();
interface ObserveOptions {
visible: () => void;
notVisible: () => void;
}
export function observe(el: HTMLElement, options?: ObserveOptions) {
const keys = ['Tab', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageDown', 'PageUp'];
const is = (event: KeyboardEvent) => {
if (keys.includes(event.key)) {
el.classList.add('focus-visible');
if (options?.visible) {
options.visible();
}
}
};
const isNot = () => {
el.classList.remove('focus-visible');
if (options?.notVisible) {
options.notVisible();
}
};
listeners.set(el, { is, isNot });
el.addEventListener('keydown', is);
el.addEventListener('keyup', is);
el.addEventListener('mousedown', isNot);
el.addEventListener('mouseup', isNot);
}
export function unobserve(el: HTMLElement) {
const { is, isNot } = listeners.get(el);
el.classList.remove('focus-visible');
el.removeEventListener('keydown', is);
el.removeEventListener('keyup', is);
el.removeEventListener('mousedown', isNot);
el.removeEventListener('mouseup', isNot);
}
export const focusVisible = {
observe,
unobserve
};
// A selector for Lit stylesheets that outputs `:focus-visible` if the browser supports it and `:focus` otherwise
//
export const focusVisibleSelector = unsafeCSS(hasFocusVisible ? ':focus-visible' : ':focus');