kopia lustrzana https://github.com/shoelace-style/shoelace
rework focus-visible logic
rodzic
36d499f253
commit
43f74583ea
|
@ -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
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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');
|
||||
|
|
Ładowanie…
Reference in New Issue