fix(focus)!: corrected outward/inward animations

BREAKING CHANGE: inward focus rings must be specified with `inward` rather than a negative offset.

PiperOrigin-RevId: 534552625
This commit is contained in:
Elizabeth Mitchell 2023-05-23 14:07:59 -07:00 committed by Copybara-Service
parent 146aa17509
commit 26d69c271e
16 changed files with 118 additions and 50 deletions

View File

@ -86,7 +86,7 @@ $_checkmark-bottom-left: 7px, -14px;
@include focus-ring.theme(
(
'offset': -2px,
'outward-offset': -2px,
)
);
}

View File

@ -21,7 +21,7 @@
'shape-end-start': var(--_container-shape-end-end),
'shape-end-end': var(--_container-shape-end-end),
'shape-start-end': var(--_container-shape-start-end),
'offset': -2px,
'outward-offset': -2px,
)
);
}

View File

@ -12,7 +12,7 @@
// Adjust the focus ring padding to account for the 1px border in HCM.
@include focus-ring.theme(
(
'offset': 3px,
'outward-offset': 3px,
)
);
border: 1px solid ButtonText;

View File

@ -8,21 +8,25 @@ import './index.js';
import './material-collection.js';
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js';
import {cssCustomProperty, Knob, textInput} from './index.js';
import {boolInput, cssCustomProperty, Knob, textInput} from './index.js';
import {stories, StoryKnobs} from './stories.js';
const collection =
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Focus', [
new Knob('inward', {ui: boolInput(), defaultValue: false}),
new Knob(
'--md-focus-ring-width',
{ui: textInput(), wiring: cssCustomProperty, defaultValue: '3px'}),
new Knob(
'--md-focus-ring-offset',
{ui: textInput(), wiring: cssCustomProperty, defaultValue: '2px'}),
new Knob(
'--md-focus-ring-active-width',
{ui: textInput(), wiring: cssCustomProperty, defaultValue: '8px'}),
new Knob(
'--md-focus-ring-outward-offset',
{ui: textInput(), wiring: cssCustomProperty, defaultValue: '2px'}),
new Knob(
'--md-focus-ring-inward-offset',
{ui: textInput(), wiring: cssCustomProperty, defaultValue: '0px'}),
]);
collection.addStories(...materialInitsToStoryInits(stories));

View File

@ -11,9 +11,11 @@ import {css, html} from 'lit';
/** Knob types for focus ring stories. */
export interface StoryKnobs {
inward: boolean;
'--md-focus-ring-width': string;
'--md-focus-ring-offset': string;
'--md-focus-ring-active-width': string;
'--md-focus-ring-outward-offset': string;
'--md-focus-ring-inward-offset': string;
}
const standard: MaterialStoryInit<StoryKnobs> = {
@ -22,7 +24,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
button {
appearance: none;
background: var(--md-sys-color-surface);
border: 1px solid var(--md-sys-color-outline);
border: none;
border-radius: 16px;
--md-focus-ring-shape: 16px;
height: 64px;
@ -33,20 +35,28 @@ const standard: MaterialStoryInit<StoryKnobs> = {
width: 64px;
}
button::before {
border: 1px solid var(--md-sys-color-outline);
border-radius: inherit;
content: '';
inset: 0;
position: absolute;
}
button:focus {
background: var(--md-sys-color-surface-variant);
}
`,
render() {
render({inward}) {
return html`
<button aria-label="A button with a focus ring">
<md-focus-ring></md-focus-ring>
<md-focus-ring ?inward=${inward}></md-focus-ring>
</button>
<button aria-label="A button with a focus ring">
<md-focus-ring></md-focus-ring>
<md-focus-ring ?inward=${inward}></md-focus-ring>
</button>
<button aria-label="A button with a focus ring">
<md-focus-ring></md-focus-ring>
<md-focus-ring ?inward=${inward}></md-focus-ring>
</button>
`;
}
@ -59,7 +69,6 @@ const multiAction: MaterialStoryInit<StoryKnobs> = {
align-items: center;
appearance: none;
background: var(--md-sys-color-surface);
border: 1px solid var(--md-sys-color-outline);
border-radius: 16px;
--md-focus-ring-shape: 16px;
display: flex;
@ -94,17 +103,25 @@ const multiAction: MaterialStoryInit<StoryKnobs> = {
}
#secondary {
border: 1px solid var(--md-sys-color-outline);
height: 32px;
width: 32px;
border-radius: 32px;
--md-focus-ring-shape: 32px;
}
[role="list"]::before,
#secondary::before {
border: 1px solid var(--md-sys-color-outline);
border-radius: inherit;
content: '';
inset: 0;
position: absolute;
}
`,
render() {
render({inward}) {
return html`
<div role="list">
<md-focus-ring for="primary"></md-focus-ring>
<md-focus-ring for="primary" ?inward=${inward}></md-focus-ring>
<div role="listitem">
<button id="primary" aria-label="The primary action for a multi-action component">
@ -115,7 +132,7 @@ const multiAction: MaterialStoryInit<StoryKnobs> = {
<div role="listitem">
<button id="secondary" aria-label="The secondary action for a multi-action component">
X
<md-focus-ring></md-focus-ring>
<md-focus-ring ?inward=${inward}></md-focus-ring>
</button>
</div>
</div>

View File

@ -44,37 +44,80 @@ $_md-sys-motion: tokens.md-sys-motion-values();
--_shape-end-end: var(--md-focus-ring-shape-end-end, var(--_shape));
--_shape-end-start: var(--md-focus-ring-shape-end-start, var(--_shape));
animation-duration: var(--_duration);
animation-delay: 0s, calc(var(--_duration) * 0.25);
animation-duration: calc(var(--_duration) * 0.25),
calc(var(--_duration) * 0.75);
animation-timing-function: map.get($_md-sys-motion, 'easing-emphasized');
border-end-end-radius: calc(var(--_offset) + var(--_shape-end-end));
border-end-start-radius: calc(var(--_offset) + var(--_shape-end-start));
border-start-end-radius: calc(var(--_offset) + var(--_shape-start-end));
border-start-start-radius: calc(var(--_offset) + var(--_shape-start-start));
box-shadow: inset 0 0 0 0 currentColor;
box-sizing: border-box;
color: var(--_color);
display: none;
inset: calc(-1 * (var(--_offset) + 1px));
// NOTE: this 1px offset hides a transparent ring between the outline and
// offset when border-radius is large
outline-offset: -1px;
outline: var(--_width) solid currentColor;
pointer-events: none;
position: absolute;
}
:host([visible]) {
display: flex;
animation-name: focus-ring;
}
@keyframes focus-ring {
:host(:not([inward])) {
animation-name: outward-grow, outward-shrink;
border-end-end-radius: calc(var(--_shape-end-end) + var(--_outward-offset));
border-end-start-radius: calc(
var(--_shape-end-start) + var(--_outward-offset)
);
border-start-end-radius: calc(
var(--_shape-start-end) + var(--_outward-offset)
);
border-start-start-radius: calc(
var(--_shape-start-start) + var(--_outward-offset)
);
inset: calc(-1 * (var(--_outward-offset)));
outline: var(--_width) solid currentColor;
}
:host([inward]) {
animation-name: inward-grow, inward-shrink;
border-end-end-radius: calc(var(--_shape-end-end) - var(--_inward-offset));
border-end-start-radius: calc(
var(--_shape-end-start) - var(--_inward-offset)
);
border-start-end-radius: calc(
var(--_shape-start-end) - var(--_inward-offset)
);
border-start-start-radius: calc(
var(--_shape-start-start) - var(--_inward-offset)
);
border: var(--_width) solid currentColor;
inset: var(--_inward-offset);
}
@keyframes outward-grow {
from {
outline-width: 0px;
outline-width: 0;
}
25% {
box-shadow: inset 0 0 0 calc(var(--_active-width) / 2) currentColor;
outline-width: calc(var(--_active-width) / 2);
to {
outline-width: var(--_active-width);
}
}
@keyframes outward-shrink {
from {
outline-width: var(--_active-width);
}
}
@keyframes inward-grow {
from {
border-width: 0;
}
to {
border-width: var(--_active-width);
}
}
@keyframes inward-shrink {
from {
border-width: var(--_active-width);
}
}

View File

@ -15,6 +15,7 @@ export class FocusRing extends LitElement {
* Makes the focus ring visible.
*/
@property({type: Boolean, reflect: true}) visible = false;
@property({type: Boolean, reflect: true}) inward = false;
/**
* Reflects the value of the `for` attribute, which is the ID of the focus

View File

@ -47,8 +47,7 @@
@include focus-ring.theme(
(
'offset': -3px,
'shape': map.get(tokens.md-sys-shape-values(), 'corner-extra-small'),
'shape': 8px,
)
);
@include ripple.theme(

View File

@ -159,7 +159,7 @@ export class ListItemEl extends LitElement implements ListItem {
* Handles rendering of the focus ring.
*/
protected renderFocusRing() {
return html`<md-focus-ring class="focus-ring" for="item"></md-focus-ring>`;
return html`<md-focus-ring class="focus-ring" for="item" inward></md-focus-ring>`;
}
/**

View File

@ -43,8 +43,8 @@ $_custom-property-prefix: 'navigation-bar';
@include focus-ring.theme(
(
'offset': -2px,
'shape': map.get(tokens.md-sys-shape-values(), 'corner-small'),
'inward-offset': -1px,
)
);

View File

@ -52,8 +52,9 @@ export class NavigationTab extends LitElement implements NavigationTabState {
aria-label=${ariaLabel || nothing}
tabindex="${this.active ? 0 : -1}"
@click="${this.handleClick}"
${ripple(this.getRipple)}>
<md-focus-ring></md-focus-ring>
${ripple(this.getRipple)}
>
<md-focus-ring inward></md-focus-ring>
${when(this.showRipple, this.renderRipple)}
<span aria-hidden="true" class="md3-navigation-tab__icon-content"
><span class="md3-navigation-tab__active-indicator"

View File

@ -45,7 +45,7 @@ $_md-sys-motion: tokens.md-sys-motion-values();
@include focus-ring.theme(
(
'offset': -2px,
'outward-offset': -2px,
)
);

View File

@ -450,7 +450,7 @@ $_md-sys-shape: tokens.md-sys-shape-values();
md-focus-ring {
@include focus-ring.theme(
(
'offset': -2px,
'outward-offset': -2px,
)
);
}

View File

@ -54,14 +54,15 @@
@include focus-ring.theme(
(
// desired border-radius is 8px and it's internally calc'd as sum
// of shape + offset (-7 + 15)
shape: 15px,
offset: -7px
'shape': 8px,
)
);
}
:host([selected]) md-focus-ring {
margin-bottom: calc(var(--_active-indicator-height) + 1px);
}
.button {
display: inline-flex;
align-items: center;

View File

@ -115,7 +115,7 @@ export class Tab extends LitElement {
aria-label=${this.ariaLabel || nothing}
${ripple(this.getRipple)}
>
<md-focus-ring></md-focus-ring>
<md-focus-ring inward></md-focus-ring>
<md-elevation></md-elevation>
${when(this.showRipple, this.renderRipple)}
<span class="touch"></span>

View File

@ -19,7 +19,8 @@ $supported-tokens: (
'active-width',
'color',
'duration',
'offset',
'inward-offset',
'outward-offset',
'shape',
'width',
// go/keep-sorted end
@ -36,7 +37,8 @@ $_default: (
'active-width': if($exclude-hardcoded-values, null, 8px),
'color': map.get($deps, 'md-sys-color', 'secondary'),
'duration': map.get($deps, 'md-sys-motion', 'duration-long4'),
'offset': if($exclude-hardcoded-values, null, 2px),
'inward-offset': if($exclude-hardcoded-values, null, 0px),
'outward-offset': if($exclude-hardcoded-values, null, 2px),
'shape': map.get($deps, 'md-sys-shape', 'corner-full'),
'width': if($exclude-hardcoded-values, null, 3px),
);