mirror of
https://github.com/material-components/material-web.git
synced 2024-09-11 13:46:10 +03:00
feat(linear-progress): adds linear-progress element
PiperOrigin-RevId: 524016869
This commit is contained in:
parent
a44bc3a87c
commit
1c7fcf39a5
@ -47,7 +47,7 @@ Icon | ✅ | ✅ | ❌
|
||||
List | ✅ | ✅ | ❌
|
||||
Menu | ✅ | ✅ | ❌
|
||||
Progress indicator (circular) | ✅ | ✅ | ❌
|
||||
Progress indicator (linear) | 🟡 | ❌ | ❌
|
||||
Progress indicator (linear) | ✅ | 🟡 | ❌
|
||||
Radio button | ✅ | ✅ | ❌
|
||||
Ripple | ✅ | ✅ | 🟡
|
||||
Select | 🟡 | ❌ | ❌
|
||||
|
6
linearprogress/_linear-progress.scss
Normal file
6
linearprogress/_linear-progress.scss
Normal 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
21
linearprogress/harness.ts
Normal 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'];
|
||||
}
|
||||
}
|
380
linearprogress/lib/_linear-progress.scss
Normal file
380
linearprogress/lib/_linear-progress.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
8
linearprogress/lib/linear-progress-styles.scss
Normal file
8
linearprogress/lib/linear-progress-styles.scss
Normal file
@ -0,0 +1,8 @@
|
||||
//
|
||||
// Copyright 2023 Google LLC
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
@use './linear-progress';
|
||||
|
||||
@include linear-progress.styles;
|
117
linearprogress/lib/linear-progress.ts
Normal file
117
linearprogress/lib/linear-progress.ts
Normal 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;
|
||||
}
|
||||
}
|
30
linearprogress/linear-progress.ts
Normal file
30
linearprogress/linear-progress.ts
Normal 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];
|
||||
}
|
15
linearprogress/linear-progress_test.ts
Normal file
15
linearprogress/linear-progress_test.ts
Normal 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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user