feat(linear-progress): adds linear-progress element

PiperOrigin-RevId: 524016869
This commit is contained in:
Material Web Team 2023-04-13 09:06:10 -07:00 committed by Copybara-Service
parent a44bc3a87c
commit 1c7fcf39a5
9 changed files with 592 additions and 2 deletions

View File

@ -47,7 +47,7 @@ Icon | ✅ | ✅ | ❌
List | ✅ | ✅ | ❌
Menu | ✅ | ✅ | ❌
Progress indicator (circular) | ✅ | ✅ | ❌
Progress indicator (linear) | 🟡 | ❌ | ❌
Progress indicator (linear) | ✅ | 🟡 | ❌
Radio button | ✅ | ✅ | ❌
Ripple | ✅ | ✅ | 🟡
Select | 🟡 | ❌ | ❌

View File

@ -0,0 +1,6 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@forward './lib/linear-progress' show theme;

21
linearprogress/harness.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {Harness} from '../testing/harness.js';
import {LinearProgress} from './lib/linear-progress.js';
/**
* Test harness for linear-progress.
*/
export class LinearProgressHarness extends Harness<LinearProgress> {
override async getInteractiveElement() {
await this.element.updateComplete;
// Test access to protected property
// tslint:disable-next-line:no-dict-access-on-struct-type
return this.element['rootEl'];
}
}

View File

@ -0,0 +1,380 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use '../../sass/theme';
@use '../../tokens';
// go/keep-sorted end
@mixin theme($tokens) {
$reference: tokens.md-comp-linear-progress-indicator-values();
$tokens: theme.validate-theme($reference, $tokens);
$tokens: theme.create-theme-vars($tokens, 'linear-progress');
@include theme.emit-theme-vars($tokens);
}
// note, transition settings match MDC
// see https://github.com/material-components/material-components-web/blob/master/packages/mdc-linear-progress/_linear-progress.scss#L79
$_determinate-duration: 250ms;
$_determinate-easing: cubic-bezier(0.4, 0, 0.6, 1);
// see https://github.com/material-components/material-components-web/blob/master/packages/mdc-linear-progress/_linear-progress.scss#L218
$_indeterminate-duration: 2s;
// Note, track background is a series of animating dots sized to fit
// the track height. Since the dots are circular, width scales with height.
// Background width is separated because it's also used to help animate the
// /dots.
$_track-dot-size: calc(var(--_track-height) / 2);
$_track-background-width: calc($_track-dot-size * 5);
// this is a series of sized/colored dots.
$_track-background: 0 / $_track-background-width 100%
radial-gradient(
circle at calc($_track-dot-size * 2),
var(--_track-color) 0,
var(--_track-color) $_track-dot-size,
transparent $_track-dot-size
);
// Generates a list of rtl selectors. This is done so rules can be generated
// separately so they don't get dropped where unsupported.
$rtl-selectors: (
':host-context([dir="rtl"]) .linear-progress',
':host([dir="rtl"]) .linear-progress',
'.linear-progress:dir(rtl)'
);
// Generates keyframes for ltr and rtl.
@mixin directional-keyframes($dir) {
$is-rtl: $dir == 'rtl';
$sign: if($is-rtl, -1, 1);
$suffix: if($is-rtl, '-rtl', '');
@keyframes buffering#{$suffix} {
0% {
// the amount to animate is aligned with the default track background
transform: translateX(calc(#{$sign} * #{$_track-background-width}));
}
}
// note, the numbers here come directly from the mdc implementation.
// see https://github.com/material-components/material-components-web/blob/master/packages/mdc-linear-progress/_linear-progress.scss#L208.
// keyframes
@keyframes primary-indeterminate-translate#{$suffix} {
0% {
transform: translateX(0px);
}
20% {
animation-timing-function: cubic-bezier(0.5, 0, 0.701732, 0.495819);
transform: translateX(0px);
}
59.15% {
animation-timing-function: cubic-bezier(
0.302435,
0.381352,
0.55,
0.956352
);
transform: translateX(calc(#{$sign} * 83.6714%));
}
100% {
transform: translateX(calc(#{$sign} * 200.611%));
}
}
@keyframes secondary-indeterminate-translate#{$suffix} {
0% {
animation-timing-function: cubic-bezier(0.15, 0, 0.515058, 0.409685);
transform: translateX(0px);
}
25% {
animation-timing-function: cubic-bezier(0.31033, 0.284058, 0.8, 0.733712);
transform: translateX(calc(#{$sign} * 37.6519%));
}
48.35% {
animation-timing-function: cubic-bezier(0.4, 0.627035, 0.6, 0.902026);
transform: translateX(calc(#{$sign} * 84.3862%));
}
100% {
transform: translateX(calc(#{$sign} * 160.278%));
}
}
}
@mixin styles() {
$tokens: tokens.md-comp-linear-progress-indicator-values();
$tokens: theme.create-theme-vars($tokens, 'linear-progress');
:host {
@each $token, $value in $tokens {
--_#{$token}: #{$value};
}
display: block;
position: relative;
// note, this matches the `meter` element and is just done so
// there's a minimum width when in a container with a display like
// inline-flex.
min-inline-size: 80px;
block-size: var(--_track-height);
content-visibility: auto;
contain: strict;
}
.linear-progress,
.track,
.buffer-bar,
.bar,
.bar-inner {
position: absolute;
}
.linear-progress {
inset: 0;
outline: transparent solid 1px;
border-radius: var(--_track-shape);
overflow: hidden;
display: flex;
align-items: center;
}
.bar {
animation: none;
// position is offset for indeterminate animation, so we lock the inline size here.
inline-size: 100%;
block-size: var(--_active-indicator-height);
transform-origin: left center;
will-change: transform;
transition: transform $_determinate-duration $_determinate-easing;
}
.secondary-bar {
display: none;
}
.bar-inner {
inset: 0;
animation: none;
background: var(--_active-indicator-color);
}
.buffer-bar {
background: var(--_track-color);
inset: 0;
will-change: transform;
transition: transform $_determinate-duration $_determinate-easing;
transform-origin: left center;
}
.track {
inset: 0;
will-change: transform;
animation: linear infinite $_determinate-duration;
// stylelint-disable-next-line no-unknown-animations --
// animation generated via mixin
animation-name: buffering;
background: $_track-background;
}
// indeterminate
.indeterminate .bar {
transition: none;
}
// note, the numbers here come directly from the mdc implementation.
// see https://github.com/material-components/material-components-web/blob/master/packages/mdc-linear-progress/_linear-progress.scss#L208.
.indeterminate .primary-bar {
inset-inline-start: -145.167%;
}
.indeterminate .secondary-bar {
inset-inline-start: -54.8889%;
// this is display none by default.
display: block;
}
.indeterminate .track {
display: none;
}
.indeterminate.animation-ready .primary-bar {
will-change: transform;
animation: linear infinite $_indeterminate-duration;
// stylelint-disable-next-line no-unknown-animations --
// animation generated via mixin
animation-name: primary-indeterminate-translate;
}
.indeterminate.animation-ready .primary-bar > .bar-inner {
will-change: transform;
animation: linear infinite $_indeterminate-duration
primary-indeterminate-scale;
}
.indeterminate.animation-ready.four-colors .primary-bar > .bar-inner {
animation-name: primary-indeterminate-scale, four-colors;
animation-duration: $_indeterminate-duration,
calc($_indeterminate-duration * 2);
}
.indeterminate.animation-ready .secondary-bar {
will-change: transform;
animation: linear infinite $_indeterminate-duration;
// stylelint-disable-next-line no-unknown-animations --
// animation generated via mixin
animation-name: secondary-indeterminate-translate;
}
.indeterminate.animation-ready .secondary-bar > .bar-inner {
will-change: transform;
animation: linear infinite $_indeterminate-duration
secondary-indeterminate-scale;
}
.indeterminate.animation-ready.four-colors .secondary-bar > .bar-inner {
animation-name: secondary-indeterminate-scale, four-colors;
animation-duration: $_indeterminate-duration,
calc($_indeterminate-duration * 2);
}
@each $selector in $rtl-selectors {
#{$selector} {
.bar {
transform-origin: right center;
}
.buffer-bar {
transform-origin: right center;
}
.track {
// stylelint-disable-next-line no-unknown-animations --
// animation generated via mixin
animation-name: buffering-rtl;
}
&.indeterminate.animation-ready .primary-bar {
// stylelint-disable-next-line no-unknown-animations --
// animation generated via mixin
animation-name: primary-indeterminate-translate-rtl;
}
&.indeterminate.animation-ready .secondary-bar {
// stylelint-disable-next-line no-unknown-animations --
// animation generated via mixin
animation-name: secondary-indeterminate-translate-rtl;
}
}
}
@keyframes primary-indeterminate-scale {
0% {
transform: scaleX(0.08);
}
36.65% {
animation-timing-function: cubic-bezier(0.334731, 0.12482, 0.785844, 1);
transform: scaleX(0.08);
}
69.15% {
animation-timing-function: cubic-bezier(0.06, 0.11, 0.6, 1);
transform: scaleX(0.661479);
}
100% {
transform: scaleX(0.08);
}
}
@keyframes secondary-indeterminate-scale {
0% {
animation-timing-function: cubic-bezier(
0.205028,
0.057051,
0.57661,
0.453971
);
transform: scaleX(0.08);
}
19.15% {
animation-timing-function: cubic-bezier(
0.152313,
0.196432,
0.648374,
1.00432
);
transform: scaleX(0.457104);
}
44.15% {
animation-timing-function: cubic-bezier(
0.257759,
-0.003163,
0.211762,
1.38179
);
transform: scaleX(0.72796);
}
100% {
transform: scaleX(0.08);
}
}
@include directional-keyframes('ltr');
@include directional-keyframes('rtl');
@keyframes four-colors {
0% {
background: var(--_four-color-active-indicator-one-color);
}
15% {
background: var(--_four-color-active-indicator-one-color);
}
25% {
background: var(--_four-color-active-indicator-two-color);
}
40% {
background: var(--_four-color-active-indicator-two-color);
}
50% {
background: var(--_four-color-active-indicator-three-color);
}
65% {
background: var(--_four-color-active-indicator-three-color);
}
75% {
background: var(--_four-color-active-indicator-four-color);
}
90% {
background: var(--_four-color-active-indicator-four-color);
}
100% {
background: var(--_four-color-active-indicator-one-color);
}
}
@media screen and (forced-colors: active) {
.linear-progress {
--_active-indicator-color: canvastext;
--_track-color: graytext;
border: 1px solid canvastext;
}
.indeterminate.linear-progress {
--_track-color: canvas;
}
}
}

View File

@ -0,0 +1,8 @@
//
// Copyright 2023 Google LLC
// SPDX-License-Identifier: Apache-2.0
//
@use './linear-progress';
@include linear-progress.styles;

View File

@ -0,0 +1,117 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {html, LitElement, nothing} from 'lit';
import {property, query, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import {styleMap} from 'lit/directives/style-map.js';
import {ariaProperty} from '../../decorators/aria-property.js';
/**
* LinearProgress component.
*/
export class LinearProgress extends LitElement {
/**
* Whether or not to render indeterminate progress in an animated state.
*/
@property({type: Boolean}) indeterminate = false;
/**
* Progress to display, a fraction between 0 and 1.
*/
@property({type: Number}) progress = 0;
/**
* Buffer amount to display, a fraction between 0 and 1.
*/
@property({type: Number}) buffer = 1;
/**
* Whether or not to render indeterminate mode using 4 colors instead of one.
*
*/
@property({type: Boolean, attribute: 'four-colors'}) fourColors = false;
@property({type: String, attribute: 'data-aria-label', noAccessor: true})
// tslint:disable-next-line:no-new-decorators
@ariaProperty
override ariaLabel!: string;
@query('.linear-progress') protected rootEl!: HTMLElement;
@state() protected animationReady = true;
protected resizeObserver: ResizeObserver|null = null;
// Note, the indeterminate animation is rendered with transform %'s
// Previously, this was optimized to use px calculated with the resizeObserver
// due to a now fixed Chrome bug: crbug.com/389359.
protected override render() {
const rootClasses = {
'indeterminate': this.indeterminate,
'animation-ready': this.animationReady,
'four-colors': this.fourColors
};
const progressStyles = {
transform: `scaleX(${(this.indeterminate ? 1 : this.progress) * 100}%)`
};
const bufferStyles = {
transform: `scaleX(${(this.indeterminate ? 1 : this.buffer) * 100}%)`
};
return html`
<div
role="progressbar"
class="linear-progress ${classMap(rootClasses)}"
aria-label="${this.ariaLabel || nothing}"
aria-valuemin="0"
aria-valuemax="1"
aria-valuenow="${this.indeterminate ? nothing : this.progress}">
<div class="track"></div>
<div class="buffer-bar" style=${styleMap(bufferStyles)}></div>
<div class="bar primary-bar" style=${styleMap(progressStyles)}>
<div class="bar-inner"></div>
</div>
<div class="bar secondary-bar">
<div class="bar-inner"></div>
</div>
</div>`;
}
override async connectedCallback() {
super.connectedCallback();
// wait for rendering.
await this.updateComplete;
if (this.resizeObserver) {
return;
}
this.resizeObserver = new ResizeObserver(() => {
if (this.indeterminate) {
this.restartAnimation();
}
});
this.resizeObserver.observe(this.rootEl);
}
override disconnectedCallback() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
super.disconnectedCallback();
}
// When size changes, restart the animation
// to avoid jank.
protected async restartAnimation() {
await this.updateComplete;
this.animationReady = false;
await new Promise(requestAnimationFrame);
this.animationReady = true;
await this.updateComplete;
}
}

View File

@ -0,0 +1,30 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement} from 'lit/decorators.js';
import {LinearProgress} from './lib/linear-progress.js';
import {styles} from './lib/linear-progress-styles.css.js';
declare global {
interface HTMLElementTagNameMap {
'md-linear-progress': MdLinearProgress;
}
}
/**
* @summary Linear progress indicators display progress by animating along the
* length of a fixed, visible track.
*
* @description
* Progress indicators inform users about the status of ongoing processes.
* - Determinate indicators display how long a process will take.
* - Indeterminate indicators express an unspecified amount of wait time.
*/
@customElement('md-linear-progress')
export class MdLinearProgress extends LinearProgress {
static override styles = [styles];
}

View File

@ -0,0 +1,15 @@
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {createTokenTests} from '../testing/tokens.js';
import {MdLinearProgress} from './linear-progress.js';
describe('<md-linear-progress>', () => {
describe('.styles', () => {
createTokenTests(MdLinearProgress.styles);
});
});

View File

@ -3,6 +3,9 @@
// SPDX-License-Identifier: Apache-2.0
//
// go/keep-sorted start
@use 'sass:map';
// go/keep-sorted end
// go/keep-sorted start
@use './md-sys-color';
@use './md-sys-shape';
@ -14,9 +17,19 @@ $_default: (
'md-sys-shape': md-sys-shape.values(),
);
$_unsupported-tokens: (
// can only control track since scaling is used on buffer/progress
'active-indicator-shape'
);
// TODO(b/275045611) may need to update based on resolution of design
// for buffer and background animation.
@function values($deps: $_default, $exclude-hardcoded-values: false) {
@return md-comp-linear-progress-indicator.values(
$tokens: md-comp-linear-progress-indicator.values(
$deps,
$exclude-hardcoded-values
);
$tokens: map.remove($tokens, $_unsupported-tokens...);
@return $tokens;
}