diff --git a/button/demo/demo.ts b/button/demo/demo.ts index f69762f75..33295e2dd 100644 --- a/button/demo/demo.ts +++ b/button/demo/demo.ts @@ -4,19 +4,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob, textInput} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Button', [ - new Knob('label', {ui: textInput(), defaultValue: ''}), - new Knob('disabled', {ui: boolInput(), defaultValue: false}), - ]); +const collection = new MaterialCollection>( + 'Button', + [ + new Knob('label', {ui: textInput(), defaultValue: ''}), + new Knob('disabled', {ui: boolInput(), defaultValue: false}), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/button/demo/stories.ts b/button/demo/stories.ts index ead4a67bf..96ee61bcb 100644 --- a/button/demo/stories.ts +++ b/button/demo/stories.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/icon/icon.js'; import '@material/web/button/elevated-button.js'; import '@material/web/button/filled-button.js'; +import '@material/web/button/filled-tonal-button.js'; import '@material/web/button/outlined-button.js'; import '@material/web/button/text-button.js'; -import '@material/web/button/filled-tonal-button.js'; +import '@material/web/icon/icon.js'; import {MaterialStoryInit} from './material-collection.js'; import {css, html} from 'lit'; @@ -88,7 +88,7 @@ const buttons: MaterialStoryInit = { `; - } + }, }; const links: MaterialStoryInit = { @@ -101,40 +101,35 @@ const links: MaterialStoryInit = { + trailing-icon> ${label || 'Filled'} + trailing-icon> ${label || 'Outlined'} + trailing-icon> ${label || 'Elevated'} + trailing-icon> ${label || 'Tonal'} + trailing-icon> ${label || 'Text'} @@ -142,8 +137,7 @@ const links: MaterialStoryInit = { + trailing-icon> open_in_new ${label || 'Filled'} @@ -151,8 +145,7 @@ const links: MaterialStoryInit = { + trailing-icon> open_in_new ${label || 'Outlined'} @@ -160,8 +153,7 @@ const links: MaterialStoryInit = { + trailing-icon> open_in_new ${label || 'Elevated'} @@ -169,8 +161,7 @@ const links: MaterialStoryInit = { + trailing-icon> open_in_new ${label || 'Tonal'} @@ -178,15 +169,14 @@ const links: MaterialStoryInit = { + trailing-icon> open_in_new ${label || 'Text'} `; - } + }, }; /** Button stories. */ diff --git a/button/elevated-button.ts b/button/elevated-button.ts index 2f5a1b477..6b94fef30 100644 --- a/button/elevated-button.ts +++ b/button/elevated-button.ts @@ -41,6 +41,9 @@ declare global { */ @customElement('md-elevated-button') export class MdElevatedButton extends ElevatedButton { - static override styles = - [sharedStyles, sharedElevationStyles, elevatedStyles]; + static override styles = [ + sharedStyles, + sharedElevationStyles, + elevatedStyles, + ]; } diff --git a/button/internal/button.ts b/button/internal/button.ts index bb7e2766a..501268dc3 100644 --- a/button/internal/button.ts +++ b/button/internal/button.ts @@ -10,13 +10,20 @@ import '../../ripple/ripple.js'; import {html, isServer, LitElement, nothing} from 'lit'; import {property, query, queryAssignedElements} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; -import {html as staticHtml, literal} from 'lit/static-html.js'; +import {literal, html as staticHtml} from 'lit/static-html.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; import {internals} from '../../internal/controller/element-internals.js'; -import {dispatchActivationClick, isActivationClick} from '../../internal/controller/events.js'; -import {FormSubmitter, FormSubmitterType, setupFormSubmitter} from '../../internal/controller/form-submitter.js'; +import { + dispatchActivationClick, + isActivationClick, +} from '../../internal/controller/events.js'; +import { + FormSubmitter, + FormSubmitterType, + setupFormSubmitter, +} from '../../internal/controller/form-submitter.js'; /** * A button component. @@ -31,8 +38,10 @@ export abstract class Button extends LitElement implements FormSubmitter { static readonly formAssociated = true; /** @nocollapse */ - static override shadowRootOptions: - ShadowRootInit = {mode: 'open', delegatesFocus: true}; + static override shadowRootOptions: ShadowRootInit = { + mode: 'open', + delegatesFocus: true, + }; /** * Whether or not the button is disabled. @@ -48,7 +57,7 @@ export abstract class Button extends LitElement implements FormSubmitter { * Where to display the linked `href` URL for a link button. Common options * include `_blank` to open in a new tab. */ - @property() target: '_blank'|'_parent'|'_self'|'_top'|'' = ''; + @property() target: '_blank' | '_parent' | '_self' | '_top' | '' = ''; /** * Whether to render the icon at the inline end of the label rather than the @@ -81,14 +90,14 @@ export abstract class Button extends LitElement implements FormSubmitter { return this[internals].form; } - @query('.button') private readonly buttonElement!: HTMLElement|null; + @query('.button') private readonly buttonElement!: HTMLElement | null; @queryAssignedElements({slot: 'icon', flatten: true}) private readonly assignedIcons!: HTMLElement[]; /** @private */ - [internals] = - (this as HTMLElement /* needed for closure */).attachInternals(); + [internals] = (this as HTMLElement) /* needed for closure */ + .attachInternals(); constructor() { super(); @@ -138,12 +147,12 @@ export abstract class Button extends LitElement implements FormSubmitter { private renderContent() { // Link buttons may not be disabled const isDisabled = this.disabled && !this.href; - const icon = - html``; + const icon = html``; return html` - ${this.renderElevation?.()} - ${this.renderOutline?.()} + ${this.renderElevation?.()} ${this.renderOutline?.()} @@ -154,7 +163,7 @@ export abstract class Button extends LitElement implements FormSubmitter { } private readonly handleActivationClick = (event: MouseEvent) => { - if (!isActivationClick((event)) || !this.buttonElement) { + if (!isActivationClick(event) || !this.buttonElement) { return; } this.focus(); diff --git a/catalog/src/components/catalog-component-header-title.ts b/catalog/src/components/catalog-component-header-title.ts index 611f7ac8b..aa7e3b2db 100644 --- a/catalog/src/components/catalog-component-header-title.ts +++ b/catalog/src/components/catalog-component-header-title.ts @@ -57,4 +57,4 @@ export class CatalogComponentHeaderTitle extends LitElement { } } `; -} \ No newline at end of file +} diff --git a/catalog/src/components/catalog-component-header.ts b/catalog/src/components/catalog-component-header.ts index f17184425..67482b635 100644 --- a/catalog/src/components/catalog-component-header.ts +++ b/catalog/src/components/catalog-component-header.ts @@ -17,13 +17,10 @@ import {customElement} from 'lit/decorators.js'; @customElement('catalog-component-header') export class CatalogComponentHeader extends LitElement { render() { - return html` -
- - -
`; + return html`
+ + +
`; } static styles = css` diff --git a/catalog/src/components/copy-code-button.ts b/catalog/src/components/copy-code-button.ts index 2ebb08119..b94fd8d05 100644 --- a/catalog/src/components/copy-code-button.ts +++ b/catalog/src/components/copy-code-button.ts @@ -4,12 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/iconbutton/icon-button.js'; import '@material/web/icon/icon.js'; +import '@material/web/iconbutton/icon-button.js'; import {MdIconButton} from '@material/web/iconbutton/icon-button.js'; import {css, html, LitElement} from 'lit'; -import {customElement, property, query, queryAssignedElements, state,} from 'lit/decorators.js'; +import { + customElement, + property, + query, + queryAssignedElements, + state, +} from 'lit/decorators.js'; /** * A custom element that places a copy button at the top right corner of a @@ -37,7 +43,7 @@ export class CopyCodeButton extends LitElement { } `; - private timeoutId: number|undefined; + private timeoutId: number | undefined; @state() private showCheckmark = false; @@ -72,8 +78,7 @@ export class CopyCodeButton extends LitElement { title=${this.buttonTitle} .selected=${this.showCheckmark} aria-label=${this.label} - aria-label-selected=${this.successLabel} - > + aria-label-selected=${this.successLabel}> content_copy checkmark diff --git a/catalog/src/components/hct-slider.ts b/catalog/src/components/hct-slider.ts index 5e105b522..4676036fb 100644 --- a/catalog/src/components/hct-slider.ts +++ b/catalog/src/components/hct-slider.ts @@ -48,7 +48,7 @@ export class HCTSlider extends LitElement { /** * The type of HCT slider to display */ - @property({type: String}) type: 'hue'|'chroma'|'tone' = 'hue'; + @property({type: String}) type: 'hue' | 'chroma' | 'tone' = 'hue'; override render() { let range = HUE_RANGE; @@ -62,21 +62,19 @@ export class HCTSlider extends LitElement { return html`
${this.label} + id="source" + labeled + aria-label=${this.label} + .min=${range[0]} + .max=${range[1]} + .value=${this.value} + @input=${this.onInput}>
+ background: this.buildGradient(), + })}>
`; } @@ -100,7 +98,7 @@ export class HCTSlider extends LitElement { if (this.type === 'hue') { for (let i = 0; i < numStops; i++) { - const hue = HUE_RANGE[1] / numStops * i; + const hue = (HUE_RANGE[1] / numStops) * i; // Set chroma to something fairly saturated + tone in the middle of // black and white so it's not too dark or too bright and vary the hue const hex = hexFromHct(hue, 100, 50); @@ -111,7 +109,7 @@ export class HCTSlider extends LitElement { const hue = hct.hue; for (let i = 0; i < numStops; i++) { - const chroma = CHROMA_RANGE[1] / numStops * i; + const chroma = (CHROMA_RANGE[1] / numStops) * i; // Change the color of the bar to the current hue and set the tone to // mid so we it's not too dark or too bright and vary the chroma const hex = hexFromHct(hue, chroma, 50); @@ -119,7 +117,7 @@ export class HCTSlider extends LitElement { } } else if (this.type === 'tone') { for (let i = 0; i < numStops; i++) { - const tone = TONE_RANGE[1] / numStops * i; + const tone = (TONE_RANGE[1] / numStops) * i; // Set tone color to black (0 chroma means that hue doesn't matter) and // vary the tone const hex = hexFromHct(0, 0, tone); diff --git a/catalog/src/components/nav-drawer.ts b/catalog/src/components/nav-drawer.ts index ed3ffa2b8..de9da1c4b 100644 --- a/catalog/src/components/nav-drawer.ts +++ b/catalog/src/components/nav-drawer.ts @@ -6,7 +6,7 @@ import {animate, fadeIn, fadeOut} from '@lit-labs/motion'; import {EASING} from '@material/web/internal/motion/animation.js'; -import {css, html, LitElement, nothing, PropertyValues} from 'lit'; +import {LitElement, PropertyValues, css, html, nothing} from 'lit'; import {customElement, property, state} from 'lit/decorators.js'; import {drawerOpenSignal} from '../signals/drawer-open-state.js'; @@ -21,8 +21,8 @@ import {SignalElement} from '../signals/signal-element.js'; * widths, and position itself inline with the page at wider page widths. Most * importantly, this sidebar is SSR compatible. */ -@customElement('nav-drawer') export class NavDrawer extends SignalElement -(LitElement) { +@customElement('nav-drawer') +export class NavDrawer extends SignalElement(LitElement) { /** * Whether or not the side drawer is collapsible or inline. */ @@ -45,8 +45,9 @@ import {SignalElement} from '../signals/signal-element.js'; const drawerContentOpacityDuration = showModal ? 300 : 150; const scrimOpacityDuration = 150; - const drawerSlideAnimationEasing = - showModal ? EASING.EMPHASIZED : EASING.EMPHASIZED_ACCELERATE; + const drawerSlideAnimationEasing = showModal + ? EASING.EMPHASIZED + : EASING.EMPHASIZED_ACCELERATE; return html`
@@ -65,8 +66,7 @@ import {SignalElement} from '../signals/signal-element.js'; }, in: fadeIn, out: fadeOut, - })} - >
` + })}>` : nothing} @@ -102,8 +100,7 @@ import {SignalElement} from '../signals/signal-element.js'; private renderContent(showModal: boolean) { return html`
+ ?inert=${showModal || inertContentSignal.value}>
@@ -119,8 +116,7 @@ import {SignalElement} from '../signals/signal-element.js'; return html`
+ ?inert=${showModal || inertContentSignal.value}>

On this page:

${this.pageTitle}

@@ -148,11 +144,16 @@ import {SignalElement} from '../signals/signal-element.js'; updated(changed: PropertyValues) { super.updated(changed); - if (this.lastDrawerOpen !== drawerOpenSignal.value && - drawerOpenSignal.value && this.isCollapsible) { - (this.querySelector('md-list.nav md-list-item[tabindex="0"]') as - HTMLElement) - ?.focus(); + if ( + this.lastDrawerOpen !== drawerOpenSignal.value && + drawerOpenSignal.value && + this.isCollapsible + ) { + ( + this.querySelector( + 'md-list.nav md-list-item[tabindex="0"]', + ) as HTMLElement + )?.focus(); } } diff --git a/catalog/src/components/theme-changer.ts b/catalog/src/components/theme-changer.ts index 79a0b804c..15a792e04 100644 --- a/catalog/src/components/theme-changer.ts +++ b/catalog/src/components/theme-changer.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/labs/segmentedbuttonset/outlined-segmented-button-set.js'; -import '@material/web/labs/segmentedbutton/outlined-segmented-button.js'; -import '@material/web/icon/icon.js'; -import './hct-slider.js'; -import './copy-code-button.js'; import '@material/web/focus/md-focus-ring.js'; +import '@material/web/icon/icon.js'; +import '@material/web/labs/segmentedbutton/outlined-segmented-button.js'; +import '@material/web/labs/segmentedbuttonset/outlined-segmented-button-set.js'; +import './copy-code-button.js'; +import './hct-slider.js'; import type {MdOutlinedSegmentedButton} from '@material/web/labs/segmentedbutton/outlined-segmented-button.js'; import {css, html, LitElement} from 'lit'; @@ -18,8 +18,12 @@ import {live} from 'lit/directives/live.js'; import {ChangeColorEvent, ChangeDarkModeEvent} from '../types/color-events.js'; import {hctFromHex, hexFromHct} from '../utils/material-color-helpers.js'; -import {getCurrentMode, getCurrentSeedColor, getCurrentThemeString} from '../utils/theme.js'; import type {ColorMode} from '../utils/theme.js'; +import { + getCurrentMode, + getCurrentSeedColor, + getCurrentThemeString, +} from '../utils/theme.js'; import type {HCTSlider} from './hct-slider.js'; @@ -37,7 +41,7 @@ export class ThemeChanger extends LitElement { /** * The currently selected color mode. */ - @state() selectedColorMode: ColorMode|null = null; + @state() selectedColorMode: ColorMode | null = null; /** * The currently selected hex color. @@ -68,17 +72,14 @@ export class ThemeChanger extends LitElement { render() { return html`
-

- Theme Controls -

+

Theme Controls

+ button-title="Copy current theme to clipboard" + label="Copy current theme" + .getCopyText=${getCurrentThemeString}>
- ${this.renderHexPicker()} - ${this.renderHctPicker()} + ${this.renderHexPicker()} ${this.renderHctPicker()} ${this.renderColorModePicker()} `; } @@ -96,8 +97,7 @@ export class ThemeChanger extends LitElement { id="color-input" @input=${this.onHexPickerInput} type="color" - .value=${live(this.hexColor)} - /> + .value=${live(this.hexColor)} />
@@ -111,27 +111,24 @@ export class ThemeChanger extends LitElement { private renderHctPicker() { return html`
+ .value=${live(this.hue)} + type="hue" + label="Hue" + max="360" + @input=${this.onSliderInput}> + .value=${live(this.chroma)} + .color=${this.hexColor} + type="chroma" + label="Chroma" + max="150" + @input=${this.onSliderInput}> + .value=${live(this.tone)} + type="tone" + label="Tone" + max="100" + @input=${this.onSliderInput}>
`; } @@ -140,9 +137,8 @@ export class ThemeChanger extends LitElement { */ private renderColorModePicker() { return html` + @segmented-button-set-selection=${this.onColorModeSelection} + aria-label="Color mode"> ${this.renderModeButton('dark', 'dark_mode')} ${this.renderModeButton('auto', 'brightness_medium')} ${this.renderModeButton('light', 'light_mode')} @@ -161,8 +157,7 @@ export class ThemeChanger extends LitElement { data-value=${mode} title=${mode} aria-label="${mode} color scheme" - .selected=${this.selectedColorMode === mode} - > + .selected=${this.selectedColorMode === mode}> ${icon} `; } @@ -208,9 +203,13 @@ export class ThemeChanger extends LitElement { this.updateHctFromHex(this.hexColor); } - private onColorModeSelection(e: CustomEvent<{ - button: MdOutlinedSegmentedButton; selected: boolean; index: number; - }>) { + private onColorModeSelection( + e: CustomEvent<{ + button: MdOutlinedSegmentedButton; + selected: boolean; + index: number; + }>, + ) { const {button} = e.detail; const value = button.dataset.value as ColorMode; this.selectedColorMode = value; diff --git a/catalog/src/components/top-app-bar.ts b/catalog/src/components/top-app-bar.ts index a9c38b949..ad613892c 100644 --- a/catalog/src/components/top-app-bar.ts +++ b/catalog/src/components/top-app-bar.ts @@ -5,8 +5,8 @@ */ import '@material/web/focus/md-focus-ring.js'; -import '@material/web/iconbutton/icon-button.js'; import '@material/web/icon/icon.js'; +import '@material/web/iconbutton/icon-button.js'; import type {MdIconButton} from '@material/web/iconbutton/icon-button.js'; import {css, html, LitElement} from 'lit'; @@ -21,8 +21,8 @@ import {materialDesign} from '../svg/material-design-logo.js'; /** * Top app bar of the catalog. */ -@customElement('top-app-bar') export class TopAppBar extends SignalElement -(LitElement) { +@customElement('top-app-bar') +export class TopAppBar extends SignalElement(LitElement) { /** * Whether or not the color picker menu is open. */ @@ -40,11 +40,11 @@ import {materialDesign} from '../svg/material-design-logo.js'; aria-label-selected="open navigation menu" aria-label="close navigation menu" aria-expanded=${drawerOpenSignal.value ? 'false' : 'true'} - title="${ - !drawerOpenSignal.value ? 'Open' : 'Close'} navigation menu" + title="${!drawerOpenSignal.value + ? 'Open' + : 'Close'} navigation menu" .selected=${live(!drawerOpenSignal.value)} - @input=${this.onMenuIconToggle} - > + @input=${this.onMenuIconToggle}> menu menu_open @@ -52,8 +52,7 @@ import {materialDesign} from '../svg/material-design-logo.js'; href="/" class="home-button" title="Home" - aria-label="Home" - > + aria-label="Home"> ${materialDesign} @@ -71,16 +70,14 @@ import {materialDesign} from '../svg/material-design-logo.js'; + id="menu-island"> + aria-expanded=${this.menuOpen ? 'true' : 'false'}> palette + @keydown=${this.onKeydown}> diff --git a/catalog/src/hydration-entrypoints/components/button.ts b/catalog/src/hydration-entrypoints/components/button.ts index 49744f2df..a13f00be4 100644 --- a/catalog/src/hydration-entrypoints/components/button.ts +++ b/catalog/src/hydration-entrypoints/components/button.ts @@ -8,4 +8,4 @@ import '@material/web/button/elevated-button.js'; import '@material/web/button/filled-button.js'; import '@material/web/button/filled-tonal-button.js'; import '@material/web/button/outlined-button.js'; -import '@material/web/button/text-button.js'; \ No newline at end of file +import '@material/web/button/text-button.js'; diff --git a/catalog/src/hydration-entrypoints/components/checkbox.ts b/catalog/src/hydration-entrypoints/components/checkbox.ts index 8e49dbede..8a81ee197 100644 --- a/catalog/src/hydration-entrypoints/components/checkbox.ts +++ b/catalog/src/hydration-entrypoints/components/checkbox.ts @@ -4,4 +4,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/checkbox/checkbox.js'; \ No newline at end of file +import '@material/web/checkbox/checkbox.js'; diff --git a/catalog/src/hydration-entrypoints/components/fab.ts b/catalog/src/hydration-entrypoints/components/fab.ts index 93c7a3eec..75d1803a5 100644 --- a/catalog/src/hydration-entrypoints/components/fab.ts +++ b/catalog/src/hydration-entrypoints/components/fab.ts @@ -4,5 +4,5 @@ * SPDX-License-Identifier: Apache-2.0 */ +import '@material/web/fab/branded-fab.js'; import '@material/web/fab/fab.js'; -import '@material/web/fab/branded-fab.js'; \ No newline at end of file diff --git a/catalog/src/hydration-entrypoints/components/icon-button.ts b/catalog/src/hydration-entrypoints/components/icon-button.ts index 8f5e00a15..f19f0f98b 100644 --- a/catalog/src/hydration-entrypoints/components/icon-button.ts +++ b/catalog/src/hydration-entrypoints/components/icon-button.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/iconbutton/icon-button.js'; import '@material/web/iconbutton/filled-icon-button.js'; import '@material/web/iconbutton/filled-tonal-icon-button.js'; +import '@material/web/iconbutton/icon-button.js'; import '@material/web/iconbutton/outlined-icon-button.js'; diff --git a/catalog/src/hydration-entrypoints/components/menu.ts b/catalog/src/hydration-entrypoints/components/menu.ts index fa3149849..f0d9e8117 100644 --- a/catalog/src/hydration-entrypoints/components/menu.ts +++ b/catalog/src/hydration-entrypoints/components/menu.ts @@ -7,4 +7,4 @@ import '@material/web/button/filled-button.js'; import '@material/web/menu/menu.js'; import '@material/web/menu/menu-item.js'; -import '@material/web/menu/sub-menu.js'; \ No newline at end of file +import '@material/web/menu/sub-menu.js'; diff --git a/catalog/src/hydration-entrypoints/components/select.ts b/catalog/src/hydration-entrypoints/components/select.ts index b8cfe1ea3..46253aa56 100644 --- a/catalog/src/hydration-entrypoints/components/select.ts +++ b/catalog/src/hydration-entrypoints/components/select.ts @@ -4,6 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/select/select-option.js'; -import '@material/web/select/outlined-select.js'; import '@material/web/select/filled-select.js'; +import '@material/web/select/outlined-select.js'; +import '@material/web/select/select-option.js'; diff --git a/catalog/src/hydration-entrypoints/components/text-field.ts b/catalog/src/hydration-entrypoints/components/text-field.ts index 65b98d197..9c92e387d 100644 --- a/catalog/src/hydration-entrypoints/components/text-field.ts +++ b/catalog/src/hydration-entrypoints/components/text-field.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/textfield/filled-text-field.js'; -import '@material/web/textfield/outlined-text-field.js'; import '@material/web/icon/icon.js'; import '@material/web/iconbutton/icon-button.js'; +import '@material/web/textfield/filled-text-field.js'; +import '@material/web/textfield/outlined-text-field.js'; diff --git a/catalog/src/hydration-entrypoints/navigation.ts b/catalog/src/hydration-entrypoints/navigation.ts index 4c256f022..c116461e7 100644 --- a/catalog/src/hydration-entrypoints/navigation.ts +++ b/catalog/src/hydration-entrypoints/navigation.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import '@material/web/list/list.js'; +import '@material/web/list/list-item.js'; import '../components/nav-drawer.js'; import '../components/top-app-bar.js'; -import '@material/web/list/list-item.js'; -import '@material/web/list/list.js'; diff --git a/catalog/src/hydration-entrypoints/playground-elements.ts b/catalog/src/hydration-entrypoints/playground-elements.ts index ea42d5c03..0b8b52113 100644 --- a/catalog/src/hydration-entrypoints/playground-elements.ts +++ b/catalog/src/hydration-entrypoints/playground-elements.ts @@ -4,6 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import 'playground-elements/playground-project.js'; +import 'playground-elements/playground-file-editor.js'; import 'playground-elements/playground-preview.js'; -import 'playground-elements/playground-file-editor.js'; \ No newline at end of file +import 'playground-elements/playground-project.js'; diff --git a/catalog/src/pages/components.ts b/catalog/src/pages/components.ts index 93c48c0a1..7f6a26ed3 100644 --- a/catalog/src/pages/components.ts +++ b/catalog/src/pages/components.ts @@ -20,7 +20,9 @@ import {getCurrentThemeString} from '../utils/theme.js'; * @param previewEl An element reference to the playground preview element. */ async function updateMessageTargetOnIframeLoad( - postdoc: PostDoc, previewEl: PlaygroundPreview) { + postdoc: PostDoc, + previewEl: PlaygroundPreview, +) { await previewEl.updateComplete; const iframe = previewEl.iframe!; @@ -76,9 +78,9 @@ function demoDropdown() { // tslint:disable:no-unnecessary-type-assertion TSC externally seems to differ // from internal here and needs these type assertions - const expandButton = - detailsEl?.querySelector('summary md-outlined-icon-button') as - MdOutlinedIconButton; + const expandButton = detailsEl?.querySelector( + 'summary md-outlined-icon-button', + ) as MdOutlinedIconButton; // tslint:enable:no-unnecessary-type-assertion // Synchronize details open state with toggle button diff --git a/catalog/src/pages/global.ts b/catalog/src/pages/global.ts index c4e700137..7e3185362 100644 --- a/catalog/src/pages/global.ts +++ b/catalog/src/pages/global.ts @@ -12,7 +12,16 @@ * and global scroll listeners. */ -import {changeColor, changeColorAndMode, changeColorMode, getCurrentMode, getCurrentSeedColor, getCurrentThemeString, getLastSavedAutoColorMode, isModeDark} from '../utils/theme.js'; +import { + changeColor, + changeColorAndMode, + changeColorMode, + getCurrentMode, + getCurrentSeedColor, + getCurrentThemeString, + getLastSavedAutoColorMode, + isModeDark, +} from '../utils/theme.js'; /** * Applies theme-based event listeners such as changing color, mode, and @@ -29,14 +38,15 @@ function applyColorThemeListeners() { // Listen for system color change and applies the new theme if the current // color mode is 'auto'. - window.matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', () => { - if (getCurrentMode() !== 'auto') { - return; - } + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', () => { + if (getCurrentMode() !== 'auto') { + return; + } - changeColor(getCurrentSeedColor()!); - }); + changeColor(getCurrentSeedColor()!); + }); } /** diff --git a/catalog/src/signals/drawer-open-state.ts b/catalog/src/signals/drawer-open-state.ts index 8c90c61e5..875fc0bc6 100644 --- a/catalog/src/signals/drawer-open-state.ts +++ b/catalog/src/signals/drawer-open-state.ts @@ -9,4 +9,4 @@ import {signal} from './signal-element.js'; /** * Whether or not the sidebar drawer should be open. */ -export const drawerOpenSignal = signal(false); \ No newline at end of file +export const drawerOpenSignal = signal(false); diff --git a/catalog/src/signals/inert.ts b/catalog/src/signals/inert.ts index 001990092..3199c4e25 100644 --- a/catalog/src/signals/inert.ts +++ b/catalog/src/signals/inert.ts @@ -15,4 +15,4 @@ export const inertContentSignal = signal(false); /** * Whether or not the sidebar should be inert. */ -export const inertSidebarSignal = signal(false); \ No newline at end of file +export const inertSidebarSignal = signal(false); diff --git a/catalog/src/signals/signal-element.ts b/catalog/src/signals/signal-element.ts index f083b68cb..e57b74a95 100644 --- a/catalog/src/signals/signal-element.ts +++ b/catalog/src/signals/signal-element.ts @@ -18,8 +18,9 @@ type ReactiveElementConstructor = new (...args: any[]) => ReactiveElement; * * @param Base The class to mix-in and listen to Preact signal changes. */ -export function SignalElement(Base: T): - T { +export function SignalElement( + Base: T, +): T { return class SignalElement extends Base { private _disposeEffect?: () => void; @@ -39,4 +40,4 @@ export function SignalElement(Base: T): }); } }; -} \ No newline at end of file +} diff --git a/catalog/src/ssr-utils/dsd-polyfill.ts b/catalog/src/ssr-utils/dsd-polyfill.ts index 35f79dc46..6f64fdc2a 100644 --- a/catalog/src/ssr-utils/dsd-polyfill.ts +++ b/catalog/src/ssr-utils/dsd-polyfill.ts @@ -10,4 +10,4 @@ import {hydrateShadowRoots} from '@webcomponents/template-shadowroot/template-sh if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) { hydrateShadowRoots(document.body); } -document.body.removeAttribute('dsd-pending'); \ No newline at end of file +document.body.removeAttribute('dsd-pending'); diff --git a/catalog/src/ssr-utils/lit-hydrate-support.ts b/catalog/src/ssr-utils/lit-hydrate-support.ts index 23b4acc13..0158fd1a7 100644 --- a/catalog/src/ssr-utils/lit-hydrate-support.ts +++ b/catalog/src/ssr-utils/lit-hydrate-support.ts @@ -6,4 +6,4 @@ // Must be loaded before any lit element see /site/_includes/default.html for // usage. Allows hydrating SSRd lit elements. -import '@lit-labs/ssr-client/lit-element-hydrate-support.js'; \ No newline at end of file +import '@lit-labs/ssr-client/lit-element-hydrate-support.js'; diff --git a/catalog/src/ssr-utils/lit-island.ts b/catalog/src/ssr-utils/lit-island.ts index 23f114385..6a87af037 100644 --- a/catalog/src/ssr-utils/lit-island.ts +++ b/catalog/src/ssr-utils/lit-island.ts @@ -6,7 +6,10 @@ import {Island} from '@11ty/is-land'; -customElements.define('lit-island', class extends Island { - // Removes the feature in which 11ty island removes DOM to render a fallback. - override forceFallback() {} -}); +customElements.define( + 'lit-island', + class extends Island { + // Removes the feature in which 11ty island removes DOM to render a fallback. + override forceFallback() {} + }, +); diff --git a/catalog/src/ssr.ts b/catalog/src/ssr.ts index 955e3b5f7..52ea560e7 100644 --- a/catalog/src/ssr.ts +++ b/catalog/src/ssr.ts @@ -6,11 +6,11 @@ // This file imports only files that will be SSRd e.g. if you can't SSR a // component, don't import it here. +import '@material/web/all.js'; import './components/catalog-component-header.js'; import './components/catalog-component-header-title.js'; -import './components/top-app-bar.js'; import './components/nav-drawer.js'; import './components/theme-changer.js'; -import '@material/web/all.js'; +import './components/top-app-bar.js'; // 🤫 -import '@material/web/labs/item/item.js'; \ No newline at end of file +import '@material/web/labs/item/item.js'; diff --git a/catalog/src/svg/material-design-logo.ts b/catalog/src/svg/material-design-logo.ts index 29a21fe1c..164f65884 100644 --- a/catalog/src/svg/material-design-logo.ts +++ b/catalog/src/svg/material-design-logo.ts @@ -11,8 +11,7 @@ import {html} from 'lit'; * * Source: Internal google symbols search. */ -export const materialDesign = html` - { touch-target="none" style="margin-inline-end: 16px;" .checked=${!!knob.latestValue} - @change="${valueChanged}" - > + @change="${valueChanged}"> ${knob.name} @@ -70,11 +69,17 @@ export class KnobColorSelector extends LitElement { box-sizing: content-box; width: var(--_color-picker-size); height: var(--_color-picker-size); - margin: calc((var(--_component-size) - var(--_color-picker-size) - var(--_color-picker-border-width) * 2) / 2); + margin: calc( + ( + var(--_component-size) - var(--_color-picker-size) - + var(--_color-picker-border-width) * 2 + ) / 2 + ); padding: 0; cursor: pointer; border-radius: var(--_color-picker-border-width); - border: var(--_color-picker-border-width) solid var(--md-sys-color-outline); + border: var(--_color-picker-border-width) solid + var(--md-sys-color-outline); outline: none; } @@ -126,9 +131,8 @@ export class KnobColorSelector extends LitElement { { - this.hasAlpha = !this.hasAlpha; - }} - > + this.hasAlpha = !this.hasAlpha; + }}> ${this.hasAlpha ? 'rgba' : 'rgb'} `; @@ -139,8 +143,7 @@ export class KnobColorSelector extends LitElement { style=${styleMap(sharedTextFieldStyles)} .value=${this.value} @change=${this.propagateEvt} - @input=${this.onInput} - >`; + @input=${this.onInput}>`; } private renderColorInput() { @@ -155,8 +158,7 @@ export class KnobColorSelector extends LitElement { id="color-picker" .value=${this.value} @change=${this.propagateEvt} - @input=${this.onInput} - /> + @input=${this.onInput} /> `; } @@ -176,14 +178,16 @@ export class KnobColorSelector extends LitElement { override click() { const input = this.renderRoot!.querySelector( - 'input,md-filled-text-field') as HTMLElement; + 'input,md-filled-text-field', + ) as HTMLElement; input.click(); input.focus(); } override focus() { const input = this.renderRoot!.querySelector( - 'input,md-filled-text-field') as HTMLElement; + 'input,md-filled-text-field', + ) as HTMLElement; input.focus(); } } @@ -222,8 +226,7 @@ export function colorPicker(opts?: ColorPickerOpts): KnobUi { + @input=${valueChanged}> ${knob.name}
@@ -269,8 +272,7 @@ export function textInput(options?: TextInputOptions): KnobUi { + @input="${valueChanged}"> ${knob.name}
@@ -311,8 +313,7 @@ export function numberInput(opts?: NumberInputOpts): KnobUi { type="number" step="${config.step}" .value="${knob.latestValue ? knob.latestValue.toString() : '0'}" - @input="${valueChanged}" - > + @input="${valueChanged}"> ${knob.name}
@@ -343,7 +344,7 @@ export function button(): KnobUi { } interface RadioSelectorConfig { - readonly options: ReadonlyArray<{readonly value: T; readonly label: string;}>; + readonly options: ReadonlyArray<{readonly value: T; readonly label: string}>; readonly name: string; } @@ -364,8 +365,7 @@ export function radioSelector({ name="${name}" value="${value}" @change="${valueChanged}" - ?checked="${knob.latestValue === option.value}" - > + ?checked="${knob.latestValue === option.value}"> ${option.label} `; }); @@ -375,15 +375,15 @@ export function radioSelector({ } interface SelectDropdownConfig { - readonly options: ReadonlyArray<{readonly value: T; readonly label: string;}>; + readonly options: ReadonlyArray<{readonly value: T; readonly label: string}>; } /** A select dropdown Knob UI. */ export function selectDropdown({ options, -}: SelectDropdownConfig): KnobUi { +}: SelectDropdownConfig): KnobUi { return { - render(knob: Knob, onChange: (val: T) => void) { + render(knob: Knob, onChange: (val: T) => void) { const valueChanged = (e: Event) => { onChange((e.target as HTMLInputElement).value as T); }; @@ -391,15 +391,14 @@ export function selectDropdown({ return html``; + .headline=${option.label}>`; }); return html`
`; @@ -99,8 +98,7 @@ export class StoryKnobPanel extends LitElement { + @click=${this.onDragIconClick}> ${iconSvg} `; @@ -127,11 +125,13 @@ export class StoryKnobPanel extends LitElement { super.updated(changed); if (changed.has('open')) { - this.dispatchEvent(new CustomEvent('open-changed', { - detail: { - open: this.open, - }, - })); + this.dispatchEvent( + new CustomEvent('open-changed', { + detail: { + open: this.open, + }, + }), + ); } } @@ -181,11 +181,12 @@ export class StoryKnobPanel extends LitElement { } const rightBound = DEFAULT_DIMENSIONS.RIGHT_OFFSET; - const leftBound = DEFAULT_DIMENSIONS.RIGHT_OFFSET + this.containerWidth - - window.innerWidth; + const leftBound = + DEFAULT_DIMENSIONS.RIGHT_OFFSET + this.containerWidth - window.innerWidth; const topBound = -DEFAULT_DIMENSIONS.TOP_OFFSET; - const bottomBound = window.innerHeight - - (DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET); + const bottomBound = + window.innerHeight - + (DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET); // do not allow drag outside right bound if (x > rightBound) { diff --git a/catalog/stories/components/story-renderer.ts b/catalog/stories/components/story-renderer.ts index 9446ec215..044a338f8 100644 --- a/catalog/stories/components/story-renderer.ts +++ b/catalog/stories/components/story-renderer.ts @@ -6,7 +6,7 @@ /* Slimmed down version of Lit stories story-renderer without IE renderer */ -import {css, LitElement, PropertyValues,} from 'lit'; +import {css, LitElement, PropertyValues} from 'lit'; import {customElement, property} from 'lit/decorators.js'; import {Story} from '../story.js'; @@ -24,7 +24,7 @@ export class StoryRenderer extends LitElement { `, ]; @property({attribute: false}) story?: Story = undefined; - private storyRenderComplete: Promise|undefined = undefined; + private storyRenderComplete: Promise | undefined = undefined; override updated(propertiesChanged: PropertyValues) { super.updated(propertiesChanged); diff --git a/catalog/stories/index.ts b/catalog/stories/index.ts index 084461adf..615c64a60 100644 --- a/catalog/stories/index.ts +++ b/catalog/stories/index.ts @@ -10,4 +10,4 @@ export * from './knobs.js'; export * from './story.js'; // This file is resolved by base.json -import './components/stories-renderer.js'; \ No newline at end of file +import './components/stories-renderer.js'; diff --git a/catalog/stories/knobs.ts b/catalog/stories/knobs.ts index 559f8d6c8..4f3041879 100644 --- a/catalog/stories/knobs.ts +++ b/catalog/stories/knobs.ts @@ -53,8 +53,9 @@ export class Knob extends EventTarget { private readonly onReset = () => { this.reset(); }; - private readonly renderedStoryContainers = - new Set(); + private readonly renderedStoryContainers = new Set< + HTMLElement | DocumentFragment + >(); constructor(readonly name: Name, init: KnobInit) { super(); @@ -80,25 +81,28 @@ export class Knob extends EventTarget { * Connect the knob's wiring, if any, up to a container of a rendered story. * This is fast and idempotent, so it's fine to call frequently. */ - connectWiring(containerOfRenderedStory: HTMLElement|DocumentFragment) { + connectWiring(containerOfRenderedStory: HTMLElement | DocumentFragment) { // Fast path the common case where we have no wiring. if (!this.wiring) { return; } - const alreadyWired = - this.renderedStoryContainers.has(containerOfRenderedStory); + const alreadyWired = this.renderedStoryContainers.has( + containerOfRenderedStory, + ); if (!alreadyWired) { this.renderedStoryContainers.add(containerOfRenderedStory); // Ensure default values are wired correctly. - if (this.dirty || - (this.latestValue !== undefined && - this.latestValue === this.defaultValue)) { + if ( + this.dirty || + (this.latestValue !== undefined && + this.latestValue === this.defaultValue) + ) { this.wiring?.(this, this.latestValue!, containerOfRenderedStory); } } } - disconnectWiring(containerOfRenderedStory: HTMLElement|DocumentFragment) { + disconnectWiring(containerOfRenderedStory: HTMLElement | DocumentFragment) { return this.renderedStoryContainers.delete(containerOfRenderedStory); } @@ -139,15 +143,17 @@ type KnobKeys = Knobs[number]['name']; * * This type operator would return `number`. */ -type TypeOfKnobWithName = - Extract extends Knob? - U|undefined : - never; +type TypeOfKnobWithName< + Knobs extends PolymorphicArrayOfKnobs, + SearchName extends string, +> = Extract extends Knob + ? U | undefined + : never; /** A helper class for getting the latest value for a knob by name. */ -export class KnobValues extends - EventTarget { +export class KnobValues< + Knobs extends PolymorphicArrayOfKnobs, +> extends EventTarget { private readonly byName: ReadonlyMap>; constructor(knobsArray: PolymorphicArrayOfKnobs) { @@ -156,25 +162,32 @@ export class KnobValues extends for (const knob of knobsArray) { if (byName.has(knob.name)) { throw new Error( - `More than one knob with name '${knob.name}' given to a story.`); + `More than one knob with name '${knob.name}' given to a story.`, + ); } byName.set(knob.name, knob); knob.addEventListener('changed', (e) => { this.dispatchEvent( - new CustomEvent('changed', {detail: {knobName: knob.name}})); + new CustomEvent('changed', {detail: {knobName: knob.name}}), + ); }); } this.byName = byName; } - get>(knobName: SearchName): - TypeOfKnobWithName { - return this.byName.get(knobName)?.latestValue as - TypeOfKnobWithName; + get>( + knobName: SearchName, + ): TypeOfKnobWithName { + return this.byName.get(knobName)?.latestValue as TypeOfKnobWithName< + Knobs, + SearchName + >; } set>( - knobName: SearchName, newValue: TypeOfKnobWithName) { + knobName: SearchName, + newValue: TypeOfKnobWithName, + ) { const knob = this.byName.get(knobName); if (knob === undefined) { throw new Error(`No knob with name ${knobName}`); @@ -210,7 +223,7 @@ export class KnobValues extends * Unlikely that any code outside of the stories system internals would * call this. */ - connectWiring(container: HTMLElement|DocumentFragment) { + connectWiring(container: HTMLElement | DocumentFragment) { for (const knob of this.byName.values()) { if (container instanceof DocumentFragment) { container = container.firstElementChild as HTMLElement; @@ -225,7 +238,7 @@ export class KnobValues extends * * Returns false if the container wasn't actually connected. */ - disconnectWiring(container: HTMLElement|DocumentFragment) { + disconnectWiring(container: HTMLElement | DocumentFragment) { let disconnected = false; for (const knob of this.byName.values()) { disconnected = knob.disconnectWiring(container) || disconnected; @@ -261,8 +274,11 @@ export interface KnobUi { * and wiring may treat this case differently (e.g. restoring a value * to the value it had before the wiring set it the first time). */ - render(knob: Knob, onChange: (val: T) => void, onReset: () => void): - TemplateResult; + render( + knob: Knob, + onChange: (val: T) => void, + onReset: () => void, + ): TemplateResult; } /** @@ -276,6 +292,9 @@ export interface KnobUi { * wired up by just applying styles to the containerOfRenderedStory. */ export interface KnobWiring { - (knob: Knob, val: T, - containerOfRenderedStory: HTMLElement|DocumentFragment): void; + ( + knob: Knob, + val: T, + containerOfRenderedStory: HTMLElement | DocumentFragment, + ): void; } diff --git a/catalog/stories/material-collection.ts b/catalog/stories/material-collection.ts index ca7648b06..9534a3ec8 100644 --- a/catalog/stories/material-collection.ts +++ b/catalog/stories/material-collection.ts @@ -52,11 +52,11 @@ export function title(): KnobUi { * ``` */ export type KnobTypesToKnobs< - // tslint:disable-next-line:no-any No way to represent this type clearly. - T extends {[name: string]: any}, - Names extends Extract = Extract, - // tslint:disable-next-line:no-any We need to "map" the union type to knobs. - > = ReadonlyArray: never>; + // tslint:disable-next-line:no-any No way to represent this type clearly. + T extends {[name: string]: any}, + Names extends Extract = Extract, + // tslint:disable-next-line:no-any We need to "map" the union type to knobs. +> = ReadonlyArray : never>; /** * An init object for Material Stories. This should be exposed to the user. @@ -86,7 +86,7 @@ export type KnobTypesToKnobs< export interface MaterialStoryInit { name: string; render: (knobs: T) => TemplateResult | Promise; - styles?: CSSResult|CSSResult[]; + styles?: CSSResult | CSSResult[]; } /** @@ -94,8 +94,8 @@ export interface MaterialStoryInit { */ // tslint:disable-next-line:no-any No way to represent this type clearly. export function materialInitsToStoryInits( - inits: Array>): - Array>>> { + inits: Array>, +): Array>>> { return inits.map((init) => { return { name: init.name, @@ -119,4 +119,4 @@ export function setUpDemo(collection: LitCollection): void { const renderer = document.createElement('stories-renderer'); renderer.collection = collection; document.body.appendChild(renderer); -} \ No newline at end of file +} diff --git a/catalog/stories/story.ts b/catalog/stories/story.ts index e9274798d..2a5b0dc74 100644 --- a/catalog/stories/story.ts +++ b/catalog/stories/story.ts @@ -24,22 +24,23 @@ export interface BaseStoryInit { type GenericKnobValues = KnobValues; /** A story with an arbitrary render function. */ -export interface StoryInit< - KV extends GenericKnobValues = GenericKnobValues> extends BaseStoryInit { - render(container: HTMLElement|DocumentFragment, knobs: KV): Promise; +export interface StoryInit + extends BaseStoryInit { + render(container: HTMLElement | DocumentFragment, knobs: KV): Promise; styles?: CSSStyleSheet[]; } -class StoryImpl { +class StoryImpl< + Knobs extends PolymorphicArrayOfKnobs = PolymorphicArrayOfKnobs, +> { readonly name: string; readonly id: string; - readonly description: string|undefined; - readonly render: (container: HTMLElement|DocumentFragment) => Promise; - readonly dispose: (container: HTMLElement|DocumentFragment) => void; + readonly description: string | undefined; + readonly render: (container: HTMLElement | DocumentFragment) => Promise; + readonly dispose: (container: HTMLElement | DocumentFragment) => void; readonly knobs: KnobValues; - private readonly initStyles: CSSStyleSheet[]|undefined; + private readonly initStyles: CSSStyleSheet[] | undefined; get styles() { let styles = [...this.collection.customStyles]; @@ -54,19 +55,22 @@ class StoryImpl(); + const wrapperDivMap = new WeakMap< + HTMLElement | DocumentFragment, + HTMLDivElement + >(); this.initStyles = init.styles; this.knobs = collection.knobs; - this.render = async (container: HTMLElement|DocumentFragment) => { + this.render = async (container: HTMLElement | DocumentFragment) => { let wrapperDiv = wrapperDivMap.get(container); if (wrapperDiv === undefined) { wrapperDiv = document.createElement('div'); @@ -77,7 +81,7 @@ class StoryImpl { + this.dispose = (container: HTMLElement | DocumentFragment) => { wrapperDivMap.delete(container); render(nothing, container); }; @@ -93,8 +97,9 @@ class StoryImpl = StoryImpl; +export type Story< + Knobs extends PolymorphicArrayOfKnobs = PolymorphicArrayOfKnobs, +> = StoryImpl; /** * A tree of related stories and sub-collections. @@ -106,9 +111,10 @@ export type Story>> { - private readonly children = new Map(); +export class Collection< + T extends PolymorphicArrayOfKnobs = ReadonlyArray>, +> { + private readonly children = new Map(); readonly customStyles: CSSStyleSheet[] = []; private static readonly collectionsByName = new Map(); readonly knobs: KnobValues; @@ -136,7 +142,7 @@ export class Collection|Collection> { + get tree(): ReadonlyMap | Collection> { return this.children; } @@ -148,8 +154,9 @@ export class Collection = - KnobValues> extends BaseStoryInit { - renderLit(knobs: KV): TemplateResult|Promise; - litStyles?: CSSResult|CSSResult[]; + KV extends KnobValues = KnobValues, +> extends BaseStoryInit { + renderLit(knobs: KV): TemplateResult | Promise; + litStyles?: CSSResult | CSSResult[]; } function isLitStoryInit(init: Partial): init is LitStoryInit { @@ -181,19 +188,21 @@ function isLitStoryInit(init: Partial): init is LitStoryInit { /** * A collection with convenience methods for rendering lit-html templates. */ -export class LitCollection>> extends - Collection { +export class LitCollection< + T extends PolymorphicArrayOfKnobs = ReadonlyArray>, +> extends Collection { override addStories( - ...inits: Array>|LitStoryInit>>) { + ...inits: Array> | LitStoryInit>> + ) { const simpleInits: StoryInit[] = []; for (const init of inits) { if (isLitStoryInit(init)) { let styles: CSSStyleSheet[] = []; if (init.litStyles) { - styles = init.litStyles instanceof Array ? - init.litStyles.map((s) => s.styleSheet!) : - [init.litStyles.styleSheet!]; + styles = + init.litStyles instanceof Array + ? init.litStyles.map((s) => s.styleSheet!) + : [init.litStyles.styleSheet!]; } simpleInits.push({ ...init, diff --git a/catalog/stories/theme-loader.ts b/catalog/stories/theme-loader.ts index 91a8047b5..eda95aa61 100644 --- a/catalog/stories/theme-loader.ts +++ b/catalog/stories/theme-loader.ts @@ -44,7 +44,6 @@ const postdoc = new PostDoc({ onMessage, }); - await postdoc.handshake; // Request the initial theme. diff --git a/checkbox/checkbox_test.ts b/checkbox/checkbox_test.ts index d73db3969..0b58261bc 100644 --- a/checkbox/checkbox_test.ts +++ b/checkbox/checkbox_test.ts @@ -20,63 +20,68 @@ describe('', () => { describe('forms', () => { createFormTests({ - queryControl: root => root.querySelector('md-checkbox'), + queryControl: (root) => root.querySelector('md-checkbox'), valueTests: [ { name: 'unnamed', render: () => html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form without a name') - .toHaveSize(0); - } + .withContext('should not add anything to form without a name') + .toHaveSize(0); + }, }, { name: 'unchecked', render: () => html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when unchecked') - .toHaveSize(0); - } + .withContext('should not add anything to form when unchecked') + .toHaveSize(0); + }, }, { name: 'checked default value', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('checkbox')).toBe('on'); - } + }, }, { name: 'checked custom value', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('checkbox')).toBe('Custom value'); - } + }, }, { name: 'indeterminate', render: () => - html``, + html``, assertValue(formData) { expect(formData) - .withContext( - 'should not add anything to form when indeterminate') - .toHaveSize(0); - } + .withContext('should not add anything to form when indeterminate') + .toHaveSize(0); + }, }, { name: 'disabled', render: () => - html``, + html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when disabled') - .toHaveSize(0); - } - } + .withContext('should not add anything to form when disabled') + .toHaveSize(0); + }, + }, ], resetTests: [ { @@ -87,36 +92,36 @@ describe('', () => { }, assertReset(checkbox) { expect(checkbox.checked) - .withContext('checkbox.checked after reset') - .toBeFalse(); - } + .withContext('checkbox.checked after reset') + .toBeFalse(); + }, }, { name: 'reset to checked', render: () => - html``, + html``, change(checkbox) { checkbox.checked = false; }, assertReset(checkbox) { expect(checkbox.checked) - .withContext('checkbox.checked after reset') - .toBeTrue(); - } + .withContext('checkbox.checked after reset') + .toBeTrue(); + }, }, { name: 'reset to indeterminate', render: () => - html``, + html``, change(checkbox) { checkbox.indeterminate = false; }, assertReset(checkbox) { expect(checkbox.indeterminate) - .withContext('checkbox.indeterminate should not be reset') - .toBeFalse(); - } - } + .withContext('checkbox.indeterminate should not be reset') + .toBeFalse(); + }, + }, ], restoreTests: [ { @@ -124,31 +129,31 @@ describe('', () => { render: () => html``, assertRestored(checkbox) { expect(checkbox.checked) - .withContext('checkbox.checked after restore') - .toBeFalse(); - } + .withContext('checkbox.checked after restore') + .toBeFalse(); + }, }, { name: 'restore checked', render: () => - html``, + html``, assertRestored(checkbox) { expect(checkbox.checked) - .withContext('checkbox.checked after restore') - .toBeTrue(); - } + .withContext('checkbox.checked after restore') + .toBeTrue(); + }, }, { name: 'restore indeterminate', render: () => - html``, + html``, assertRestored(checkbox) { expect(checkbox.indeterminate) - .withContext('checkbox.indeterminate should not be restored') - .toBeFalse(); - } - } - ] + .withContext('checkbox.indeterminate should not be restored') + .toBeFalse(); + }, + }, + ], }); }); }); diff --git a/checkbox/demo/demo.ts b/checkbox/demo/demo.ts index c520af9d7..79767ec5a 100644 --- a/checkbox/demo/demo.ts +++ b/checkbox/demo/demo.ts @@ -4,20 +4,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Checkbox', [ - new Knob('checked', {defaultValue: false, ui: boolInput()}), - new Knob('indeterminate', {defaultValue: false, ui: boolInput()}), - new Knob('disabled', {defaultValue: false, ui: boolInput()}), - ]); +const collection = new MaterialCollection>( + 'Checkbox', + [ + new Knob('checked', {defaultValue: false, ui: boolInput()}), + new Knob('indeterminate', {defaultValue: false, ui: boolInput()}), + new Knob('disabled', {defaultValue: false, ui: boolInput()}), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/checkbox/demo/stories.ts b/checkbox/demo/stories.ts index c4c7f0acd..f91d88880 100644 --- a/checkbox/demo/stories.ts +++ b/checkbox/demo/stories.ts @@ -6,7 +6,10 @@ import '@material/web/checkbox/checkbox.js'; -import {labelStyles, MaterialStoryInit} from './material-collection.js'; +import { + labelStyles, + MaterialStoryInit, +} from './material-collection.js'; import {css, html} from 'lit'; /** Knob types for checkbox stories. */ @@ -25,8 +28,7 @@ const checkbox: MaterialStoryInit = { ?checked=${checked} ?disabled=${disabled} ?indeterminate=${indeterminate} - touch-target="wrapper" - > + touch-target="wrapper"> `; }, }; @@ -53,8 +55,7 @@ const withLabels: MaterialStoryInit = { + touch-target="wrapper"> Cats diff --git a/checkbox/internal/checkbox.ts b/checkbox/internal/checkbox.ts index f0911483a..941b88b49 100644 --- a/checkbox/internal/checkbox.ts +++ b/checkbox/internal/checkbox.ts @@ -13,7 +13,11 @@ import {classMap} from 'lit/directives/class-map.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; -import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../internal/controller/events.js'; +import { + dispatchActivationClick, + isActivationClick, + redispatchEvent, +} from '../../internal/controller/events.js'; /** * A checkbox component. @@ -26,7 +30,7 @@ export class Checkbox extends LitElement { /** @nocollapse */ static override shadowRootOptions = { ...LitElement.shadowRootOptions, - delegatesFocus: true + delegatesFocus: true, }; /** @nocollapse */ @@ -126,12 +130,12 @@ export class Checkbox extends LitElement { @state() private prevChecked = false; @state() private prevDisabled = false; @state() private prevIndeterminate = false; - @query('input') private readonly input!: HTMLInputElement|null; + @query('input') private readonly input!: HTMLInputElement | null; // Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432 // Replace with this.internals.validity.customError when resolved. private hasCustomValidityError = false; - private readonly internals = - (this as HTMLElement /* needed for closure */).attachInternals(); + // Cast needed for closure + private readonly internals = (this as HTMLElement).attachInternals(); constructor() { super(); @@ -196,12 +200,15 @@ export class Checkbox extends LitElement { } protected override update(changed: PropertyValues) { - if (changed.has('checked') || changed.has('disabled') || - changed.has('indeterminate')) { + if ( + changed.has('checked') || + changed.has('disabled') || + changed.has('indeterminate') + ) { this.prevChecked = changed.get('checked') ?? this.checked; this.prevDisabled = changed.get('disabled') ?? this.disabled; this.prevIndeterminate = - changed.get('indeterminate') ?? this.indeterminate; + changed.get('indeterminate') ?? this.indeterminate; } const shouldAddFormValue = this.checked && !this.indeterminate; @@ -235,7 +242,8 @@ export class Checkbox extends LitElement { // form.reportValidity() to work in Chrome. return html`
- + @change=${this.handleChange} />
@@ -284,7 +291,10 @@ export class Checkbox extends LitElement { } this.internals.setValidity( - input.validity, input.validationMessage, this.getInput()); + input.validity, + input.validationMessage, + this.getInput(), + ); } private getInput() { diff --git a/checkbox/internal/checkbox_test.ts b/checkbox/internal/checkbox_test.ts index ebc8bc40b..9106f4421 100644 --- a/checkbox/internal/checkbox_test.ts +++ b/checkbox/internal/checkbox_test.ts @@ -23,7 +23,8 @@ describe('checkbox', () => { const env = new Environment(); async function setupTest( - template = html``) { + template = html``, + ) { const element = env.render(template).querySelector('md-test-checkbox'); if (!element) { throw new Error('Could not query rendered .'); @@ -84,84 +85,78 @@ describe('checkbox', () => { }); describe('checked', () => { - it('get/set updates the checked property on the native checkbox element', - async () => { - const {harness, input} = await setupTest(); - harness.element.checked = true; - await env.waitForStability(); - expect(input.checked).toEqual(true); - harness.element.checked = false; - await env.waitForStability(); - expect(input.checked).toEqual(false); - }); + it('get/set updates the checked property on the native checkbox element', async () => { + const {harness, input} = await setupTest(); + harness.element.checked = true; + await env.waitForStability(); + expect(input.checked).toEqual(true); + harness.element.checked = false; + await env.waitForStability(); + expect(input.checked).toEqual(false); + }); - it('get/set updates the checked property after user updates checked state', - async () => { - const {harness, input} = await setupTest(); + it('get/set updates the checked property after user updates checked state', async () => { + const {harness, input} = await setupTest(); - // Simulate user interaction setting checked to true. - await harness.clickWithMouse(); - await env.waitForStability(); - expect(input.checked).toEqual(true); - expect(harness.element.checked).toEqual(true); + // Simulate user interaction setting checked to true. + await harness.clickWithMouse(); + await env.waitForStability(); + expect(input.checked).toEqual(true); + expect(harness.element.checked).toEqual(true); - // Set custom element checked to false. - harness.element.checked = false; - await env.waitForStability(); - expect(input.checked).toEqual(false); - expect(harness.element.checked).toEqual(false); + // Set custom element checked to false. + harness.element.checked = false; + await env.waitForStability(); + expect(input.checked).toEqual(false); + expect(harness.element.checked).toEqual(false); - // Set custom element checked to true. - harness.element.checked = true; - await env.waitForStability(); - expect(input.checked).toEqual(true); - expect(harness.element.checked).toEqual(true); - }); + // Set custom element checked to true. + harness.element.checked = true; + await env.waitForStability(); + expect(input.checked).toEqual(true); + expect(harness.element.checked).toEqual(true); + }); }); describe('indeterminate', () => { - it('get/set updates the indeterminate property on the native checkbox element', - async () => { - const {harness, input} = await setupTest(); - harness.element.indeterminate = true; - await env.waitForStability(); + it('get/set updates the indeterminate property on the native checkbox element', async () => { + const {harness, input} = await setupTest(); + harness.element.indeterminate = true; + await env.waitForStability(); - expect(input.indeterminate).toEqual(true); - expect(input.getAttribute('aria-checked')).toEqual('mixed'); + expect(input.indeterminate).toEqual(true); + expect(input.getAttribute('aria-checked')).toEqual('mixed'); - harness.element.indeterminate = false; - await env.waitForStability(); + harness.element.indeterminate = false; + await env.waitForStability(); - expect(input.indeterminate).toEqual(false); - expect(input.getAttribute('aria-checked')).not.toEqual('mixed'); - }); + expect(input.indeterminate).toEqual(false); + expect(input.getAttribute('aria-checked')).not.toEqual('mixed'); + }); }); describe('disabled', () => { - it('get/set updates the disabled property on the native checkbox element', - async () => { - const {harness, input} = await setupTest(); - harness.element.disabled = true; - await env.waitForStability(); + it('get/set updates the disabled property on the native checkbox element', async () => { + const {harness, input} = await setupTest(); + harness.element.disabled = true; + await env.waitForStability(); - expect(input.disabled).toEqual(true); - harness.element.disabled = false; - await env.waitForStability(); - expect(input.disabled).toEqual(false); - }); + expect(input.disabled).toEqual(true); + harness.element.disabled = false; + await env.waitForStability(); + expect(input.disabled).toEqual(false); + }); }); describe('form submission', () => { async function setupFormTest(propsInit: Partial = {}) { - return await setupTest(html` -
- -
`); + return await setupTest(html`
+ +
`); } it('does not submit if not checked', async () => { @@ -171,8 +166,11 @@ describe('checkbox', () => { }); it('does not submit if disabled', async () => { - const {harness} = - await setupFormTest({name: 'foo', checked: true, disabled: true}); + const {harness} = await setupFormTest({ + name: 'foo', + checked: true, + disabled: true, + }); const formData = await harness.submitForm(); expect(formData.get('foo')).toBeNull(); }); @@ -185,8 +183,11 @@ describe('checkbox', () => { }); it('submits under correct conditions', async () => { - const {harness} = - await setupFormTest({name: 'foo', checked: true, value: 'bar'}); + const {harness} = await setupFormTest({ + name: 'foo', + checked: true, + value: 'bar', + }); const formData = await harness.submitForm(); expect(formData.get('foo')).toEqual('bar'); }); @@ -195,17 +196,21 @@ describe('checkbox', () => { describe('label activation', () => { async function setupLabelTest() { const test = await setupTest(html` - - `); - const label = (test.harness.element.getRootNode() as HTMLElement) - .querySelector('label')!; + + `); + const label = ( + test.harness.element.getRootNode() as HTMLElement + ).querySelector('label')!; return {...test, label}; } it('toggles when label is clicked', async () => { - const {harness: {element}, label} = await setupLabelTest(); + const { + harness: {element}, + label, + } = await setupLabelTest(); label.click(); await env.waitForStability(); expect(element.checked).toBeTrue(); @@ -221,8 +226,8 @@ describe('checkbox', () => { harness.element.required = true; expect(harness.element.validity.valueMissing) - .withContext('checkbox.validity.valueMissing') - .toBeTrue(); + .withContext('checkbox.validity.valueMissing') + .toBeTrue(); }); it('should not set valueMissing when required and checked', async () => { @@ -231,8 +236,8 @@ describe('checkbox', () => { harness.element.checked = true; expect(harness.element.validity.valueMissing) - .withContext('checkbox.validity.valueMissing') - .toBeFalse(); + .withContext('checkbox.validity.valueMissing') + .toBeFalse(); }); it('should set valueMissing when required and indeterminate', async () => { @@ -241,8 +246,8 @@ describe('checkbox', () => { harness.element.indeterminate = true; expect(harness.element.validity.valueMissing) - .withContext('checkbox.validity.valueMissing') - .toBeTrue(); + .withContext('checkbox.validity.valueMissing') + .toBeTrue(); }); }); }); diff --git a/chips/demo/demo.ts b/chips/demo/demo.ts index 7aee954e6..9053f62f3 100644 --- a/chips/demo/demo.ts +++ b/chips/demo/demo.ts @@ -4,21 +4,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob, textInput} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Chips', [ - new Knob('label', {defaultValue: '', ui: textInput()}), - new Knob('elevated', {defaultValue: false, ui: boolInput()}), - new Knob('disabled', {defaultValue: false, ui: boolInput()}), - new Knob('scrolling', {defaultValue: false, ui: boolInput()}), - ]); +const collection = new MaterialCollection>( + 'Chips', + [ + new Knob('label', {defaultValue: '', ui: textInput()}), + new Knob('elevated', {defaultValue: false, ui: boolInput()}), + new Knob('disabled', {defaultValue: false, ui: boolInput()}), + new Knob('scrolling', {defaultValue: false, ui: boolInput()}), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/chips/demo/stories.ts b/chips/demo/stories.ts index d5e522110..98e0a68fb 100644 --- a/chips/demo/stories.ts +++ b/chips/demo/stories.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/icon/icon.js'; -import '@material/web/chips/chip-set.js'; import '@material/web/chips/assist-chip.js'; +import '@material/web/chips/chip-set.js'; import '@material/web/chips/filter-chip.js'; import '@material/web/chips/input-chip.js'; import '@material/web/chips/suggestion-chip.js'; +import '@material/web/icon/icon.js'; import {MaterialStoryInit} from './material-collection.js'; import {css, html, svg} from 'lit'; @@ -53,13 +53,11 @@ const assist: MaterialStoryInit = { + ?elevated=${elevated}> + ?elevated=${elevated}> local_laundry_service = { ?elevated=${elevated} href="https://google.com" target="_blank" - >${GOOGLE_LOGO} + >${GOOGLE_LOGO} + ?elevated=${elevated}> `; - } + }, }; const filters: MaterialStoryInit = { @@ -85,36 +83,31 @@ const filters: MaterialStoryInit = { render({label, elevated, disabled, scrolling}) { const classes = {'scrolling': scrolling}; return html` - + + ?elevated=${elevated}> + ?elevated=${elevated}> local_laundry_service + removable> + removable> `; - } + }, }; const inputs: MaterialStoryInit = { @@ -126,39 +119,37 @@ const inputs: MaterialStoryInit = { + ?disabled=${disabled}> + ?disabled=${disabled}> local_laundry_service - + avatar> + ${GOOGLE_LOGO} + >${GOOGLE_LOGO} + remove-only> + always-focusable> `; - } + }, }; const suggestions: MaterialStoryInit = { @@ -171,13 +162,11 @@ const suggestions: MaterialStoryInit = { + ?elevated=${elevated}> + ?elevated=${elevated}> local_laundry_service = { ?elevated=${elevated} href="https://google.com" target="_blank" - >${GOOGLE_LOGO} + >${GOOGLE_LOGO} + ?elevated=${elevated}> `; - } + }, }; /** Chips stories. */ diff --git a/chips/filter-chip.ts b/chips/filter-chip.ts index 600d1aaff..01820897d 100644 --- a/chips/filter-chip.ts +++ b/chips/filter-chip.ts @@ -28,6 +28,10 @@ declare global { @customElement('md-filter-chip') export class MdFilterChip extends FilterChip { static override styles = [ - sharedStyles, elevatedStyles, trailingIconStyles, selectableStyles, styles + sharedStyles, + elevatedStyles, + trailingIconStyles, + selectableStyles, + styles, ]; } diff --git a/chips/harness.ts b/chips/harness.ts index 4e028e96f..484afbbc6 100644 --- a/chips/harness.ts +++ b/chips/harness.ts @@ -12,16 +12,18 @@ import {Chip} from './internal/chip.js'; * Test harness for chips. */ export class ChipHarness extends Harness { - action: 'primary'|'trailing' = 'primary'; + action: 'primary' | 'trailing' = 'primary'; protected override async getInteractiveElement() { await this.element.updateComplete; const {primaryId} = this.element as unknown as {primaryId: string}; - const primaryAction = primaryId && - this.element.renderRoot.querySelector(`#${primaryId}`); + const primaryAction = + primaryId && + this.element.renderRoot.querySelector(`#${primaryId}`); // Retrieve MultiActionChip's trailingAction - const {trailingAction} = - this.element as {trailingAction?: HTMLElement | null}; + const {trailingAction} = this.element as { + trailingAction?: HTMLElement | null; + }; // Default to trailing action if there isn't a primary action and the user // didn't explicitly set `harness.action = 'trailing'` (remove-only input @@ -29,7 +31,8 @@ export class ChipHarness extends Harness { if (this.action === 'trailing' || !primaryAction) { if (!trailingAction) { throw new Error( - '`ChipHarness.action` is "trailing", but the chip does not have a trailing action.'); + '`ChipHarness.action` is "trailing", but the chip does not have a trailing action.', + ); } return trailingAction; @@ -37,7 +40,8 @@ export class ChipHarness extends Harness { if (!primaryAction) { throw new Error( - '`ChipHarness.action` is "primary", but the chip does not have a primary action.'); + '`ChipHarness.action` is "primary", but the chip does not have a primary action.', + ); } return primaryAction; diff --git a/chips/input-chip.ts b/chips/input-chip.ts index 3311f4cd7..32e826e21 100644 --- a/chips/input-chip.ts +++ b/chips/input-chip.ts @@ -26,6 +26,10 @@ declare global { */ @customElement('md-input-chip') export class MdInputChip extends InputChip { - static override styles = - [sharedStyles, trailingIconStyles, selectableStyles, styles]; + static override styles = [ + sharedStyles, + trailingIconStyles, + selectableStyles, + styles, + ]; } diff --git a/chips/internal/assist-chip.ts b/chips/internal/assist-chip.ts index 3892ebabb..d342ddb2a 100644 --- a/chips/internal/assist-chip.ts +++ b/chips/internal/assist-chip.ts @@ -19,7 +19,7 @@ import {Chip} from './chip.js'; export class AssistChip extends Chip { @property({type: Boolean}) elevated = false; @property() href = ''; - @property() target: '_blank'|'_parent'|'_self'|'_top'|'' = ''; + @property() target: '_blank' | '_parent' | '_self' | '_top' | '' = ''; protected get primaryId() { return this.href ? 'link' : 'button'; @@ -44,22 +44,26 @@ export class AssistChip extends Chip { const {ariaLabel} = this as ARIAMixinStrict; if (this.href) { return html` - ${content} + >${content} `; } return html` - + >${content} `; } diff --git a/chips/internal/assist-chip_test.ts b/chips/internal/assist-chip_test.ts index d91226a55..fbf15218f 100644 --- a/chips/internal/assist-chip_test.ts +++ b/chips/internal/assist-chip_test.ts @@ -31,8 +31,8 @@ describe('Assist chip', () => { await chip.updateComplete; expect(chip.renderRoot.querySelector('a')) - .withContext('should have a rendered link') - .toBeTruthy(); + .withContext('should have a rendered link') + .toBeTruthy(); }); it('should not allow link chips to be disabled', async () => { @@ -42,8 +42,8 @@ describe('Assist chip', () => { await chip.updateComplete; expect(chip.renderRoot.querySelector('.disabled,:disabled')) - .withContext('should not have any disabled styling or behavior') - .toBeNull(); + .withContext('should not have any disabled styling or behavior') + .toBeNull(); }); }); }); diff --git a/chips/internal/chip-set.ts b/chips/internal/chip-set.ts index 5b33e3268..c7ac6eeb8 100644 --- a/chips/internal/chip-set.ts +++ b/chips/internal/chip-set.ts @@ -7,7 +7,10 @@ import {html, isServer, LitElement} from 'lit'; import {queryAssignedElements} from 'lit/decorators.js'; -import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js'; +import { + polyfillElementInternalsAria, + setupHostAria, +} from '../../internal/aria/aria.js'; import {Chip} from './chip.js'; @@ -21,12 +24,16 @@ export class ChipSet extends LitElement { get chips() { return this.childElements.filter( - (child): child is Chip => child instanceof Chip); + (child): child is Chip => child instanceof Chip, + ); } @queryAssignedElements() private readonly childElements!: HTMLElement[]; private readonly internals = polyfillElementInternalsAria( - this, (this as HTMLElement /* needed for closure */).attachInternals()); + this, + // Cast needed for closure + (this as HTMLElement).attachInternals(), + ); constructor() { super(); @@ -71,7 +78,7 @@ export class ChipSet extends LitElement { // Check if moving forwards or backwards const isRtl = getComputedStyle(this).direction === 'rtl'; const forwards = isRtl ? isLeft : isRight; - const focusedChip = chips.find(chip => chip.matches(':focus-within')); + const focusedChip = chips.find((chip) => chip.matches(':focus-within')); if (!focusedChip) { // If there is not already a chip focused, select the first or last chip // based on the direction we're traveling. @@ -120,7 +127,7 @@ export class ChipSet extends LitElement { // The chip that should be focusable is either the chip that currently has // focus or the first chip that can be focused. const {chips} = this; - let chipToFocus: Chip|undefined; + let chipToFocus: Chip | undefined; for (const chip of chips) { const isChipFocusable = chip.alwaysFocusable || !chip.disabled; const chipIsFocused = chip.matches(':focus-within'); @@ -147,5 +154,5 @@ export class ChipSet extends LitElement { } interface MaybeMultiActionChip extends Chip { - focus(options?: FocusOptions&{trailing?: boolean}): void; + focus(options?: FocusOptions & {trailing?: boolean}): void; } diff --git a/chips/internal/chip-set_test.ts b/chips/internal/chip-set_test.ts index 244c05105..69379512a 100644 --- a/chips/internal/chip-set_test.ts +++ b/chips/internal/chip-set_test.ts @@ -19,14 +19,11 @@ import {ChipSet} from './chip-set.js'; import {InputChip} from './input-chip.js'; @customElement('test-chip-set') -class TestChipSet extends ChipSet { -} +class TestChipSet extends ChipSet {} @customElement('test-chip-set-assist-chip') -class TestAssistChip extends AssistChip { -} +class TestAssistChip extends AssistChip {} @customElement('test-chip-set-input-chip') -class TestInputChip extends InputChip { -} +class TestInputChip extends InputChip {} describe('Chip set', () => { const env = new Environment(); @@ -61,42 +58,51 @@ describe('Chip set', () => { describe('navigation', () => { it('should add tabindex="-1" to all chips except the first', async () => { - const chipSet = await setupTest( - [new TestAssistChip(), new TestAssistChip(), new TestAssistChip()]); + const chipSet = await setupTest([ + new TestAssistChip(), + new TestAssistChip(), + new TestAssistChip(), + ]); expect(chipSet.chips[0].getAttribute('tabindex')) - .withContext('first tabindex') - .toBe('0'); + .withContext('first tabindex') + .toBe('0'); expect(chipSet.chips[1].getAttribute('tabindex')) - .withContext('second tabindex') - .toBe('-1'); + .withContext('second tabindex') + .toBe('-1'); expect(chipSet.chips[2].getAttribute('tabindex')) - .withContext('third tabindex') - .toBe('-1'); + .withContext('third tabindex') + .toBe('-1'); }); - async function testNavigation({chipSet, ltrKey, rtlKey, current, next}: { - chipSet: ChipSet, - ltrKey: string, - rtlKey: string, - current: Chip|null, - next: Chip, + async function testNavigation({ + chipSet, + ltrKey, + rtlKey, + current, + next, + }: { + chipSet: ChipSet; + ltrKey: string; + rtlKey: string; + current: Chip | null; + next: Chip; }) { const harness = current ? new ChipHarness(current) : new Harness(chipSet); // Don't use harness focusing since we need to test real focus states current?.focus(); await harness.keypress(ltrKey); expect(next.matches(':focus-within')) - .withContext(`next chip is focused in LTR after ${ltrKey}`) - .toBeTrue(); + .withContext(`next chip is focused in LTR after ${ltrKey}`) + .toBeTrue(); next.blur(); chipSet.style.direction = 'rtl'; current?.focus(); await harness.keypress(rtlKey); expect(next.matches(':focus-within')) - .withContext(`next chip is focused in RTL after ${rtlKey}`) - .toBeTrue(); + .withContext(`next chip is focused in RTL after ${rtlKey}`) + .toBeTrue(); } it('should navigate forward on horizontal arrow keys', async () => { @@ -109,7 +115,7 @@ describe('Chip set', () => { ltrKey: 'ArrowRight', rtlKey: 'ArrowLeft', current: first, - next: second + next: second, }); }); @@ -123,7 +129,7 @@ describe('Chip set', () => { ltrKey: 'ArrowLeft', rtlKey: 'ArrowRight', current: second, - next: first + next: first, }); }); @@ -137,7 +143,7 @@ describe('Chip set', () => { ltrKey: 'Home', rtlKey: 'Home', current: second, - next: first + next: first, }); }); @@ -151,39 +157,37 @@ describe('Chip set', () => { ltrKey: 'End', rtlKey: 'End', current: second, - next: third + next: third, }); }); - it('should navigate to first chip on forward when none focused', - async () => { - const first = new TestAssistChip(); - const second = new TestAssistChip(); - const third = new TestAssistChip(); - const chipSet = await setupTest([first, second, third]); - await testNavigation({ - chipSet, - ltrKey: 'ArrowRight', - rtlKey: 'ArrowLeft', - current: null, - next: first - }); - }); + it('should navigate to first chip on forward when none focused', async () => { + const first = new TestAssistChip(); + const second = new TestAssistChip(); + const third = new TestAssistChip(); + const chipSet = await setupTest([first, second, third]); + await testNavigation({ + chipSet, + ltrKey: 'ArrowRight', + rtlKey: 'ArrowLeft', + current: null, + next: first, + }); + }); - it('should navigate to last chip on backward when none focused', - async () => { - const first = new TestAssistChip(); - const second = new TestAssistChip(); - const third = new TestAssistChip(); - const chipSet = await setupTest([first, second, third]); - await testNavigation({ - chipSet, - ltrKey: 'ArrowLeft', - rtlKey: 'ArrowRight', - current: null, - next: third - }); - }); + it('should navigate to last chip on backward when none focused', async () => { + const first = new TestAssistChip(); + const second = new TestAssistChip(); + const third = new TestAssistChip(); + const chipSet = await setupTest([first, second, third]); + await testNavigation({ + chipSet, + ltrKey: 'ArrowLeft', + rtlKey: 'ArrowRight', + current: null, + next: third, + }); + }); it('should skip over disabled chips', async () => { const first = new TestAssistChip(); @@ -196,7 +200,7 @@ describe('Chip set', () => { ltrKey: 'ArrowRight', rtlKey: 'ArrowLeft', current: first, - next: third + next: third, }); }); @@ -212,7 +216,7 @@ describe('Chip set', () => { ltrKey: 'ArrowRight', rtlKey: 'ArrowLeft', current: first, - next: second + next: second, }); }); @@ -226,11 +230,12 @@ describe('Chip set', () => { // Don't use harness focusing since we need to test real focus states second.focus(); await harness.keypress('ArrowLeft'); - const {trailingAction} = - first as unknown as {trailingAction: HTMLElement}; + const {trailingAction} = first as unknown as { + trailingAction: HTMLElement; + }; expect(trailingAction.matches(':focus-within')) - .withContext('trailing action of first chip is focused') - .toBeTrue(); + .withContext('trailing action of first chip is focused') + .toBeTrue(); }); it('should ignore other keyboard events', async () => { @@ -244,8 +249,8 @@ describe('Chip set', () => { first.focus(); await harness.keypress('Enter'); expect(first.matches(':focus-within')) - .withContext('first chip is still focused') - .toBeTrue(); + .withContext('first chip is still focused') + .toBeTrue(); }); it('should do nothing if there are not at least two chips', async () => { @@ -257,8 +262,8 @@ describe('Chip set', () => { single.focus(); await harness.keypress('ArrowRight'); expect(single.matches(':focus-within')) - .withContext('single chip is still focused') - .toBeTrue(); + .withContext('single chip is still focused') + .toBeTrue(); }); }); }); diff --git a/chips/internal/chip.ts b/chips/internal/chip.ts index 0d590cbdd..5262d6489 100644 --- a/chips/internal/chip.ts +++ b/chips/internal/chip.ts @@ -24,7 +24,7 @@ export abstract class Chip extends LitElement { /** @nocollapse */ static override shadowRootOptions = { ...LitElement.shadowRootOptions, - delegatesFocus: true + delegatesFocus: true, }; /** @@ -93,10 +93,10 @@ export abstract class Chip extends LitElement { protected renderContainerContent() { return html` ${this.renderOutline()} - - + + ${this.renderPrimaryAction(this.renderPrimaryContent())} `; } diff --git a/chips/internal/chip_test.ts b/chips/internal/chip_test.ts index a61714f38..d5da8ca4d 100644 --- a/chips/internal/chip_test.ts +++ b/chips/internal/chip_test.ts @@ -33,14 +33,13 @@ describe('Chip', () => { return {chip, harness: new ChipHarness(chip)}; } - it('should dispatch `update-focus` for chip set when disabled changes', - async () => { - const {chip} = await setupTest(); - const updateFocusListener = jasmine.createSpy('updateFocusListener'); - chip.addEventListener('update-focus', updateFocusListener); + it('should dispatch `update-focus` for chip set when disabled changes', async () => { + const {chip} = await setupTest(); + const updateFocusListener = jasmine.createSpy('updateFocusListener'); + chip.addEventListener('update-focus', updateFocusListener); - chip.disabled = true; - await env.waitForStability(); - expect(updateFocusListener).toHaveBeenCalled(); - }); + chip.disabled = true; + await env.waitForStability(); + expect(updateFocusListener).toHaveBeenCalled(); + }); }); diff --git a/chips/internal/filter-chip.ts b/chips/internal/filter-chip.ts index 027ef9dea..8014faaac 100644 --- a/chips/internal/filter-chip.ts +++ b/chips/internal/filter-chip.ts @@ -27,9 +27,10 @@ export class FilterChip extends MultiActionChip { return 'button'; } - @query('.primary.action') protected readonly primaryAction!: HTMLElement|null; + @query('.primary.action') + protected readonly primaryAction!: HTMLElement | null; @query('.trailing.action') - protected readonly trailingAction!: HTMLElement|null; + protected readonly trailingAction!: HTMLElement | null; protected override getContainerClasses() { return { @@ -43,13 +44,15 @@ export class FilterChip extends MultiActionChip { protected override renderPrimaryAction(content: unknown) { const {ariaLabel} = this as ARIAMixinStrict; return html` - + >${content} `; } @@ -60,7 +63,8 @@ export class FilterChip extends MultiActionChip { return html` `; } @@ -70,7 +74,7 @@ export class FilterChip extends MultiActionChip { return renderRemoveButton({ focusListener, ariaLabel: this.ariaLabelRemove, - disabled: this.disabled + disabled: this.disabled, }); } diff --git a/chips/internal/input-chip.ts b/chips/internal/input-chip.ts index d19215b11..c2b01fff9 100644 --- a/chips/internal/input-chip.ts +++ b/chips/internal/input-chip.ts @@ -18,7 +18,7 @@ import {renderRemoveButton} from './trailing-icons.js'; export class InputChip extends MultiActionChip { @property({type: Boolean}) avatar = false; @property() href = ''; - @property() target: '_blank'|'_parent'|'_self'|'_top'|'' = ''; + @property() target: '_blank' | '_parent' | '_self' | '_top' | '' = ''; @property({type: Boolean, attribute: 'remove-only'}) removeOnly = false; @property({type: Boolean, reflect: true}) selected = false; @@ -50,7 +50,7 @@ export class InputChip extends MultiActionChip { } @query('.trailing.action') - protected readonly trailingAction!: HTMLElement|null; + protected readonly trailingAction!: HTMLElement | null; protected override getContainerClasses() { return { @@ -68,12 +68,14 @@ export class InputChip extends MultiActionChip { const {ariaLabel} = this as ARIAMixinStrict; if (this.href) { return html` - ${content} + >${content} `; } @@ -86,12 +88,14 @@ export class InputChip extends MultiActionChip { } return html` - + >${content} `; } diff --git a/chips/internal/input-chip_test.ts b/chips/internal/input-chip_test.ts index b5df03ef6..e295c071a 100644 --- a/chips/internal/input-chip_test.ts +++ b/chips/internal/input-chip_test.ts @@ -31,8 +31,8 @@ describe('Input chip', () => { await chip.updateComplete; expect(chip.renderRoot.querySelector('a')) - .withContext('should have a rendered link') - .toBeTruthy(); + .withContext('should have a rendered link') + .toBeTruthy(); }); it('should not allow link chips to be disabled', async () => { @@ -42,8 +42,8 @@ describe('Input chip', () => { await chip.updateComplete; expect(chip.renderRoot.querySelector('.disabled,:disabled')) - .withContext('should not have any disabled styling or behavior') - .toBeNull(); + .withContext('should not have any disabled styling or behavior') + .toBeNull(); }); }); }); diff --git a/chips/internal/multi-action-chip.ts b/chips/internal/multi-action-chip.ts index af427b7fe..21310c82d 100644 --- a/chips/internal/multi-action-chip.ts +++ b/chips/internal/multi-action-chip.ts @@ -24,7 +24,7 @@ export abstract class MultiActionChip extends Chip { const {ariaLabel} = this as ARIAMixinStrict; return `Remove ${ariaLabel || this.label}`; } - set ariaLabelRemove(ariaLabel: string|null) { + set ariaLabelRemove(ariaLabel: string | null) { const prev = this.ariaLabelRemove; if (ariaLabel === prev) { return; @@ -39,8 +39,8 @@ export abstract class MultiActionChip extends Chip { this.requestUpdate(); } - protected abstract readonly primaryAction: HTMLElement|null; - protected abstract readonly trailingAction: HTMLElement|null; + protected abstract readonly primaryAction: HTMLElement | null; + protected abstract readonly trailingAction: HTMLElement | null; constructor() { super(); @@ -50,7 +50,7 @@ export abstract class MultiActionChip extends Chip { } } - override focus(options?: FocusOptions&{trailing?: boolean}) { + override focus(options?: FocusOptions & {trailing?: boolean}) { const isFocusable = this.alwaysFocusable || !this.disabled; if (isFocusable && options?.trailing && this.trailingAction) { this.trailingAction.focus(options); @@ -67,8 +67,9 @@ export abstract class MultiActionChip extends Chip { `; } - protected abstract renderTrailingAction(focusListener: EventListener): - unknown; + protected abstract renderTrailingAction( + focusListener: EventListener, + ): unknown; private handleKeyDown(event: KeyboardEvent) { const isLeft = event.key === 'ArrowLeft'; @@ -112,8 +113,12 @@ export abstract class MultiActionChip extends Chip { // shift+tab from the trailing action to move to the previous chip rather // than the primary action in the same chip. primaryAction.tabIndex = -1; - trailingAction.addEventListener('focusout', () => { - primaryAction.tabIndex = 0; - }, {once: true}); + trailingAction.addEventListener( + 'focusout', + () => { + primaryAction.tabIndex = 0; + }, + {once: true}, + ); } } diff --git a/chips/internal/multi-action-chip_test.ts b/chips/internal/multi-action-chip_test.ts index 04a41674b..af8413381 100644 --- a/chips/internal/multi-action-chip_test.ts +++ b/chips/internal/multi-action-chip_test.ts @@ -17,7 +17,11 @@ import {renderRemoveButton} from './trailing-icons.js'; @customElement('test-multi-action-chip') class TestMultiActionChip extends MultiActionChip { - static override styles = css`:host { position: relative; }`; + static override styles = css` + :host { + position: relative; + } + `; @query('#primary') primaryAction!: HTMLElement; @query('.trailing.action') trailingAction!: HTMLElement; @@ -37,7 +41,7 @@ class TestMultiActionChip extends MultiActionChip { return renderRemoveButton({ focusListener, ariaLabel: this.ariaLabelRemove, - disabled: this.disabled + disabled: this.disabled, }); } } @@ -59,13 +63,13 @@ describe('Multi-action chips', () => { await primaryHarness.focusWithKeyboard(); expect(chip.primaryAction.matches(':focus-within')) - .withContext('primary action is focused') - .toBeTrue(); + .withContext('primary action is focused') + .toBeTrue(); await primaryHarness.keypress('ArrowRight'); expect(chip.trailingAction.matches(':focus-within')) - .withContext('trailing action is focused') - .toBeTrue(); + .withContext('trailing action is focused') + .toBeTrue(); }); it('should move internal focus forwards in rtl', async () => { @@ -75,13 +79,13 @@ describe('Multi-action chips', () => { await primaryHarness.focusWithKeyboard(); expect(chip.primaryAction.matches(':focus-within')) - .withContext('primary action is focused') - .toBeTrue(); + .withContext('primary action is focused') + .toBeTrue(); await primaryHarness.keypress('ArrowLeft'); expect(chip.trailingAction.matches(':focus-within')) - .withContext('trailing action is focused') - .toBeTrue(); + .withContext('trailing action is focused') + .toBeTrue(); }); it('should move internal focus backwards', async () => { @@ -91,13 +95,13 @@ describe('Multi-action chips', () => { await trailingHarness.focusWithKeyboard(); expect(chip.trailingAction.matches(':focus-within')) - .withContext('trailing action is focused') - .toBeTrue(); + .withContext('trailing action is focused') + .toBeTrue(); await trailingHarness.keypress('ArrowLeft'); expect(chip.primaryAction.matches(':focus-within')) - .withContext('primary action is focused') - .toBeTrue(); + .withContext('primary action is focused') + .toBeTrue(); }); it('should move internal focus backwards in rtl', async () => { @@ -108,13 +112,13 @@ describe('Multi-action chips', () => { await trailingHarness.focusWithKeyboard(); expect(chip.trailingAction.matches(':focus-within')) - .withContext('trailing action is focused') - .toBeTrue(); + .withContext('trailing action is focused') + .toBeTrue(); await trailingHarness.keypress('ArrowRight'); expect(chip.primaryAction.matches(':focus-within')) - .withContext('primary action is focused') - .toBeTrue(); + .withContext('primary action is focused') + .toBeTrue(); }); it('should not bubble when navigating internally', async () => { @@ -132,38 +136,36 @@ describe('Multi-action chips', () => { expect(keydownHandler).not.toHaveBeenCalled(); }); - it('should bubble event when navigating forward past trailing action', - async () => { - const chip = await setupTest(); - const trailingHarness = new ChipHarness(chip); - trailingHarness.action = 'trailing'; - const keydownHandler = jasmine.createSpy(); - if (!chip.parentElement) { - throw new Error('Expected chip to have a parentElement for test.'); - } + it('should bubble event when navigating forward past trailing action', async () => { + const chip = await setupTest(); + const trailingHarness = new ChipHarness(chip); + trailingHarness.action = 'trailing'; + const keydownHandler = jasmine.createSpy(); + if (!chip.parentElement) { + throw new Error('Expected chip to have a parentElement for test.'); + } - chip.parentElement.addEventListener('keydown', keydownHandler); + chip.parentElement.addEventListener('keydown', keydownHandler); - await trailingHarness.focusWithKeyboard(); - await trailingHarness.keypress('ArrowRight'); - expect(keydownHandler).toHaveBeenCalledTimes(1); - }); + await trailingHarness.focusWithKeyboard(); + await trailingHarness.keypress('ArrowRight'); + expect(keydownHandler).toHaveBeenCalledTimes(1); + }); - it('should bubble event when navigating backward before primary action', - async () => { - const chip = await setupTest(); - const primaryHarness = new ChipHarness(chip); - const keydownHandler = jasmine.createSpy(); - if (!chip.parentElement) { - throw new Error('Expected chip to have a parentElement for test.'); - } + it('should bubble event when navigating backward before primary action', async () => { + const chip = await setupTest(); + const primaryHarness = new ChipHarness(chip); + const keydownHandler = jasmine.createSpy(); + if (!chip.parentElement) { + throw new Error('Expected chip to have a parentElement for test.'); + } - chip.parentElement.addEventListener('keydown', keydownHandler); + chip.parentElement.addEventListener('keydown', keydownHandler); - await primaryHarness.focusWithKeyboard(); - await primaryHarness.keypress('ArrowLeft'); - expect(keydownHandler).toHaveBeenCalledTimes(1); - }); + await primaryHarness.focusWithKeyboard(); + await primaryHarness.keypress('ArrowLeft'); + expect(keydownHandler).toHaveBeenCalledTimes(1); + }); it('should do nothing if it does not have multiple actions', async () => { const chip = await setupTest(); @@ -174,8 +176,8 @@ describe('Multi-action chips', () => { await primaryHarness.focusWithKeyboard(); await primaryHarness.keypress('ArrowLeft'); expect(chip.primaryAction.matches(':focus-within')) - .withContext('primary action is still focused') - .toBeTrue(); + .withContext('primary action is still focused') + .toBeTrue(); }); }); @@ -186,12 +188,12 @@ describe('Multi-action chips', () => { harness.action = 'trailing'; expect(chip.parentElement) - .withContext('chip should be attached before removing') - .not.toBeNull(); + .withContext('chip should be attached before removing') + .not.toBeNull(); await harness.clickWithMouse(); expect(chip.parentElement) - .withContext('chip should be detached after removing') - .toBeNull(); + .withContext('chip should be detached after removing') + .toBeNull(); }); it('should dispatch a "remove" event when removed', async () => { @@ -205,20 +207,19 @@ describe('Multi-action chips', () => { expect(handler).toHaveBeenCalledTimes(1); }); - it('should not remove chip if "remove" event is default prevented', - async () => { - const chip = await setupTest(); - const harness = new ChipHarness(chip); - harness.action = 'trailing'; - chip.addEventListener('remove', event => { - event.preventDefault(); - }); + it('should not remove chip if "remove" event is default prevented', async () => { + const chip = await setupTest(); + const harness = new ChipHarness(chip); + harness.action = 'trailing'; + chip.addEventListener('remove', (event) => { + event.preventDefault(); + }); - await harness.clickWithMouse(); - expect(chip.parentElement) - .withContext('chip should still be attached') - .not.toBeNull(); - }); + await harness.clickWithMouse(); + expect(chip.parentElement) + .withContext('chip should still be attached') + .not.toBeNull(); + }); it('should provide a default "ariaLabelRemove" value', async () => { const chip = await setupTest(); @@ -227,14 +228,13 @@ describe('Multi-action chips', () => { expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.label}`); }); - it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', - async () => { - const chip = await setupTest(); - chip.label = 'Label'; - chip.ariaLabel = 'Descriptive label'; + it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', async () => { + const chip = await setupTest(); + chip.label = 'Label'; + chip.ariaLabel = 'Descriptive label'; - expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.ariaLabel}`); - }); + expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.ariaLabel}`); + }); it('should allow setting a custom "ariaLabelRemove"', async () => { const chip = await setupTest(); diff --git a/chips/internal/trailing-icons.ts b/chips/internal/trailing-icons.ts index f3fd6ba89..234afe851 100644 --- a/chips/internal/trailing-icons.ts +++ b/chips/internal/trailing-icons.ts @@ -19,20 +19,24 @@ interface RemoveButtonProperties { } /** @protected */ -export function renderRemoveButton( - {ariaLabel, disabled, focusListener, tabbable = false}: - RemoveButtonProperties) { +export function renderRemoveButton({ + ariaLabel, + disabled, + focusListener, + tabbable = false, +}: RemoveButtonProperties) { return html` - @@ -45,8 +49,9 @@ function handleRemoveClick(this: Chip, event: Event) { } event.stopPropagation(); - const preventDefault = - !this.dispatchEvent(new Event('remove', {cancelable: true})); + const preventDefault = !this.dispatchEvent( + new Event('remove', {cancelable: true}), + ); if (preventDefault) { return; } diff --git a/dialog/demo/demo.ts b/dialog/demo/demo.ts index 355168131..4b82893e5 100644 --- a/dialog/demo/demo.ts +++ b/dialog/demo/demo.ts @@ -4,22 +4,30 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {Knob, textInput} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Dialog', [ - new Knob('icon', {defaultValue: '', ui: textInput()}), - new Knob('headline', {defaultValue: 'Dialog', ui: textInput()}), - new Knob( - 'supportingText', - {defaultValue: 'Just a simple dialog.', ui: textInput()}), - ]); +const collection = new MaterialCollection>( + 'Dialog', + [ + new Knob('icon', {defaultValue: '', ui: textInput()}), + new Knob('headline', {defaultValue: 'Dialog', ui: textInput()}), + new Knob('supportingText', { + defaultValue: 'Just a simple dialog.', + ui: textInput(), + }), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/dialog/demo/stories.ts b/dialog/demo/stories.ts index 598947f2b..e63038f58 100644 --- a/dialog/demo/stories.ts +++ b/dialog/demo/stories.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/iconbutton/icon-button.js'; -import '@material/web/textfield/filled-text-field.js'; -import '@material/web/radio/radio.js'; -import '@material/web/icon/icon.js'; -import '@material/web/button/filled-tonal-button.js'; import '@material/web/button/filled-button.js'; +import '@material/web/button/filled-tonal-button.js'; import '@material/web/button/text-button.js'; import '@material/web/dialog/dialog.js'; +import '@material/web/icon/icon.js'; +import '@material/web/iconbutton/icon-button.js'; +import '@material/web/radio/radio.js'; +import '@material/web/textfield/filled-text-field.js'; import {MdDialog} from '@material/web/dialog/dialog.js'; import {MaterialStoryInit} from './material-collection.js'; @@ -32,8 +32,9 @@ const standard: MaterialStoryInit = { name: 'Dialog', render({icon, headline, supportingText}) { return html` - Open + Open ${icon ? html`${icon}` : nothing} @@ -47,15 +48,16 @@ const standard: MaterialStoryInit = {
`; - } + }, }; const alert: MaterialStoryInit = { name: 'Alert', render() { return html` - Alert + Alert
Alert dialog
@@ -68,15 +70,18 @@ const alert: MaterialStoryInit = {
`; - } + }, }; const confirm: MaterialStoryInit = { name: 'Confirm', render() { return html` - Confirm + Confirm
Permanently delete?
@@ -87,12 +92,13 @@ const confirm: MaterialStoryInit = {
Delete - Cancel + Cancel
`; - } + }, }; const choose: MaterialStoryInit = { @@ -105,22 +111,36 @@ const choose: MaterialStoryInit = { `, render() { return html` - Choice + Choice
Choose your favorite pet
@@ -130,7 +150,7 @@ const choose: MaterialStoryInit = {
`; - } + }, }; const contacts: MaterialStoryInit = { @@ -140,7 +160,7 @@ const contacts: MaterialStoryInit = { min-width: calc(100vw - 212px); } - .contacts [slot="header"] { + .contacts [slot='header'] { display: flex; flex-direction: row-reverse; align-items: center; @@ -150,7 +170,8 @@ const contacts: MaterialStoryInit = { flex: 1; } - .contact-content, .contact-row { + .contact-content, + .contact-row { display: flex; gap: 8px; } @@ -165,8 +186,9 @@ const contacts: MaterialStoryInit = { `, render() { return html` - Form + Form @@ -177,7 +199,9 @@ const contacts: MaterialStoryInit = {
- +
@@ -188,24 +212,23 @@ const contacts: MaterialStoryInit = {
- Reset + Reset
Cancel Save
`; - } + }, }; const floatingSheet: MaterialStoryInit = { name: 'Floating sheet', render() { return html` - + Floating sheet @@ -217,16 +240,21 @@ const floatingSheet: MaterialStoryInit = {
- This is a floating sheet with title. - Floating sheets offer no action buttons at the bottom, - but there's a close icon button at the top right. - They accept any HTML content. + This is a floating sheet with title. Floating sheets offer no action + buttons at the bottom, but there's a close icon button at the top + right. They accept any HTML content.
`; - } + }, }; /** Dialog stories. */ -export const stories = - [standard, alert, confirm, choose, contacts, floatingSheet]; +export const stories = [ + standard, + alert, + confirm, + choose, + contacts, + floatingSheet, +]; diff --git a/dialog/dialog_test.ts b/dialog/dialog_test.ts index 17702de4d..5dd98065b 100644 --- a/dialog/dialog_test.ts +++ b/dialog/dialog_test.ts @@ -20,7 +20,7 @@ describe('', () => {
Content - +
@@ -107,14 +107,19 @@ describe('', () => { it('closes when element with action is clicked', async () => { const {harness} = await setupTest(); await harness.element.show(); - const closedPromise = new Promise(resolve => { - harness.element.addEventListener('closed', () => { - resolve(); - }, {once: true}); + const closedPromise = new Promise((resolve) => { + harness.element.addEventListener( + 'closed', + () => { + resolve(); + }, + {once: true}, + ); }); - harness.element.querySelector( - '[value="button"]')!.click(); + harness.element + .querySelector('[value="button"]')! + .click(); await closedPromise; expect(harness.element.open).toBeFalse(); expect(harness.element.returnValue).toBe('button'); @@ -132,19 +137,18 @@ describe('', () => { expect(isClosing).toHaveBeenCalled(); }); - it('focuses element with autofocus when shown and previously focused element when closed', - async () => { - const {harness, focusElement} = await setupTest(); - const button = document.createElement('button'); - document.body.append(button); - button.focus(); - expect(document.activeElement).toBe(button); - await harness.element.show(); - expect(document.activeElement).toBe(focusElement); - await harness.element.close(); - expect(document.activeElement).toBe(button); - button.remove(); - }); + it('focuses element with autofocus when shown and previously focused element when closed', async () => { + const {harness, focusElement} = await setupTest(); + const button = document.createElement('button'); + document.body.append(button); + button.focus(); + expect(document.activeElement).toBe(button); + await harness.element.show(); + expect(document.activeElement).toBe(focusElement); + await harness.element.close(); + expect(document.activeElement).toBe(button); + button.remove(); + }); }); it('should set returnValue during the close event', async () => { @@ -159,14 +163,14 @@ describe('', () => { const returnValue = 'foo'; await harness.element.close(returnValue); expect(returnValueDuringClose) - .withContext('dialog.returnValue during close event') - .toBe(returnValue); + .withContext('dialog.returnValue during close event') + .toBe(returnValue); }); it('should not change returnValue if close event is canceled', async () => { const {harness} = await setupTest(); - harness.element.addEventListener('close', event => { + harness.element.addEventListener('close', (event) => { event.preventDefault(); }); @@ -174,8 +178,8 @@ describe('', () => { const prevReturnValue = harness.element.returnValue; await harness.element.close('new return value'); expect(harness.element.returnValue) - .withContext('dialog.returnValue after close event canceled') - .toBe(prevReturnValue); + .withContext('dialog.returnValue after close event canceled') + .toBe(prevReturnValue); }); it('should open on connected if opened before connected to DOM', async () => { @@ -185,50 +189,49 @@ describe('', () => { dialog.addEventListener('open', openListener); dialog.open = true; expect(openListener) - .withContext('should not trigger open before connected') - .not.toHaveBeenCalled(); + .withContext('should not trigger open before connected') + .not.toHaveBeenCalled(); const root = env.render(html``); root.appendChild(dialog); await env.waitForStability(); expect(openListener) - .withContext('opens after connecting') - .toHaveBeenCalled(); + .withContext('opens after connecting') + .toHaveBeenCalled(); }); - it('should not open on connected if opened, but closed before connected to DOM', - async () => { - const openListener = jasmine.createSpy('openListener'); - const dialog = document.createElement('md-dialog'); - disableDialogAnimations(dialog); - dialog.addEventListener('open', openListener); - dialog.open = true; - await env.waitForStability(); - dialog.open = false; - const root = env.render(html``); - root.appendChild(dialog); - await env.waitForStability(); - expect(openListener) - .withContext('should not open on connected since close was called') - .not.toHaveBeenCalled(); - }); + it('should not open on connected if opened, but closed before connected to DOM', async () => { + const openListener = jasmine.createSpy('openListener'); + const dialog = document.createElement('md-dialog'); + disableDialogAnimations(dialog); + dialog.addEventListener('open', openListener); + dialog.open = true; + await env.waitForStability(); + dialog.open = false; + const root = env.render(html``); + root.appendChild(dialog); + await env.waitForStability(); + expect(openListener) + .withContext('should not open on connected since close was called') + .not.toHaveBeenCalled(); + }); - it('should not open on connected if opened before connection but closed after', - async () => { - const openListener = jasmine.createSpy('openListener'); - const dialog = document.createElement('md-dialog'); - disableDialogAnimations(dialog); - dialog.addEventListener('open', openListener); - dialog.open = true; - const root = env.render(html``); - root.appendChild(dialog); - dialog.open = false; - await env.waitForStability(); - expect(openListener) - .withContext( - 'should not open on connected since close was called before open could complete') - .not.toHaveBeenCalled(); - }); + it('should not open on connected if opened before connection but closed after', async () => { + const openListener = jasmine.createSpy('openListener'); + const dialog = document.createElement('md-dialog'); + disableDialogAnimations(dialog); + dialog.addEventListener('open', openListener); + dialog.open = true; + const root = env.render(html``); + root.appendChild(dialog); + dialog.open = false; + await env.waitForStability(); + expect(openListener) + .withContext( + 'should not open on connected since close was called before open could complete', + ) + .not.toHaveBeenCalled(); + }); it('should not dispatch close if closed while disconnected', async () => { const {harness, root} = await setupTest(); @@ -240,19 +243,19 @@ describe('', () => { await env.waitForStability(); expect(closeListener) - .withContext('should not trigger close when disconnected') - .not.toHaveBeenCalled(); + .withContext('should not trigger close when disconnected') + .not.toHaveBeenCalled(); await harness.element.close(); expect(closeListener) - .withContext('should not trigger close when disconnected') - .not.toHaveBeenCalled(); + .withContext('should not trigger close when disconnected') + .not.toHaveBeenCalled(); root.appendChild(harness.element); await env.waitForStability(); expect(closeListener) - .withContext('should not trigger close when disconnected') - .not.toHaveBeenCalled(); + .withContext('should not trigger close when disconnected') + .not.toHaveBeenCalled(); }); }); diff --git a/dialog/harness.ts b/dialog/harness.ts index 20a5a1a02..185614eb9 100644 --- a/dialog/harness.ts +++ b/dialog/harness.ts @@ -14,7 +14,8 @@ import {Dialog} from './internal/dialog.js'; export class DialogHarness extends Harness { override async getInteractiveElement() { await this.element.updateComplete; - return this.element.querySelector('[autocomplete]') ?? - this.element; + return ( + this.element.querySelector('[autocomplete]') ?? this.element + ); } } diff --git a/dialog/internal/animations.ts b/dialog/internal/animations.ts index 826a9187e..7c7e3add4 100644 --- a/dialog/internal/animations.ts +++ b/dialog/internal/animations.ts @@ -54,20 +54,21 @@ export const DIALOG_DEFAULT_OPEN_ANIMATION: DialogAnimation = { [ // Dialog slide down [{'transform': 'translateY(-50px)'}, {'transform': 'translateY(0)'}], - {duration: 500, easing: EASING.EMPHASIZED} + {duration: 500, easing: EASING.EMPHASIZED}, ], ], scrim: [ [ // Scrim fade in - [{'opacity': 0}, {'opacity': 0.32}], {duration: 500, easing: 'linear'} + [{'opacity': 0}, {'opacity': 0.32}], + {duration: 500, easing: 'linear'}, ], ], container: [ [ // Container fade in [{'opacity': 0}, {'opacity': 1}], - {duration: 50, easing: 'linear', pseudoElement: '::before'} + {duration: 50, easing: 'linear', pseudoElement: '::before'}, ], [ // Container grow @@ -83,21 +84,21 @@ export const DIALOG_DEFAULT_OPEN_ANIMATION: DialogAnimation = { [ // Headline fade in [{'opacity': 0}, {'opacity': 0, offset: 0.2}, {'opacity': 1}], - {duration: 250, easing: 'linear', fill: 'forwards'} + {duration: 250, easing: 'linear', fill: 'forwards'}, ], ], content: [ [ // Content fade in [{'opacity': 0}, {'opacity': 0, offset: 0.2}, {'opacity': 1}], - {duration: 250, easing: 'linear', fill: 'forwards'} + {duration: 250, easing: 'linear', fill: 'forwards'}, ], ], actions: [ [ // Actions fade in [{'opacity': 0}, {'opacity': 0, offset: 0.5}, {'opacity': 1}], - {duration: 300, easing: 'linear', fill: 'forwards'} + {duration: 300, easing: 'linear', fill: 'forwards'}, ], ], }; @@ -110,13 +111,14 @@ export const DIALOG_DEFAULT_CLOSE_ANIMATION: DialogAnimation = { [ // Dialog slide up [{'transform': 'translateY(0)'}, {'transform': 'translateY(-50px)'}], - {duration: 150, easing: EASING.EMPHASIZED_ACCELERATE} + {duration: 150, easing: EASING.EMPHASIZED_ACCELERATE}, ], ], scrim: [ [ // Scrim fade out - [{'opacity': 0.32}, {'opacity': 0}], {duration: 150, easing: 'linear'} + [{'opacity': 0.32}, {'opacity': 0}], + {duration: 150, easing: 'linear'}, ], ], container: [ @@ -133,27 +135,27 @@ export const DIALOG_DEFAULT_CLOSE_ANIMATION: DialogAnimation = { // Container fade out [{'opacity': '1'}, {'opacity': '0'}], {delay: 100, duration: 50, easing: 'linear', pseudoElement: '::before'}, - ] + ], ], headline: [ [ // Headline fade out [{'opacity': 1}, {'opacity': 0}], - {duration: 100, easing: 'linear', fill: 'forwards'} + {duration: 100, easing: 'linear', fill: 'forwards'}, ], ], content: [ [ // Content fade out [{'opacity': 1}, {'opacity': 0}], - {duration: 100, easing: 'linear', fill: 'forwards'} + {duration: 100, easing: 'linear', fill: 'forwards'}, ], ], actions: [ [ // Actions fade out [{'opacity': 1}, {'opacity': 0}], - {duration: 100, easing: 'linear', fill: 'forwards'} + {duration: 100, easing: 'linear', fill: 'forwards'}, ], ], }; diff --git a/dialog/internal/dialog.ts b/dialog/internal/dialog.ts index 190d0f1fa..8e8ee49ab 100644 --- a/dialog/internal/dialog.ts +++ b/dialog/internal/dialog.ts @@ -14,7 +14,12 @@ import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; import {redispatchEvent} from '../../internal/controller/events.js'; -import {DIALOG_DEFAULT_CLOSE_ANIMATION, DIALOG_DEFAULT_OPEN_ANIMATION, DialogAnimation, DialogAnimationArgs} from './animations.js'; +import { + DIALOG_DEFAULT_CLOSE_ANIMATION, + DIALOG_DEFAULT_OPEN_ANIMATION, + DialogAnimation, + DialogAnimationArgs, +} from './animations.js'; /** * A dialog component. @@ -34,7 +39,7 @@ export class Dialog extends LitElement { /** @nocollapse */ static override shadowRootOptions = { ...LitElement.shadowRootOptions, - delegatesFocus: true + delegatesFocus: true, }; /** @@ -91,17 +96,17 @@ export class Dialog extends LitElement { // getIsConnectedPromise() immediately sets the resolve property. private isConnectedPromiseResolve!: () => void; private isConnectedPromise = this.getIsConnectedPromise(); - @query('dialog') private readonly dialog!: HTMLDialogElement|null; - @query('.scrim') private readonly scrim!: HTMLDialogElement|null; - @query('.container') private readonly container!: HTMLDialogElement|null; - @query('.headline') private readonly headline!: HTMLDialogElement|null; - @query('.content') private readonly content!: HTMLDialogElement|null; - @query('.actions') private readonly actions!: HTMLDialogElement|null; + @query('dialog') private readonly dialog!: HTMLDialogElement | null; + @query('.scrim') private readonly scrim!: HTMLDialogElement | null; + @query('.container') private readonly container!: HTMLDialogElement | null; + @query('.headline') private readonly headline!: HTMLDialogElement | null; + @query('.content') private readonly content!: HTMLDialogElement | null; + @query('.actions') private readonly actions!: HTMLDialogElement | null; @state() private isAtScrollTop = false; @state() private isAtScrollBottom = false; - @query('.scroller') private readonly scroller!: HTMLElement|null; - @query('.top.anchor') private readonly topAnchor!: HTMLElement|null; - @query('.bottom.anchor') private readonly bottomAnchor!: HTMLElement|null; + @query('.scroller') private readonly scroller!: HTMLElement | null; + @query('.top.anchor') private readonly topAnchor!: HTMLElement | null; + @query('.bottom.anchor') private readonly bottomAnchor!: HTMLElement | null; private nextClickIsFromContent = false; private intersectionObserver?: IntersectionObserver; // Dialogs should not be SSR'd while open, so we can just use runtime checks. @@ -139,8 +144,9 @@ export class Dialog extends LitElement { return; } - const preventOpen = - !this.dispatchEvent(new Event('open', {cancelable: true})); + const preventOpen = !this.dispatchEvent( + new Event('open', {cancelable: true}), + ); if (preventOpen) { this.open = false; return; @@ -191,8 +197,9 @@ export class Dialog extends LitElement { const prevReturnValue = this.returnValue; this.returnValue = returnValue; - const preventClose = - !this.dispatchEvent(new Event('close', {cancelable: true})); + const preventClose = !this.dispatchEvent( + new Event('close', {cancelable: true}), + ); if (preventClose) { this.returnValue = prevReturnValue; return; @@ -216,7 +223,7 @@ export class Dialog extends LitElement { protected override render() { const scrollable = - this.open && !(this.isAtScrollTop && this.isAtScrollBottom); + this.open && !(this.isAtScrollTop && this.isAtScrollBottom); const classes = { 'has-headline': this.hasHeadline, 'has-actions': this.hasActions, @@ -236,18 +243,16 @@ export class Dialog extends LitElement { role=${this.type === 'alert' ? 'alertdialog' : nothing} @cancel=${this.handleCancel} @click=${this.handleDialogClick} - .returnValue=${this.returnValue || nothing} - > -
+ .returnValue=${this.returnValue || nothing}> +

- +

@@ -260,8 +265,7 @@ export class Dialog extends LitElement {
- +
@@ -269,11 +273,14 @@ export class Dialog extends LitElement { } protected override firstUpdated() { - this.intersectionObserver = new IntersectionObserver(entries => { - for (const entry of entries) { - this.handleAnchorIntersection(entry); - } - }, {root: this.scroller!}); + this.intersectionObserver = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + this.handleAnchorIntersection(entry); + } + }, + {root: this.scroller!}, + ); this.intersectionObserver.observe(this.topAnchor!); this.intersectionObserver.observe(this.bottomAnchor!); @@ -289,8 +296,9 @@ export class Dialog extends LitElement { // Click originated on the backdrop. Native ``s will not cancel, // but Material dialogs do. - const preventDefault = - !this.dispatchEvent(new Event('cancel', {cancelable: true})); + const preventDefault = !this.dispatchEvent( + new Event('cancel', {cancelable: true}), + ); if (preventDefault) { return; } @@ -343,13 +351,16 @@ export class Dialog extends LitElement { scrim: scrimAnimate, headline: headlineAnimate, content: contentAnimate, - actions: actionsAnimate + actions: actionsAnimate, } = animation; const elementAndAnimation: Array<[Element, DialogAnimationArgs[]]> = [ - [dialog, dialogAnimate ?? []], [scrim, scrimAnimate ?? []], - [container, containerAnimate ?? []], [headline, headlineAnimate ?? []], - [content, contentAnimate ?? []], [actions, actionsAnimate ?? []] + [dialog, dialogAnimate ?? []], + [scrim, scrimAnimate ?? []], + [container, containerAnimate ?? []], + [headline, headlineAnimate ?? []], + [content, contentAnimate ?? []], + [actions, actionsAnimate ?? []], ]; const animations: Animation[] = []; @@ -359,7 +370,7 @@ export class Dialog extends LitElement { } } - await Promise.all(animations.map(animation => animation.finished)); + await Promise.all(animations.map((animation) => animation.finished)); } private handleHeadlineChange(event: Event) { @@ -389,7 +400,7 @@ export class Dialog extends LitElement { } private getIsConnectedPromise() { - return new Promise(resolve => { + return new Promise((resolve) => { this.isConnectedPromiseResolve = resolve; }); } diff --git a/divider/demo/demo.ts b/divider/demo/demo.ts index 8d15afdb4..20cb4bfdc 100644 --- a/divider/demo/demo.ts +++ b/divider/demo/demo.ts @@ -4,20 +4,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Divider', [ - new Knob('inset', {defaultValue: true, ui: boolInput()}), - new Knob('inset (start)', {defaultValue: false, ui: boolInput()}), - new Knob('inset (end)', {defaultValue: false, ui: boolInput()}), - ]); +const collection = new MaterialCollection>( + 'Divider', + [ + new Knob('inset', {defaultValue: true, ui: boolInput()}), + new Knob('inset (start)', {defaultValue: false, ui: boolInput()}), + new Knob('inset (end)', {defaultValue: false, ui: boolInput()}), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/divider/demo/stories.ts b/divider/demo/stories.ts index f3365567f..0e2362b3a 100644 --- a/divider/demo/stories.ts +++ b/divider/demo/stories.ts @@ -35,25 +35,24 @@ const standard: MaterialStoryInit = { `, render(knobs) { return html` -
    +
    • List item one
    • + ?inset-end=${knobs['inset (end)']}>
    • List item two
    • List item three
    • + ?inset-end=${knobs['inset (end)']}>
    • List item four
    `; - } + }, }; /** Divider stories. */ diff --git a/elevation/demo/demo.ts b/elevation/demo/demo.ts index ca5ae4d46..716cf1a44 100644 --- a/elevation/demo/demo.ts +++ b/elevation/demo/demo.ts @@ -4,18 +4,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {Knob, numberInput} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Elevation', [ - new Knob('level', {defaultValue: 1, ui: numberInput()}), - ]); +const collection = new MaterialCollection>( + 'Elevation', + [new Knob('level', {defaultValue: 1, ui: numberInput()})], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/fab/demo/demo.ts b/fab/demo/demo.ts index 958a9c5b1..b66dfc12a 100644 --- a/fab/demo/demo.ts +++ b/fab/demo/demo.ts @@ -4,12 +4,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; import {FabSize} from '@material/web/fab/fab.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; -import {boolInput, Knob, selectDropdown, textInput} from './index.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; +import { + boolInput, + Knob, + selectDropdown, + textInput, +} from './index.js'; import {stories, StoryKnobs} from './stories.js'; @@ -24,8 +34,8 @@ const collection = new MaterialCollection>('FAB', [ {label: 'medium', value: 'medium'}, {label: 'small', value: 'small'}, {label: 'large', value: 'large'}, - ] - }) + ], + }), }), ]); diff --git a/fab/demo/stories.ts b/fab/demo/stories.ts index 3574e611a..0eabb61c1 100644 --- a/fab/demo/stories.ts +++ b/fab/demo/stories.ts @@ -4,12 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ +import '@material/web/fab/branded-fab.js'; import '@material/web/fab/fab.js'; import '@material/web/icon/icon.js'; -import '@material/web/fab/branded-fab.js'; import {FabSize} from '@material/web/fab/fab.js'; -import {labelStyles, MaterialStoryInit} from './material-collection.js'; +import { + labelStyles, + MaterialStoryInit, +} from './material-collection.js'; import {css, html, nothing} from 'lit'; /** Knob types for fab stories. */ @@ -17,7 +20,7 @@ export interface StoryKnobs { icon: string; label: string; lowered: boolean; - size: FabSize|undefined; + size: FabSize | undefined; } const styles = css` @@ -46,8 +49,7 @@ const standard: MaterialStoryInit = { variant="surface" .lowered=${lowered} .label=${label} - .size=${size!} - > + .size=${size!}> ${icon} @@ -59,8 +61,7 @@ const standard: MaterialStoryInit = { variant="primary" .lowered=${lowered} .label=${label} - .size=${size!} - > + .size=${size!}> ${icon} @@ -72,8 +73,7 @@ const standard: MaterialStoryInit = { variant="secondary" .lowered=${lowered} .label=${label} - .size=${size!} - > + .size=${size!}> ${icon} @@ -85,8 +85,7 @@ const standard: MaterialStoryInit = { variant="tertiary" .lowered=${lowered} .label=${label} - .size=${size!} - > + .size=${size!}> ${icon} @@ -97,8 +96,7 @@ const standard: MaterialStoryInit = { aria-label=${label ? nothing : 'An example branded FAB'} .lowered=${lowered} .label=${label} - .size=${size!} - > + .size=${size!}> @@ -110,7 +108,7 @@ const standard: MaterialStoryInit = {
`; - } + }, }; /** Checkbox stories. */ diff --git a/fab/fab_test.ts b/fab/fab_test.ts index c39ce3356..c0b9ff2b6 100644 --- a/fab/fab_test.ts +++ b/fab/fab_test.ts @@ -156,8 +156,9 @@ describe('', () => { const env = new Environment(); async function setupTest() { - const element = env.render(html``) - .querySelector('md-branded-fab'); + const element = env + .render(html``) + .querySelector('md-branded-fab'); if (!element) { throw new Error('Could not query rendered .'); } diff --git a/fab/harness.ts b/fab/harness.ts index 199964713..fab878dcd 100644 --- a/fab/harness.ts +++ b/fab/harness.ts @@ -14,7 +14,6 @@ import {Fab} from './internal/fab.js'; export class FabHarness extends Harness { override async getInteractiveElement() { await this.element.updateComplete; - return this.element.renderRoot.querySelector('.fab') as - HTMLButtonElement; + return this.element.renderRoot.querySelector('.fab') as HTMLButtonElement; } } diff --git a/fab/internal/fab.ts b/fab/internal/fab.ts index f4ff602e6..c68bda4ed 100644 --- a/fab/internal/fab.ts +++ b/fab/internal/fab.ts @@ -11,7 +11,7 @@ import {SharedFab} from './shared.js'; /** * The variants available to non-branded FABs. */ -export type FabVariant = 'surface'|'primary'|'secondary'|'tertiary'; +export type FabVariant = 'surface' | 'primary' | 'secondary' | 'tertiary'; // tslint:disable-next-line:enforce-comments-on-exported-symbols export class Fab extends SharedFab { diff --git a/fab/internal/shared.ts b/fab/internal/shared.ts index ad6ffca33..dc15a4b02 100644 --- a/fab/internal/shared.ts +++ b/fab/internal/shared.ts @@ -18,7 +18,7 @@ import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; /** * Sizes variants available to non-extended FABs. */ -export type FabSize = 'medium'|'small'|'large'; +export type FabSize = 'medium' | 'small' | 'large'; // tslint:disable-next-line:enforce-comments-on-exported-symbols export abstract class SharedFab extends LitElement { @@ -45,7 +45,6 @@ export abstract class SharedFab extends LitElement { */ @property() label = ''; - /** * Lowers the FAB's elevation. */ @@ -57,14 +56,11 @@ export abstract class SharedFab extends LitElement { return html` `; } @@ -90,12 +86,13 @@ export abstract class SharedFab extends LitElement { private renderIcon() { const {ariaLabel} = this as ARIAMixinStrict; return html` - - - - `; + + + + `; } } diff --git a/field/demo/demo.ts b/field/demo/demo.ts index 8667c515d..abec3c139 100644 --- a/field/demo/demo.ts +++ b/field/demo/demo.ts @@ -4,32 +4,45 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; -import {boolInput, Knob, numberInput, textInput} from './index.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; +import { + boolInput, + Knob, + numberInput, + textInput, +} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Field', [ - new Knob('label', {ui: textInput(), defaultValue: 'Label'}), - new Knob( - 'Supporting text', - {ui: textInput(), defaultValue: 'Supporting text'}), - new Knob('Error text', {ui: textInput(), defaultValue: 'Error text'}), - new Knob('count', {ui: numberInput(), defaultValue: 0}), - new Knob('max', {ui: numberInput(), defaultValue: 0}), - new Knob('disabled', {ui: boolInput(), defaultValue: false}), - new Knob('error', {ui: boolInput(), defaultValue: false}), - new Knob('focused', {ui: boolInput(), defaultValue: false}), - new Knob('populated', {ui: boolInput(), defaultValue: false}), - new Knob('required', {ui: boolInput(), defaultValue: false}), - new Knob('Leading icon', {ui: boolInput(), defaultValue: false}), - new Knob('Trailing icon', {ui: boolInput(), defaultValue: false}), - new Knob('resizable', {ui: boolInput(), defaultValue: false}), - ]); +const collection = new MaterialCollection>( + 'Field', + [ + new Knob('label', {ui: textInput(), defaultValue: 'Label'}), + new Knob('Supporting text', { + ui: textInput(), + defaultValue: 'Supporting text', + }), + new Knob('Error text', {ui: textInput(), defaultValue: 'Error text'}), + new Knob('count', {ui: numberInput(), defaultValue: 0}), + new Knob('max', {ui: numberInput(), defaultValue: 0}), + new Knob('disabled', {ui: boolInput(), defaultValue: false}), + new Knob('error', {ui: boolInput(), defaultValue: false}), + new Knob('focused', {ui: boolInput(), defaultValue: false}), + new Knob('populated', {ui: boolInput(), defaultValue: false}), + new Knob('required', {ui: boolInput(), defaultValue: false}), + new Knob('Leading icon', {ui: boolInput(), defaultValue: false}), + new Knob('Trailing icon', {ui: boolInput(), defaultValue: false}), + new Knob('resizable', {ui: boolInput(), defaultValue: false}), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/field/demo/stories.ts b/field/demo/stories.ts index e28488e99..2871e349b 100644 --- a/field/demo/stories.ts +++ b/field/demo/stories.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '@material/web/icon/icon.js'; import '@material/web/field/filled-field.js'; import '@material/web/field/outlined-field.js'; +import '@material/web/icon/icon.js'; import {MaterialStoryInit} from './material-collection.js'; import {css, html, nothing} from 'lit'; @@ -52,16 +52,16 @@ const filled: MaterialStoryInit = { required, resizable, count, - max + max, } = knobs; const supportingText = knobs['Supporting text']; const errorText = knobs['Error text']; const hasStart = knobs['Leading icon']; const hasEnd = knobs['Trailing icon']; - const content = resizable ? - html`` : - html``; + const content = resizable + ? html`` + : html``; const styles = {resize: resizable ? 'both' : null}; return html` @@ -78,14 +78,12 @@ const filled: MaterialStoryInit = { supporting-text=${supportingText} error-text=${errorText} count=${count} - max=${max} - > - ${hasStart ? START_CONTENT : nothing} - ${content} + max=${max}> + ${hasStart ? START_CONTENT : nothing} ${content} ${hasEnd ? END_CONTENT : nothing} `; - } + }, }; const outlined: MaterialStoryInit = { @@ -101,16 +99,18 @@ const outlined: MaterialStoryInit = { required, resizable, count, - max + max, } = knobs; const supportingText = knobs['Supporting text']; const errorText = knobs['Error text']; const hasStart = knobs['Leading icon']; const hasEnd = knobs['Trailing icon']; - const content = resizable ? - html`` : - html``; + const content = resizable + ? html`` + : html``; const styles = {resize: resizable ? 'both' : null}; return html` @@ -127,15 +127,13 @@ const outlined: MaterialStoryInit = { supporting-text=${supportingText} error-text=${errorText} count=${count} - max=${max} - > + max=${max}> - ${hasStart ? START_CONTENT : nothing} - ${content} + ${hasStart ? START_CONTENT : nothing} ${content} ${hasEnd ? END_CONTENT : nothing} `; - } + }, }; /** Field stories. */ diff --git a/field/harness.ts b/field/harness.ts index 5b6e6c773..4c661c8c5 100644 --- a/field/harness.ts +++ b/field/harness.ts @@ -30,6 +30,6 @@ export class FieldHarness extends Harness { protected override async getInteractiveElement() { await this.element.updateComplete; return (this.element.querySelector(':not([slot])') || - this.element.renderRoot.querySelector('.field')) as HTMLElement; + this.element.renderRoot.querySelector('.field')) as HTMLElement; } } diff --git a/field/internal/field.ts b/field/internal/field.ts index ae262856c..1ee21e56e 100644 --- a/field/internal/field.ts +++ b/field/internal/field.ts @@ -4,7 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {html, LitElement, nothing, PropertyValues, render, TemplateResult} from 'lit'; +import { + html, + LitElement, + nothing, + PropertyValues, + render, + TemplateResult, +} from 'lit'; import {property, query, queryAssignedElements, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; @@ -60,9 +67,10 @@ export class Field extends LitElement { */ @state() private refreshErrorAlert = false; @state() private disableTransitions = false; - @query('.label.floating') private readonly floatingLabelEl!: HTMLElement|null; - @query('.label.resting') private readonly restingLabelEl!: HTMLElement|null; - @query('.container') private readonly containerEl!: HTMLElement|null; + @query('.label.floating') + private readonly floatingLabelEl!: HTMLElement | null; + @query('.label.resting') private readonly restingLabelEl!: HTMLElement | null; + @query('.container') private readonly containerEl!: HTMLElement | null; /** * Re-announces the field's error supporting text to screen readers. @@ -78,7 +86,7 @@ export class Field extends LitElement { protected override update(props: PropertyValues) { // Client-side property updates const isDisabledChanging = - props.has('disabled') && props.get('disabled') !== undefined; + props.has('disabled') && props.get('disabled') !== undefined; if (isDisabledChanging) { this.disableTransitions = true; } @@ -92,7 +100,7 @@ export class Field extends LitElement { // Animate if focused or populated change. this.animateLabelIfNeeded({ wasFocused: props.get('focused'), - wasPopulated: props.get('populated') + wasPopulated: props.get('populated'), }); super.update(props); @@ -118,17 +126,14 @@ export class Field extends LitElement { return html`
- ${this.renderBackground?.()} - ${this.renderIndicator?.()} - ${outline} + ${this.renderBackground?.()} ${this.renderIndicator?.()} ${outline}
- ${restingLabel} - ${outline ? nothing : floatingLabel} + ${restingLabel} ${outline ? nothing : floatingLabel}
@@ -145,8 +150,12 @@ export class Field extends LitElement { } protected override updated(changed: PropertyValues) { - if (changed.has('supportingText') || changed.has('errorText') || - changed.has('count') || changed.has('max')) { + if ( + changed.has('supportingText') || + changed.has('errorText') || + changed.has('count') || + changed.has('max') + ) { this.updateSlottedAriaDescribedBy(); } @@ -180,21 +189,22 @@ export class Field extends LitElement { const start = html`${supportingOrErrorText}`; // Conditionally render counter so we don't render the extra `gap`. // TODO(b/244473435): add aria-label and announcements - const end = counterText ? - html`${counterText}` : - nothing; + const end = counterText + ? html`${counterText}` + : nothing; // Announce if there is an error and error text visible. // If refreshErrorAlert is true, do not announce. This will remove the // role="alert" attribute. Another render cycle will happen after an // animation frame to re-add the role. const shouldErrorAnnounce = - this.error && this.errorText && !this.refreshErrorAlert; + this.error && this.errorText && !this.refreshErrorAlert; const role = shouldErrorAnnounce ? 'alert' : nothing; return html`
${start}${end}
- + `; } @@ -230,15 +240,18 @@ export class Field extends LitElement { const labelText = `${this.label}${this.required ? '*' : ''}`; return html` - ${labelText} + ${labelText} `; } - private animateLabelIfNeeded({wasFocused, wasPopulated}: { - wasFocused?: boolean, - wasPopulated?: boolean + private animateLabelIfNeeded({ + wasFocused, + wasPopulated, + }: { + wasFocused?: boolean; + wasPopulated?: boolean; }) { if (!this.label) { return; @@ -268,7 +281,9 @@ export class Field extends LitElement { // from appearing. // TODO(b/241113345): use animation tokens this.labelAnimation = this.floatingLabelEl?.animate( - this.getLabelKeyframes(), {duration: 150, easing: EASING.STANDARD}); + this.getLabelKeyframes(), + {duration: 150, easing: EASING.STANDARD}, + ); this.labelAnimation?.addEventListener('finish', () => { // At the end of the animation, update the visible label. @@ -282,10 +297,16 @@ export class Field extends LitElement { return []; } - const {x: floatingX, y: floatingY, height: floatingHeight} = - floatingLabelEl.getBoundingClientRect(); - const {x: restingX, y: restingY, height: restingHeight} = - restingLabelEl.getBoundingClientRect(); + const { + x: floatingX, + y: floatingY, + height: floatingHeight, + } = floatingLabelEl.getBoundingClientRect(); + const { + x: restingX, + y: restingY, + height: restingHeight, + } = restingLabelEl.getBoundingClientRect(); const floatingScrollWidth = floatingLabelEl.scrollWidth; const restingScrollWidth = restingLabelEl.scrollWidth; // Scale by width ratio instead of font size since letter-spacing will scale @@ -298,14 +319,15 @@ export class Field extends LitElement { // we move the floating label down to the resting label's position, it won't // exactly match because of this. We need to adjust by half of what the // final scaled floating label's height will be. - const yDelta = restingY - floatingY + - Math.round((restingHeight - floatingHeight * scale) / 2); + const yDelta = + restingY - + floatingY + + Math.round((restingHeight - floatingHeight * scale) / 2); // Create the two transforms: floating to resting (using the calculations // above), and resting to floating (re-setting the transform to initial // values). - const restTransform = - `translateX(${xDelta}px) translateY(${yDelta}px) scale(${scale})`; + const restTransform = `translateX(${xDelta}px) translateY(${yDelta}px) scale(${scale})`; const floatTransform = `translateX(0) translateY(0) scale(1)`; // Constrain the floating labels width to a scaled percentage of the @@ -316,12 +338,14 @@ export class Field extends LitElement { const width = isRestingClipped ? `${restingClientWidth / scale}px` : ''; if (this.focused || this.populated) { return [ - {transform: restTransform, width}, {transform: floatTransform, width} + {transform: restTransform, width}, + {transform: floatTransform, width}, ]; } return [ - {transform: floatTransform, width}, {transform: restTransform, width} + {transform: floatTransform, width}, + {transform: restTransform, width}, ]; } diff --git a/field/internal/field_test.ts b/field/internal/field_test.ts index 31f2a44a9..5f40b4b46 100644 --- a/field/internal/field_test.ts +++ b/field/internal/field_test.ts @@ -31,8 +31,11 @@ class TestField extends Field { } didErrorAnnounce() { - return this.renderRoot.querySelector('.supporting-text') - ?.getAttribute('role') === 'alert'; + return ( + this.renderRoot + .querySelector('.supporting-text') + ?.getAttribute('role') === 'alert' + ); } // Ensure floating/resting labels are both rendered @@ -54,9 +57,8 @@ describe('Field', () => { .populated=${props.populated ?? false} .required=${props.required ?? false} .supportingText=${props.supportingText ?? ''} - .errorText=${props.errorText ?? ''} - > - + .errorText=${props.errorText ?? ''}> + `; const root = env.render(template); @@ -82,8 +84,8 @@ describe('Field', () => { await env.waitForStability(); // Assertion. expect(instance.focused) - .withContext('focused is false after disabled is set to true') - .toBe(false); + .withContext('focused is false after disabled is set to true') + .toBe(false); }); it('should not allow focus when disabled', async () => { @@ -94,8 +96,8 @@ describe('Field', () => { await env.waitForStability(); // Assertion. expect(instance.focused) - .withContext('focused set back to false when disabled') - .toBe(false); + .withContext('focused set back to false when disabled') + .toBe(false); }); /* @@ -291,9 +293,10 @@ describe('Field', () => { const {instance} = await setupTest({label: undefined}); // Assertion. expect(instance.labelText) - .withContext( - 'label text should be empty string if label is not provided') - .toBe(''); + .withContext( + 'label text should be empty string if label is not provided', + ) + .toBe(''); }); it('should render label', async () => { @@ -303,8 +306,8 @@ describe('Field', () => { const {instance} = await setupTest({label: labelValue}); // Assertion. expect(instance.labelText) - .withContext('label text should equal label when not required') - .toBe(labelValue); + .withContext('label text should equal label when not required') + .toBe(labelValue); }); it('should adds asterisk if required', async () => { @@ -314,44 +317,49 @@ describe('Field', () => { const {instance} = await setupTest({required: true, label: labelValue}); // Assertion. expect(instance.labelText) - .withContext( - 'label text should equal label with asterisk when required') - .toBe(`${labelValue}*`); + .withContext( + 'label text should equal label with asterisk when required', + ) + .toBe(`${labelValue}*`); }); - it('should not render asterisk if required when there is no label', - async () => { - // Setup. - // Test case. - const {instance} = await setupTest({required: true, label: undefined}); - // Assertion. - expect(instance.labelText) - .withContext( - 'label text should be empty string if label is not provided, even when required') - .toBe(''); - }); + it('should not render asterisk if required when there is no label', async () => { + // Setup. + // Test case. + const {instance} = await setupTest({required: true, label: undefined}); + // Assertion. + expect(instance.labelText) + .withContext( + 'label text should be empty string if label is not provided, even when required', + ) + .toBe(''); + }); }); describe('supporting text', () => { it('should update to errorText when error is true', async () => { const errorText = 'Error message'; - const {instance} = await setupTest( - {error: true, supportingText: 'Supporting text', errorText}); + const {instance} = await setupTest({ + error: true, + supportingText: 'Supporting text', + errorText, + }); expect(instance.supportingTextContent).toEqual(errorText); }); }); describe('error announcement', () => { - it('should announce errors when both error and errorText are set', - async () => { - const {instance} = - await setupTest({error: true, errorText: 'Error message'}); + it('should announce errors when both error and errorText are set', async () => { + const {instance} = await setupTest({ + error: true, + errorText: 'Error message', + }); - expect(instance.didErrorAnnounce()) - .withContext('instance.didErrorAnnounce()') - .toBeTrue(); - }); + expect(instance.didErrorAnnounce()) + .withContext('instance.didErrorAnnounce()') + .toBeTrue(); + }); it('should not announce supporting text', async () => { const {instance} = await setupTest(); @@ -360,26 +368,28 @@ describe('Field', () => { await env.waitForStability(); expect(instance.didErrorAnnounce()) - .withContext('instance.didErrorAnnounce()') - .toBeFalse(); + .withContext('instance.didErrorAnnounce()') + .toBeFalse(); }); it('should re-announce when reannounceError() is called', async () => { - const {instance} = - await setupTest({error: true, errorText: 'Error message'}); + const {instance} = await setupTest({ + error: true, + errorText: 'Error message', + }); instance.reannounceError(); await env.waitForStability(); // After lit update, but before re-render refresh expect(instance.didErrorAnnounce()) - .withContext('didErrorAnnounce() before refresh') - .toBeFalse(); + .withContext('didErrorAnnounce() before refresh') + .toBeFalse(); // After the second lit update render refresh await env.waitForStability(); expect(instance.didErrorAnnounce()) - .withContext('didErrorAnnounce() after refresh') - .toBeTrue(); + .withContext('didErrorAnnounce() after refresh') + .toBeTrue(); }); }); }); diff --git a/focus/demo/demo.ts b/focus/demo/demo.ts index 3b85ae95b..00c5e6447 100644 --- a/focus/demo/demo.ts +++ b/focus/demo/demo.ts @@ -4,18 +4,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Focus', [ - new Knob('inward', {ui: boolInput(), defaultValue: false}), - ]); +const collection = new MaterialCollection>( + 'Focus', + [new Knob('inward', {ui: boolInput(), defaultValue: false})], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/focus/demo/stories.ts b/focus/demo/stories.ts index 073cbf81d..9d30932b9 100644 --- a/focus/demo/stories.ts +++ b/focus/demo/stories.ts @@ -55,13 +55,13 @@ const standard: MaterialStoryInit = { `; - } + }, }; const multiAction: MaterialStoryInit = { name: 'Multi-action components', styles: css` - [role="list"] { + [role='list'] { align-items: center; appearance: none; background: var(--md-sys-color-surface); @@ -76,11 +76,11 @@ const multiAction: MaterialStoryInit = { position: relative; } - [role="list"]:focus-within { + [role='list']:focus-within { background: var(--md-sys-color-surface-variant); } - [role="listitem"] { + [role='listitem'] { display: flex; flex: 1; } @@ -105,7 +105,7 @@ const multiAction: MaterialStoryInit = { --md-focus-ring-shape: 32px; } - [role="list"]::before, + [role='list']::before, #secondary::before { border: 1px solid var(--md-sys-color-outline); border-radius: inherit; @@ -120,20 +120,24 @@ const multiAction: MaterialStoryInit = {
-
-
`; - } + }, }; /** Focus ring stories. */ diff --git a/focus/internal/focus-ring.ts b/focus/internal/focus-ring.ts index edea7d7b9..d54a5b7d5 100644 --- a/focus/internal/focus-ring.ts +++ b/focus/internal/focus-ring.ts @@ -7,7 +7,10 @@ import {isServer, LitElement, PropertyValues} from 'lit'; import {property} from 'lit/decorators.js'; -import {Attachable, AttachableController} from '../../internal/controller/attachable-controller.js'; +import { + Attachable, + AttachableController, +} from '../../internal/controller/attachable-controller.js'; /** * Events that the focus ring listens to. @@ -34,19 +37,21 @@ export class FocusRing extends LitElement implements Attachable { return this.attachableController.htmlFor; } - set htmlFor(htmlFor: string|null) { + set htmlFor(htmlFor: string | null) { this.attachableController.htmlFor = htmlFor; } get control() { return this.attachableController.control; } - set control(control: HTMLElement|null) { + set control(control: HTMLElement | null) { this.attachableController.control = control; } - private readonly attachableController = - new AttachableController(this, this.onControlChange.bind(this)); + private readonly attachableController = new AttachableController( + this, + this.onControlChange.bind(this), + ); attach(control: HTMLElement) { this.attachableController.attach(control); @@ -86,7 +91,7 @@ export class FocusRing extends LitElement implements Attachable { event[HANDLED_BY_FOCUS_RING] = true; } - private onControlChange(prev: HTMLElement|null, next: HTMLElement|null) { + private onControlChange(prev: HTMLElement | null, next: HTMLElement | null) { if (isServer) return; for (const event of EVENTS) { diff --git a/focus/internal/focus-ring_test.ts b/focus/internal/focus-ring_test.ts index 0b51cf8ef..f252e56f2 100644 --- a/focus/internal/focus-ring_test.ts +++ b/focus/internal/focus-ring_test.ts @@ -64,27 +64,26 @@ describe('focus ring', () => { expect(focusRing.control).withContext('focusRing.control').toBe(button); }); - it('should update a referenced element when for attribute changes', - async () => { - const {root, focusRing} = setupTest(html` - - - - `); + it('should update a referenced element when for attribute changes', async () => { + const {root, focusRing} = setupTest(html` + + + + `); - const secondButton = root.querySelector('#second'); - if (!secondButton) { - throw new Error('Could not query rendered
`; - } + }, }; const links: MaterialStoryInit = { @@ -129,38 +119,34 @@ const links: MaterialStoryInit = { + target="_blank"> ${icon} + target="_blank"> ${icon} + target="_blank"> ${icon} + target="_blank"> ${icon}
`; - } + }, }; /** Icon button stories. */ diff --git a/iconbutton/filled-icon-button_test.ts b/iconbutton/filled-icon-button_test.ts index 837dfc784..7ec5d9b2d 100644 --- a/iconbutton/filled-icon-button_test.ts +++ b/iconbutton/filled-icon-button_test.ts @@ -14,4 +14,4 @@ describe('', () => { describe('.styles', () => { createTokenTests(MdFilledIconButton.styles); }); -}); \ No newline at end of file +}); diff --git a/iconbutton/filled-tonal-icon-button_test.ts b/iconbutton/filled-tonal-icon-button_test.ts index 971f56e04..b8fc9958e 100644 --- a/iconbutton/filled-tonal-icon-button_test.ts +++ b/iconbutton/filled-tonal-icon-button_test.ts @@ -14,4 +14,4 @@ describe(' { describe('.styles', () => { createTokenTests(MdFilledTonalIconButton.styles); }); -}); \ No newline at end of file +}); diff --git a/iconbutton/icon-button_test.ts b/iconbutton/icon-button_test.ts index ef517e9f7..ffd4a81e8 100644 --- a/iconbutton/icon-button_test.ts +++ b/iconbutton/icon-button_test.ts @@ -44,73 +44,83 @@ describe('icon button tests', () => { }); describe('md-icon-button', () => { - it('setting `disabled` updates the disabled attribute on the native ' + - 'button element', - async () => { - const {element} = await setUpTest('button'); - const button = element.shadowRoot!.querySelector('button')!; + it( + 'setting `disabled` updates the disabled attribute on the native ' + + 'button element', + async () => { + const {element} = await setUpTest('button'); + const button = element.shadowRoot!.querySelector('button')!; - element.disabled = true; - await element.updateComplete; - expect(button.hasAttribute('disabled')).toBeTrue(); + element.disabled = true; + await element.updateComplete; + expect(button.hasAttribute('disabled')).toBeTrue(); - element.disabled = false; - await element.updateComplete; - expect(button.hasAttribute('disabled')).toBeFalse(); - }); + element.disabled = false; + await element.updateComplete; + expect(button.hasAttribute('disabled')).toBeFalse(); + }, + ); - it('setting `ariaLabel` updates the aria-label attribute on the native ' + - 'button element', - async () => { - const {element} = await setUpTest('button'); - const button = element.shadowRoot!.querySelector('button')!; + it( + 'setting `ariaLabel` updates the aria-label attribute on the native ' + + 'button element', + async () => { + const {element} = await setUpTest('button'); + const button = element.shadowRoot!.querySelector('button')!; - element.ariaLabel = 'test'; - await element.updateComplete; - expect(button.getAttribute('aria-label')).toBe('test'); - }); + element.ariaLabel = 'test'; + await element.updateComplete; + expect(button.getAttribute('aria-label')).toBe('test'); + }, + ); }); describe('md-icon-button link', () => { - it('setting `ariaLabel` updates the aria-label attribute on the anchor' + - 'tag', - async () => { - const {element} = await setUpTest('link'); - const anchor = element.shadowRoot!.querySelector('a')!; - expect(anchor).not.toBeNull(); + it( + 'setting `ariaLabel` updates the aria-label attribute on the anchor' + + 'tag', + async () => { + const {element} = await setUpTest('link'); + const anchor = element.shadowRoot!.querySelector('a')!; + expect(anchor).not.toBeNull(); - element.ariaLabel = 'test'; - await element.updateComplete; - expect(anchor.getAttribute('aria-label')).toBe('test'); - }); + element.ariaLabel = 'test'; + await element.updateComplete; + expect(anchor.getAttribute('aria-label')).toBe('test'); + }, + ); }); describe('md-icon-button toggle', () => { - it('setting `disabled` updates the disabled attribute on the native ' + - 'button element', - async () => { - const {element} = await setUpTest('toggle'); - const button = element.shadowRoot!.querySelector('button')!; + it( + 'setting `disabled` updates the disabled attribute on the native ' + + 'button element', + async () => { + const {element} = await setUpTest('toggle'); + const button = element.shadowRoot!.querySelector('button')!; - element.disabled = true; - await element.updateComplete; - expect(button.hasAttribute('disabled')).toBeTrue(); + element.disabled = true; + await element.updateComplete; + expect(button.hasAttribute('disabled')).toBeTrue(); - element.disabled = false; - await element.updateComplete; - expect(button.hasAttribute('disabled')).toBeFalse(); - }); + element.disabled = false; + await element.updateComplete; + expect(button.hasAttribute('disabled')).toBeFalse(); + }, + ); - it('setting `ariaLabel` updates the aria-label attribute on the native ' + - 'button element', - async () => { - const {element} = await setUpTest('toggle'); - const button = element.shadowRoot!.querySelector('button')!; + it( + 'setting `ariaLabel` updates the aria-label attribute on the native ' + + 'button element', + async () => { + const {element} = await setUpTest('toggle'); + const button = element.shadowRoot!.querySelector('button')!; - element.ariaLabel = 'test'; - await element.updateComplete; - expect(button.getAttribute('aria-label')).toBe('test'); - }); + element.ariaLabel = 'test'; + await element.updateComplete; + expect(button.getAttribute('aria-label')).toBe('test'); + }, + ); it('toggles the `selected` state when button is clicked', async () => { const {element, harness} = await setUpTest('toggle'); @@ -126,8 +136,8 @@ describe('icon button tests', () => { const {element, harness} = await setUpTest('toggle'); let changeEvent = false; let inputEvent = false; - element.addEventListener('input', () => inputEvent = true); - element.addEventListener('change', () => changeEvent = true); + element.addEventListener('input', () => (inputEvent = true)); + element.addEventListener('change', () => (changeEvent = true)); expect(element.selected).toBeFalse(); await harness.clickWithMouse(); expect(element.selected).toBeTrue(); @@ -135,19 +145,18 @@ describe('icon button tests', () => { expect(changeEvent).toBeTrue(); }); - it('setting `selected` updates the aria-pressed attribute on the native button element', - async () => { - const {element} = await setUpTest('toggle'); + it('setting `selected` updates the aria-pressed attribute on the native button element', async () => { + const {element} = await setUpTest('toggle'); - element.selected = true; - await element.updateComplete; - const button = element.shadowRoot!.querySelector('button')!; - expect(button.getAttribute('aria-pressed')).toEqual('true'); + element.selected = true; + await element.updateComplete; + const button = element.shadowRoot!.querySelector('button')!; + expect(button.getAttribute('aria-pressed')).toEqual('true'); - element.selected = false; - await element.updateComplete; - expect(button.getAttribute('aria-pressed')).toEqual('false'); - }); + element.selected = false; + await element.updateComplete; + expect(button.getAttribute('aria-pressed')).toEqual('false'); + }); it('button with toggled aria label toggles aria label', async () => { const {element, harness} = await setUpTest('toggle'); @@ -167,45 +176,41 @@ describe('icon button tests', () => { }); it('if `flipsIconInRtl=true`, flips icon in an RTL context', async () => { - const template = html` -
- - star - -
`; + const template = html`
+ + star + +
`; const element = env.render(template).querySelector('md-icon-button')!; await env.waitForStability(); expect((element as unknown as IconButtonInternals).flipIcon).toBeTrue(); }); - it('if `flipsIconInRtl=true`, does not flip icon in an LTR context', - async () => { - const template = html` -
- - star - -
`; - const element = env.render(template).querySelector('md-icon-button')!; - await env.waitForStability(); + it('if `flipsIconInRtl=true`, does not flip icon in an LTR context', async () => { + const template = html`
+ + star + +
`; + const element = env.render(template).querySelector('md-icon-button')!; + await env.waitForStability(); - expect((element as unknown as IconButtonInternals).flipIcon) - .toBeFalse(); - }); + expect((element as unknown as IconButtonInternals).flipIcon).toBeFalse(); + }); it('should allow preventing toggle click event', async () => { const {element, harness} = await setUpTest('toggle'); - element.addEventListener('click', event => { + element.addEventListener('click', (event) => { event.preventDefault(); }); expect(element.selected).withContext('selected before click').toBeFalse(); await harness.clickWithMouse(); expect(element.selected) - .withContext('selected after prevent default click') - .toBeFalse(); + .withContext('selected after prevent default click') + .toBeFalse(); }); }); diff --git a/iconbutton/internal/icon-button.ts b/iconbutton/internal/icon-button.ts index 03d01d73e..9162480c7 100644 --- a/iconbutton/internal/icon-button.ts +++ b/iconbutton/internal/icon-button.ts @@ -10,15 +10,19 @@ import '../../ripple/ripple.js'; import {html, LitElement, nothing} from 'lit'; import {property, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; -import {html as staticHtml, literal} from 'lit/static-html.js'; +import {literal, html as staticHtml} from 'lit/static-html.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; import {internals} from '../../internal/controller/element-internals.js'; -import {FormSubmitter, FormSubmitterType, setupFormSubmitter} from '../../internal/controller/form-submitter.js'; +import { + FormSubmitter, + FormSubmitterType, + setupFormSubmitter, +} from '../../internal/controller/form-submitter.js'; import {isRtl} from '../../internal/controller/is-rtl.js'; -type LinkTarget = '_blank'|'_parent'|'_self'|'_top'; +type LinkTarget = '_blank' | '_parent' | '_self' | '_top'; // tslint:disable-next-line:enforce-comments-on-exported-symbols export class IconButton extends LitElement implements FormSubmitter { @@ -31,8 +35,10 @@ export class IconButton extends LitElement implements FormSubmitter { static readonly formAssociated = true; /** @nocollapse */ - static override shadowRootOptions: - ShadowRootInit = {mode: 'open', delegatesFocus: true}; + static override shadowRootOptions: ShadowRootInit = { + mode: 'open', + delegatesFocus: true, + }; /** * Disables the icon button and makes it non-interactive. @@ -53,7 +59,7 @@ export class IconButton extends LitElement implements FormSubmitter { /** * Sets the underlying `HTMLAnchorElement`'s `target` attribute. */ - @property() target: LinkTarget|'' = ''; + @property() target: LinkTarget | '' = ''; /** * The `aria-label` of the button when the button is toggleable and selected. @@ -101,8 +107,8 @@ export class IconButton extends LitElement implements FormSubmitter { @state() private flipIcon = isRtl(this, this.flipIconInRtl); /** @private */ - [internals] = - (this as HTMLElement /* needed for closure */).attachInternals(); + [internals] = (this as HTMLElement) /* needed for closure */ + .attachInternals(); /** * Link buttons cannot be disabled. @@ -119,18 +125,19 @@ export class IconButton extends LitElement implements FormSubmitter { const {ariaLabel, ariaHasPopup, ariaExpanded} = this as ARIAMixinStrict; const hasToggledAriaLabel = ariaLabel && this.ariaLabelSelected; const ariaPressedValue = !this.toggle ? nothing : this.selected; - let ariaLabelValue: string|null|typeof nothing = nothing; + let ariaLabelValue: string | null | typeof nothing = nothing; if (!this.href) { - ariaLabelValue = (hasToggledAriaLabel && this.selected) ? - this.ariaLabelSelected : - ariaLabel; + ariaLabelValue = + hasToggledAriaLabel && this.selected + ? this.ariaLabelSelected + : ariaLabel; } return staticHtml`<${tag} class="icon-button ${classMap(this.getRenderClasses())}" id="button" aria-label="${ariaLabelValue || nothing}" - aria-haspopup="${!this.href && ariaHasPopup || nothing}" - aria-expanded="${!this.href && ariaExpanded || nothing}" + aria-haspopup="${(!this.href && ariaHasPopup) || nothing}" + aria-expanded="${(!this.href && ariaExpanded) || nothing}" aria-pressed="${ariaPressedValue}" ?disabled="${!this.href && this.disabled}" @click="${this.handleClick}"> @@ -147,12 +154,12 @@ export class IconButton extends LitElement implements FormSubmitter { // Needed for closure conformance const {ariaLabel} = this as ARIAMixinStrict; return html` -
+ aria-label="${ariaLabel || nothing}"> `; } @@ -169,7 +176,9 @@ export class IconButton extends LitElement implements FormSubmitter { private renderSelectedIcon() { // Use default slot as fallback to not require specifying multiple icons - return html``; + return html``; } private renderTouchTarget() { @@ -177,15 +186,15 @@ export class IconButton extends LitElement implements FormSubmitter { } private renderFocusRing() { - return html``; + return html``; } private renderRipple() { return html``; + ?disabled="${!this.href && this.disabled}">`; } override connectedCallback() { @@ -202,7 +211,8 @@ export class IconButton extends LitElement implements FormSubmitter { this.selected = !this.selected; this.dispatchEvent( - new InputEvent('input', {bubbles: true, composed: true})); + new InputEvent('input', {bubbles: true, composed: true}), + ); // Bubbles but does not compose to mimic native browser & { override async startHover() { const field = await this.getField(); - const element = - await (new SelectFieldHardness(field)).getInteractiveElement(); + const element = await new SelectFieldHardness( + field, + ).getInteractiveElement(); this.simulateStartHover(element); } /** @return ListItem harnesses for the menu's items. */ getItems() { return this.element.options.map( - (item) => new SelectOptionHarness(item as typeof item&LitElement)); + (item) => new SelectOptionHarness(item as typeof item & LitElement), + ); } async click() { @@ -50,7 +52,8 @@ export class SelectHarness extends Harness` validation message for i18n. diff --git a/select/internal/selectoption/select-option.ts b/select/internal/selectoption/select-option.ts index 7a1299b91..7f0a61173 100644 --- a/select/internal/selectoption/select-option.ts +++ b/select/internal/selectoption/select-option.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import '../../../ripple/ripple.js'; import '../../../focus/md-focus-ring.js'; import '../../../labs/item/item.js'; +import '../../../ripple/ripple.js'; import {html, LitElement, nothing} from 'lit'; import {property, query, queryAssignedElements} from 'lit/decorators.js'; @@ -32,7 +32,7 @@ export class SelectOptionEl extends LitElement implements SelectOption { /** @nocollapse */ static override shadowRootOptions = { ...LitElement.shadowRootOptions, - delegatesFocus: true + delegatesFocus: true, }; /** @@ -55,7 +55,7 @@ export class SelectOptionEl extends LitElement implements SelectOption { */ @property() value = ''; - @query('.list-item') protected readonly listItemRoot!: HTMLElement|null; + @query('.list-item') protected readonly listItemRoot!: HTMLElement | null; @queryAssignedElements({slot: 'headline'}) protected readonly headlineElements!: HTMLElement[]; @@ -99,8 +99,7 @@ export class SelectOptionEl extends LitElement implements SelectOption { return this.renderListItem(html`
- ${this.renderRipple()} - ${this.renderFocusRing()} + ${this.renderRipple()} ${this.renderFocusRing()}
@@ -128,7 +127,8 @@ export class SelectOptionEl extends LitElement implements SelectOption { class="list-item ${classMap(this.getRenderClasses())}" @click=${this.selectOptionController.onClick} @keydown=${this.selectOptionController.onKeydown} - >${content} + >${content} `; } @@ -136,22 +136,20 @@ export class SelectOptionEl extends LitElement implements SelectOption { * Handles rendering of the ripple element. */ protected renderRipple() { - return html` - `; + return html` `; } /** * Handles rendering of the focus ring. */ protected renderFocusRing() { - return html` - `; + return html` `; } /** @@ -173,8 +171,9 @@ export class SelectOptionEl extends LitElement implements SelectOption { - + `; } diff --git a/select/internal/selectoption/selectOptionController.ts b/select/internal/selectoption/selectOptionController.ts index b545371f5..2131570cb 100644 --- a/select/internal/selectoption/selectOptionController.ts +++ b/select/internal/selectoption/selectOptionController.ts @@ -6,7 +6,11 @@ import {ReactiveController, ReactiveControllerHost} from 'lit'; -import {MenuItem, MenuItemController, MenuItemControllerConfig} from '../../../menu/internal/controllers/menuItemController.js'; +import { + MenuItem, + MenuItemController, + MenuItemControllerConfig, +} from '../../../menu/internal/controllers/menuItemController.js'; /** * The interface specific to a Select Option @@ -32,7 +36,7 @@ interface SelectOptionSelf { * The interface to implement for a select option. Additionally, the element * must have `md-list-item` and `md-menu-item` attributes on the host. */ -export type SelectOption = SelectOptionSelf&MenuItem; +export type SelectOption = SelectOptionSelf & MenuItem; /** * Creates an event fired by a SelectOption to request selection from md-select. @@ -67,9 +71,8 @@ export type SelectOptionConfig = MenuItemControllerConfig; */ export class SelectOptionController implements ReactiveController { private readonly menuItemController: MenuItemController; - private readonly getHeadlineElements: - SelectOptionConfig['getHeadlineElements']; - private internalDisplayText: string|null = null; + private readonly getHeadlineElements: SelectOptionConfig['getHeadlineElements']; + private internalDisplayText: string | null = null; private lastSelected = this.host.selected; private firstUpdate = true; @@ -122,8 +125,9 @@ export class SelectOptionController implements ReactiveController { * @param config The object that configures this controller's behavior. */ constructor( - private readonly host: ReactiveControllerHost&SelectOption, - config: SelectOptionConfig) { + private readonly host: ReactiveControllerHost & SelectOption, + config: SelectOptionConfig, + ) { this.menuItemController = new MenuItemController(host, config); this.getHeadlineElements = config.getHeadlineElements; host.addController(this); @@ -168,4 +172,4 @@ export class SelectOptionController implements ReactiveController { onKeydown = (e: KeyboardEvent) => { this.menuItemController.onKeydown(e); }; -} \ No newline at end of file +} diff --git a/select/select_test.ts b/select/select_test.ts index 685ff564c..89f870679 100644 --- a/select/select_test.ts +++ b/select/select_test.ts @@ -35,14 +35,15 @@ describe('', () => { it('clicking on option triggers change', async () => { let changed = false; render( - html` - { + html` { changed = true; }}> - - - `, - root); + + + `, + root, + ); const selectEl = root.querySelector('md-outlined-select')!; await selectEl.updateComplete; @@ -53,7 +54,7 @@ describe('', () => { describe('forms', () => { createFormTests({ - queryControl: root => root.querySelector('md-outlined-select'), + queryControl: (root) => root.querySelector('md-outlined-select'), valueTests: [ { name: 'unnamed', @@ -65,9 +66,9 @@ describe('', () => { `, assertValue(formData) { expect(formData) - .withContext('should not add anything to form without a name') - .toHaveSize(0); - } + .withContext('should not add anything to form without a name') + .toHaveSize(0); + }, }, { name: 'unselected', @@ -79,7 +80,7 @@ describe('', () => { `, assertValue(formData) { expect(formData.get('select')).toBe(''); - } + }, }, { name: 'selected', @@ -91,7 +92,7 @@ describe('', () => { `, assertValue(formData) { expect(formData.get('select')).toBe('two'); - } + }, }, { name: 'disabled', @@ -103,10 +104,10 @@ describe('', () => { `, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when disabled') - .toHaveSize(0); - } - } + .withContext('should not add anything to form when disabled') + .toHaveSize(0); + }, + }, ], resetTests: [ { @@ -122,9 +123,9 @@ describe('', () => { }, assertReset(select) { expect(select.value) - .withContext('select.value after reset') - .toBe(''); - } + .withContext('select.value after reset') + .toBe(''); + }, }, { name: 'reset to selected', @@ -139,9 +140,9 @@ describe('', () => { }, assertReset(select) { expect(select.value) - .withContext('select.value after reset') - .toBe('two'); - } + .withContext('select.value after reset') + .toBe('two'); + }, }, ], restoreTests: [ @@ -155,9 +156,9 @@ describe('', () => { `, assertRestored(select) { expect(select.value) - .withContext('select.value after restore') - .toBe(''); - } + .withContext('select.value after restore') + .toBe(''); + }, }, { name: 'restore selected', @@ -169,11 +170,11 @@ describe('', () => { `, assertRestored(select) { expect(select.value) - .withContext('select.value after restore') - .toBe('two'); - } + .withContext('select.value after restore') + .toBe('two'); + }, }, - ] + ], }); }); }); diff --git a/select/test/assets.ts b/select/test/assets.ts index dd3cf824e..6de3726ee 100644 --- a/select/test/assets.ts +++ b/select/test/assets.ts @@ -4,13 +4,13 @@ * User avatar as a dataurl. */ export const AVATAR_URL = - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAAAAADH8yjkAAABsklEQVR4Ae3WBaLjMAxF0dn/np4hVEY3cQqOupNh/i4oVT76buAUEkmfzgOXgAS8QiABCUhAAg71dlsfhgJOc4vv2flpAICWGr/TS5IGQoV/qoIs0OX4r7wTBcZ40lgSqBGplgMoQ6SMxACPaF4MmCPaXAwoEa0UAyyiWTFAIZoSAwyiGTGgQLRCDJgh2kwMaBCtEQPIIpIluVnkEMkJDjuq8KSKJMd1eDLusiC70U45/ik/Se/kbqbwOzXrBrgqDtOfhJoeBrqLyLvl0nlKp2MCXi1Ap1Prndv59iQOdPVqkqu/J9FoWQcp4LguES1f7B8HaJPhStk6PASElcGN9CL0B9YKd6TWPYFjgTsrjn2ARuPudMMHdgqM1I4LtAqsVMsDyIKZJRawBLslC7BgZzlAQI8CA+gM2JmOAZy9AjPleY/pAswWZx5AFVhVxATOVIJRSfxZRAXurqA+07Qb4c5GXb99QAvc1YJ6bzSncTPtHtnJYYwbjcODV0WT40p5I3C21AUQr6iFDi8/M5HZM/OSp2O7HP+FmPGyHeD4Db5x261rfEjnewISkIAEJODDAV8A/z6x+ahJu3sAAAAASUVORK5CYII='; + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAAAAADH8yjkAAABsklEQVR4Ae3WBaLjMAxF0dn/np4hVEY3cQqOupNh/i4oVT76buAUEkmfzgOXgAS8QiABCUhAAg71dlsfhgJOc4vv2flpAICWGr/TS5IGQoV/qoIs0OX4r7wTBcZ40lgSqBGplgMoQ6SMxACPaF4MmCPaXAwoEa0UAyyiWTFAIZoSAwyiGTGgQLRCDJgh2kwMaBCtEQPIIpIluVnkEMkJDjuq8KSKJMd1eDLusiC70U45/ik/Se/kbqbwOzXrBrgqDtOfhJoeBrqLyLvl0nlKp2MCXi1Ap1Prndv59iQOdPVqkqu/J9FoWQcp4LguES1f7B8HaJPhStk6PASElcGN9CL0B9YKd6TWPYFjgTsrjn2ARuPudMMHdgqM1I4LtAqsVMsDyIKZJRawBLslC7BgZzlAQI8CA+gM2JmOAZy9AjPleY/pAswWZx5AFVhVxATOVIJRSfxZRAXurqA+07Qb4c5GXb99QAvc1YJ6bzSncTPtHtnJYYwbjcODV0WT40p5I3C21AUQr6iFDi8/M5HZM/OSp2O7HP+FmPGyHeD4Db5x261rfEjnewISkIAEJODDAV8A/z6x+ahJu3sAAAAASUVORK5CYII='; /** * Example image as a dataurl. */ export const IMAGE_URL = - 'data:image/png;base64,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'; + 'data:image/png;base64,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'; /** * One frame of the color blue in webm as a dataurl. @@ -20,4 +20,4 @@ export const IMAGE_URL = * cat ~/out.webm | base64 | tr -d '\n' */ export const VIDEO_URL = - 'data:video/webm;base64,GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAJrEU2bdLpNu4tTq4QVSalmU6yBoU27i1OrhBZUrmtTrIHYTbuMU6uEElTDZ1OsggEgTbuMU6uEHFO7a1OsggJV7AEAAAAAAABZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmsirXsYMPQkBNgI1MYXZmNTkuMjcuMTAwV0GNTGF2ZjU5LjI3LjEwMESJiEBEAAAAAAAAFlSua8OuAQAAAAAAADrXgQFzxYjFaDr5zFhASZyBACK1nIN1bmSIgQCGhVZfVlA5g4EBI+ODhAJiWgDgi7CCBQC6ggLQmoECElTDZ0CBc3OgY8CAZ8iaRaOHRU5DT0RFUkSHjUxhdmY1OS4yNy4xMDBzc9tjwItjxYjFaDr5zFhASWfIpUWjh0VOQ09ERVJEh5hMYXZjNTkuMzcuMTAwIGxpYnZweC12cDlnyKJFo4hEVVJBVElPTkSHlDAwOjAwOjAwLjA0MDAwMDAwMAAAH0O2dUCo54EAo0CigQAAgIJJg0IAT/As9gA4JBwYShgAQGIMw/o6+kdo6+kAuaP9KgAAAAAcZw5Vl/m2cRY6ymCqlMFVJYKqSwVSleqUBBBCAAAAABxnDlWX+bZxFjrKYKqUwVUlgqpLBVKV6pQEEEIAAAAAHGcOVZf5tnEWOspgqpTBVSWCqksFUpXqlAQQQgBnDlWX+bZxFjrKYKqUwVUlgqpLBVKV6pQEEEIAHFO7a5G7j7OBALeK94EB8YIBp/CBAw=='; \ No newline at end of file + 'data:video/webm;base64,GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQJChYECGFOAZwEAAAAAAAJrEU2bdLpNu4tTq4QVSalmU6yBoU27i1OrhBZUrmtTrIHYTbuMU6uEElTDZ1OsggEgTbuMU6uEHFO7a1OsggJV7AEAAAAAAABZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmsirXsYMPQkBNgI1MYXZmNTkuMjcuMTAwV0GNTGF2ZjU5LjI3LjEwMESJiEBEAAAAAAAAFlSua8OuAQAAAAAAADrXgQFzxYjFaDr5zFhASZyBACK1nIN1bmSIgQCGhVZfVlA5g4EBI+ODhAJiWgDgi7CCBQC6ggLQmoECElTDZ0CBc3OgY8CAZ8iaRaOHRU5DT0RFUkSHjUxhdmY1OS4yNy4xMDBzc9tjwItjxYjFaDr5zFhASWfIpUWjh0VOQ09ERVJEh5hMYXZjNTkuMzcuMTAwIGxpYnZweC12cDlnyKJFo4hEVVJBVElPTkSHlDAwOjAwOjAwLjA0MDAwMDAwMAAAH0O2dUCo54EAo0CigQAAgIJJg0IAT/As9gA4JBwYShgAQGIMw/o6+kdo6+kAuaP9KgAAAAAcZw5Vl/m2cRY6ymCqlMFVJYKqSwVSleqUBBBCAAAAABxnDlWX+bZxFjrKYKqUwVUlgqpLBVKV6pQEEEIAAAAAHGcOVZf5tnEWOspgqpTBVSWCqksFUpXqlAQQQgBnDlWX+bZxFjrKYKqUwVUlgqpLBVKV6pQEEEIAHFO7a5G7j7OBALeK94EB8YIBp/CBAw=='; diff --git a/slider/demo/demo.ts b/slider/demo/demo.ts index 501285804..42cbdb3ff 100644 --- a/slider/demo/demo.ts +++ b/slider/demo/demo.ts @@ -4,18 +4,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Slider', [ - new Knob('disabled', {ui: boolInput(), defaultValue: false}), - ]); +const collection = new MaterialCollection>( + 'Slider', + [new Knob('disabled', {ui: boolInput(), defaultValue: false})], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/slider/demo/stories.ts b/slider/demo/stories.ts index be7bfa17c..5e24b53ef 100644 --- a/slider/demo/stories.ts +++ b/slider/demo/stories.ts @@ -6,7 +6,10 @@ import '@material/web/slider/slider.js'; -import {labelStyles, MaterialStoryInit} from './material-collection.js'; +import { + labelStyles, + MaterialStoryInit, +} from './material-collection.js'; import {MdSlider} from '@material/web/slider/slider.js'; import {css, html} from 'lit'; @@ -31,26 +34,30 @@ const single: MaterialStoryInit = { return html` `; - } + }, }; const range: MaterialStoryInit = { @@ -60,27 +67,35 @@ const range: MaterialStoryInit = { return html` `; - } + }, }; const customStyling: MaterialStoryInit = { @@ -155,11 +170,10 @@ const customStyling: MaterialStoryInit = { step="1" .disabled=${disabled ?? false} @pointerdown=${updateLabel} - @input=${updateLabel} - > + @input=${updateLabel}> `; - } + }, }; /** slider stories. */ diff --git a/slider/harness.ts b/slider/harness.ts index 59215c339..9ef5dbba7 100644 --- a/slider/harness.ts +++ b/slider/harness.ts @@ -14,19 +14,21 @@ import {Slider} from './internal/slider.js'; export class SliderHarness extends Harness { override async getInteractiveElement() { await this.element.updateComplete; - return this.element.renderRoot.querySelector('input.end')!; + return this.element.renderRoot.querySelector( + 'input.end', + )!; } getInputs() { return [ this.element.renderRoot.querySelector('input.end')!, - this.element.renderRoot.querySelector('input.start')! + this.element.renderRoot.querySelector('input.start')!, ]; } getHandles() { return [ this.element.renderRoot.querySelector('.handle.end')!, - this.element.renderRoot.querySelector('.handle.start')! + this.element.renderRoot.querySelector('.handle.start')!, ]; } @@ -36,7 +38,7 @@ export class SliderHarness extends Harness { isLabelShowing() { const labels = this.getLabels(); - return labels.some(l => { + return labels.some((l) => { // remove transition to avoid the need to wait for it. (l as HTMLElement).style.setProperty('transition', 'none'); const {width} = l.getBoundingClientRect(); @@ -47,7 +49,7 @@ export class SliderHarness extends Harness { async simulateValueInteraction(value: number, el?: HTMLInputElement) { if (!el) { - el = (this.getInputs())[0]; + el = this.getInputs()[0]; } el.focus(); el.dispatchEvent(new Event('pointerdown', {bubbles: true, composed: true})); @@ -65,16 +67,20 @@ export class SliderHarness extends Harness { } protected override simulateStartHover( - element: HTMLElement, init: PointerEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + ) { const i = this.getInputs().indexOf(element as HTMLInputElement); - if ((i >= 0) || (element === this.element)) { + if (i >= 0 || element === this.element) { init = this.positionEventAtHandle(init, i === 1); } super.simulateStartHover(element, init); } protected override simulateMousePress( - element: HTMLElement, init: PointerEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + ) { super.simulateMousePress(element, init); // advance beyond RAF, which is used by the element's pointerDown handler. jasmine.clock().tick(1); diff --git a/slider/internal/slider.ts b/slider/internal/slider.ts index d2aa43cdf..103856f50 100644 --- a/slider/internal/slider.ts +++ b/slider/internal/slider.ts @@ -16,7 +16,11 @@ import {when} from 'lit/directives/when.js'; import {ARIAMixinStrict} from '../../internal/aria/aria.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; -import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../internal/controller/events.js'; +import { + dispatchActivationClick, + isActivationClick, + redispatchEvent, +} from '../../internal/controller/events.js'; import {MdRipple} from '../../ripple/ripple.js'; // Disable warning for classMap with destructuring @@ -31,8 +35,10 @@ export class Slider extends LitElement { } /** @nocollapse */ - static override shadowRootOptions: - ShadowRootInit = {...LitElement.shadowRootOptions, delegatesFocus: true}; + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; /** @nocollapse */ static readonly formAssociated = true; @@ -179,16 +185,15 @@ export class Slider extends LitElement { return this.internals.labels; } - @query('input.start') private readonly inputStart!: HTMLInputElement|null; - @query('.handle.start') private readonly handleStart!: HTMLDivElement|null; + @query('input.start') private readonly inputStart!: HTMLInputElement | null; + @query('.handle.start') private readonly handleStart!: HTMLDivElement | null; @queryAsync('md-ripple.start') - private readonly rippleStart!: Promise; + private readonly rippleStart!: Promise; - @query('input.end') private readonly inputEnd!: HTMLInputElement|null; - @query('.handle.end') private readonly handleEnd!: HTMLDivElement|null; + @query('input.end') private readonly inputEnd!: HTMLInputElement | null; + @query('.handle.end') private readonly handleEnd!: HTMLDivElement | null; @queryAsync('md-ripple.end') - private readonly rippleEnd!: Promise; - + private readonly rippleEnd!: Promise; // handle hover/pressed states are set manually since the handle // does not receive pointer events so that the native inputs are @@ -207,13 +212,18 @@ export class Slider extends LitElement { private get renderAriaLabelStart() { // Needed for closure conformance const {ariaLabel} = this as ARIAMixinStrict; - return this.ariaLabelStart || ariaLabel && `${ariaLabel} start` || - this.valueLabelStart || String(this.valueStart); + return ( + this.ariaLabelStart || + (ariaLabel && `${ariaLabel} start`) || + this.valueLabelStart || + String(this.valueStart) + ); } private get renderAriaValueTextStart() { - return this.ariaValueTextStart || this.valueLabelStart || - String(this.valueStart); + return ( + this.ariaValueTextStart || this.valueLabelStart || String(this.valueStart) + ); } // Note: end aria-* properties are applied for single and range sliders, which @@ -223,8 +233,12 @@ export class Slider extends LitElement { // Needed for closure conformance const {ariaLabel} = this as ARIAMixinStrict; if (this.range) { - return this.ariaLabelEnd || ariaLabel && `${ariaLabel} end` || - this.valueLabelEnd || String(this.valueEnd); + return ( + this.ariaLabelEnd || + (ariaLabel && `${ariaLabel} end`) || + this.valueLabelEnd || + String(this.valueEnd) + ); } return ariaLabel || this.valueLabel || String(this.value); @@ -232,8 +246,9 @@ export class Slider extends LitElement { private get renderAriaValueTextEnd() { if (this.range) { - return this.ariaValueTextEnd || this.valueLabelEnd || - String(this.valueEnd); + return ( + this.ariaValueTextEnd || this.valueLabelEnd || String(this.valueEnd) + ); } // Needed for conformance @@ -249,8 +264,8 @@ export class Slider extends LitElement { private action?: Action; - private readonly internals = - (this as HTMLElement /* needed for closure */).attachInternals(); + private readonly internals = (this as HTMLElement) /* needed for closure */ + .attachInternals(); constructor() { super(); @@ -270,14 +285,16 @@ export class Slider extends LitElement { } protected override willUpdate(changed: PropertyValues) { - this.renderValueStart = changed.has('valueStart') ? - this.valueStart : - this.inputStart?.valueAsNumber; + this.renderValueStart = changed.has('valueStart') + ? this.valueStart + : this.inputStart?.valueAsNumber; const endValueChanged = - (changed.has('valueEnd') && this.range) || changed.has('value'); - this.renderValueEnd = endValueChanged ? - (this.range ? this.valueEnd : this.value) : - this.inputEnd?.valueAsNumber; + (changed.has('valueEnd') && this.range) || changed.has('value'); + this.renderValueEnd = endValueChanged + ? this.range + ? this.valueEnd + : this.value + : this.inputEnd?.valueAsNumber; // manually handle ripple hover state since the handle is pointer events // none. if (changed.get('handleStartHover') !== undefined) { @@ -288,8 +305,12 @@ export class Slider extends LitElement { } protected override update(changed: PropertyValues) { - if (changed.has('value') || changed.has('range') || - changed.has('valueStart') || changed.has('valueEnd')) { + if ( + changed.has('value') || + changed.has('range') || + changed.has('valueStart') || + changed.has('valueEnd') + ) { if (this.range) { const data = new FormData(); data.append(this.nameStart, String(this.valueStart)); @@ -332,8 +353,12 @@ export class Slider extends LitElement { } else { this.value ??= this.renderValueEnd; } - if (changed.has('range') || changed.has('renderValueStart') || - changed.has('renderValueEnd') || this.isUpdatePending) { + if ( + changed.has('range') || + changed.has('renderValueStart') || + changed.has('renderValueEnd') || + this.isUpdatePending + ) { // Only check if the handle nubs are overlapping, as the ripple touch // target extends subtantially beyond the boundary of the handle nub. const startNub = this.handleStart?.querySelector('.handleNub'); @@ -348,9 +373,9 @@ export class Slider extends LitElement { protected override render() { const step = this.step === 0 ? 1 : this.step; const range = Math.max(this.max - this.min, step); - const startFraction = this.range ? - (((this.renderValueStart ?? this.min) - this.min) / range) : - 0; + const startFraction = this.range + ? ((this.renderValueStart ?? this.min) - this.min) / range + : 0; const endFraction = ((this.renderValueEnd ?? this.min) - this.min) / range; const containerStyles = { // for clipping inputs and active track. @@ -363,8 +388,9 @@ export class Slider extends LitElement { // optional label values to show in place of the value. const labelStart = this.valueLabelStart || String(this.renderValueStart); - const labelEnd = (this.range ? this.valueLabelEnd : this.valueLabel) || - String(this.renderValueEnd); + const labelEnd = + (this.range ? this.valueLabelEnd : this.valueLabel) || + String(this.renderValueEnd); const inputStartProps = { start: true, @@ -387,86 +413,102 @@ export class Slider extends LitElement { const handleStartProps = { start: true, hover: this.handleStartHover, - label: labelStart + label: labelStart, }; const handleEndProps = { start: false, hover: this.handleEndHover, - label: labelEnd + label: labelEnd, }; const handleContainerClasses = { - hover: this.handleStartHover || this.handleEndHover + hover: this.handleStartHover || this.handleEndHover, }; - return html` -
- ${when(this.range, () => this.renderInput(inputStartProps))} - ${this.renderInput(inputEndProps)} - ${this.renderTrack()} -
-
-
- ${when(this.range, () => this.renderHandle(handleStartProps))} - ${this.renderHandle(handleEndProps)} -
+ return html`
+ ${when(this.range, () => this.renderInput(inputStartProps))} + ${this.renderInput(inputEndProps)} ${this.renderTrack()} +
+
+
+ ${when(this.range, () => this.renderHandle(handleStartProps))} + ${this.renderHandle(handleEndProps)}
-
`; +
+
`; } private renderTrack() { return html` -
- ${this.ticks ? html`
` : nothing} - `; +
+ ${this.ticks ? html`
` : nothing} + `; } private renderLabel(value: string) { return html``; - } - - private renderHandle({start, hover, label}: - {start: boolean, hover: boolean, label: string}) { - const onTop = !this.disabled && start === this.startOnTop; - const isOverlapping = !this.disabled && this.handlesOverlapping; - const name = start ? 'start' : 'end'; - return html`
-
- ${when(this.labeled, () => this.renderLabel(label))} - - + ${value}
`; } - private renderInput( - {start, value, ariaLabel, ariaValueText, ariaMin, ariaMax}: { - start: boolean; - value?: number; ariaLabel: string; ariaValueText: string; - ariaMin: number; - ariaMax: number; - }) { + private renderHandle({ + start, + hover, + label, + }: { + start: boolean; + hover: boolean; + label: string; + }) { + const onTop = !this.disabled && start === this.startOnTop; + const isOverlapping = !this.disabled && this.handlesOverlapping; + const name = start ? 'start' : 'end'; + return html`
+
+ ${when(this.labeled, () => this.renderLabel(label))} + + +
`; + } + + private renderInput({ + start, + value, + ariaLabel, + ariaValueText, + ariaMin, + ariaMax, + }: { + start: boolean; + value?: number; + ariaLabel: string; + ariaValueText: string; + ariaMin: number; + ariaMax: number; + }) { // Slider requires min/max set to the overall min/max for both inputs. // This is reported to screen readers, which is why we need aria-valuemin // and aria-valuemax. const name = start ? `start` : `end`; - return html``; + aria-valuetext=${ariaValueText} />`; } private async toggleRippleHover( - ripple: Promise, hovering: boolean) { + ripple: Promise, + hovering: boolean, + ) { const rippleEl = await ripple; if (!rippleEl) { return; } // TODO(b/269799771): improve slider ripple connection if (hovering) { - rippleEl.handlePointerenter(new PointerEvent( - 'pointerenter', {isPrimary: true, pointerId: this.ripplePointerId})); + rippleEl.handlePointerenter( + new PointerEvent('pointerenter', { + isPrimary: true, + pointerId: this.ripplePointerId, + }), + ); } else { - rippleEl.handlePointerleave(new PointerEvent( - 'pointerleave', {isPrimary: true, pointerId: this.ripplePointerId})); + rippleEl.handlePointerleave( + new PointerEvent('pointerleave', { + isPrimary: true, + pointerId: this.ripplePointerId, + }), + ); } } @@ -513,14 +565,16 @@ export class Slider extends LitElement { private startAction(event: Event) { const target = event.target as HTMLInputElement; const fixed = - (target === this.inputStart) ? this.inputEnd! : this.inputStart!; + target === this.inputStart ? this.inputEnd! : this.inputStart!; this.action = { canFlip: event.type === 'pointerdown', flipped: false, target, fixed, - values: new Map( - [[target, target.valueAsNumber], [fixed, fixed?.valueAsNumber]]) + values: new Map([ + [target, target.valueAsNumber], + [fixed, fixed?.valueAsNumber], + ]), }; } @@ -539,11 +593,11 @@ export class Slider extends LitElement { private handleDown(event: PointerEvent) { this.startAction(event); this.ripplePointerId = event.pointerId; - const isStart = event.target as HTMLInputElement === this.inputStart; + const isStart = (event.target as HTMLInputElement) === this.inputStart; // Since handle moves to pointer on down and there may not be a move, // it needs to be considered hovered.. this.handleStartHover = - !this.disabled && isStart && Boolean(this.handleStart); + !this.disabled && isStart && Boolean(this.handleStart); this.handleEndHover = !this.disabled && !isStart && Boolean(this.handleEnd); } @@ -607,8 +661,9 @@ export class Slider extends LitElement { const {target, fixed} = this.action; const isStart = target === this.inputStart; - return isStart ? target.valueAsNumber > fixed.valueAsNumber : - target.valueAsNumber < fixed.valueAsNumber; + return isStart + ? target.valueAsNumber > fixed.valueAsNumber + : target.valueAsNumber < fixed.valueAsNumber; } // if start/end start coincident and the first drag input would e.g. move @@ -701,7 +756,7 @@ export class Slider extends LitElement { const changeTarget = event.target as HTMLInputElement; const {target, values} = this.action ?? {}; const squelch = - (target && (target.valueAsNumber === values!.get(changeTarget)!)); + target && target.valueAsNumber === values!.get(changeTarget)!; if (!squelch) { redispatchEvent(this, event); } @@ -723,7 +778,7 @@ export class Slider extends LitElement { } /** @private */ - formStateRestoreCallback(state: string|Array<[string, string]>|null) { + formStateRestoreCallback(state: string | Array<[string, string]> | null) { if (Array.isArray(state)) { const [[, valueStart], [, valueEnd]] = state; this.valueStart = Number(valueStart); @@ -737,7 +792,7 @@ export class Slider extends LitElement { } } -function inBounds({x, y}: PointerEvent, element?: HTMLElement|null) { +function inBounds({x, y}: PointerEvent, element?: HTMLElement | null) { if (!element) { return false; } @@ -746,15 +801,20 @@ function inBounds({x, y}: PointerEvent, element?: HTMLElement|null) { } function isOverlapping( - elA: Element|null|undefined, elB: Element|null|undefined) { + elA: Element | null | undefined, + elB: Element | null | undefined, +) { if (!(elA && elB)) { return false; } const a = elA.getBoundingClientRect(); const b = elB.getBoundingClientRect(); return !( - a.top > b.bottom || a.right < b.left || a.bottom < b.top || - a.left > b.right); + a.top > b.bottom || + a.right < b.left || + a.bottom < b.top || + a.left > b.right + ); } interface Action { @@ -762,5 +822,5 @@ interface Action { flipped: boolean; target: HTMLInputElement; fixed: HTMLInputElement; - values: Map; + values: Map; } diff --git a/slider/slider_test.ts b/slider/slider_test.ts index d95b8ef38..ba18d9a91 100644 --- a/slider/slider_test.ts +++ b/slider/slider_test.ts @@ -24,23 +24,23 @@ interface SliderTestProps { } function getSliderTemplate(props?: SliderTestProps) { - return html` - `; + return html` `; } describe('', () => { const env = new Environment(); async function setupTest( - props?: SliderTestProps, template = getSliderTemplate) { + props?: SliderTestProps, + template = getSliderTemplate, + ) { const root = env.render(template(props)); await env.waitForStability(); const slider = root.querySelector('md-slider')!; @@ -231,24 +231,23 @@ describe('', () => { expect(harness.element.valueEnd).toEqual(4); }); - it('when starting coincident, can move start > end and end < start', - async () => { - const props = {range: true, valueStart: 2, valueEnd: 6}; - const {harness} = await setupTest(props); - await harness.element.updateComplete; - const [endInput, startInput] = harness.getInputs(); - await harness.simulateValueInteraction(6, startInput); - expect(harness.element.valueStart).toEqual(6); - await harness.simulateValueInteraction(8, startInput); - expect(harness.element.valueStart).toEqual(6); - expect(harness.element.valueEnd).toEqual(8); - await harness.simulateValueInteraction(8, startInput); - expect(harness.element.valueStart).toEqual(8); - expect(harness.element.valueEnd).toEqual(8); - await harness.simulateValueInteraction(4, endInput); - expect(harness.element.valueStart).toEqual(4); - expect(harness.element.valueEnd).toEqual(8); - }); + it('when starting coincident, can move start > end and end < start', async () => { + const props = {range: true, valueStart: 2, valueEnd: 6}; + const {harness} = await setupTest(props); + await harness.element.updateComplete; + const [endInput, startInput] = harness.getInputs(); + await harness.simulateValueInteraction(6, startInput); + expect(harness.element.valueStart).toEqual(6); + await harness.simulateValueInteraction(8, startInput); + expect(harness.element.valueStart).toEqual(6); + expect(harness.element.valueEnd).toEqual(8); + await harness.simulateValueInteraction(8, startInput); + expect(harness.element.valueStart).toEqual(8); + expect(harness.element.valueEnd).toEqual(8); + await harness.simulateValueInteraction(4, endInput); + expect(harness.element.valueStart).toEqual(4); + expect(harness.element.valueEnd).toEqual(8); + }); }); describe('dispatches input and change events', () => { @@ -362,81 +361,94 @@ describe('', () => { describe('forms', () => { createFormTests({ - queryControl: root => root.querySelector('md-slider'), + queryControl: (root) => root.querySelector('md-slider'), valueTests: [ { name: 'unnamed', render: () => html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form without a name') - .toHaveSize(0); - } + .withContext('should not add anything to form without a name') + .toHaveSize(0); + }, }, { name: 'single value', render: () => html``, assertValue(formData) { expect(formData.get('slider')).toBe('10'); - } + }, }, { name: 'multiple values same name', render: () => - html``, + html``, assertValue(formData) { expect(formData.getAll('slider')).toEqual(['0', '10']); - } + }, }, { name: 'multiple values different names', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('slider-start')).toBe('0'); expect(formData.get('slider-end')).toBe('10'); - } + }, }, { name: 'single default value', render: () => html``, assertValue(formData) { expect(formData.get('slider')).toBe('50'); - } + }, }, { name: 'single default value with min/max', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('slider')).toBe('200'); - } + }, }, { name: 'multiple default values', render: () => html``, assertValue(formData) { expect(formData.getAll('slider')).toEqual(['33', '67']); - } + }, }, { name: 'multiple default values with min/max', render: () => - html``, + html``, assertValue(formData) { expect(formData.getAll('slider')).toEqual(['167', '233']); - } + }, }, { name: 'disabled', render: () => - html``, + html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when disabled') - .toHaveSize(0); - } - } + .withContext('should not add anything to form when disabled') + .toHaveSize(0); + }, + }, ], resetTests: [ { @@ -447,43 +459,52 @@ describe('', () => { }, assertReset(slider) { expect(slider.value) - .withContext('slider.value after reset') - .toBe(10); - } + .withContext('slider.value after reset') + .toBe(10); + }, }, { name: 'reset multiple values same name', render: () => - html``, + html``, change(slider) { slider.valueStart = 5; slider.valueEnd = 5; }, assertReset(slider) { expect(slider.valueStart) - .withContext('slider.valueStart after reset') - .toEqual(0); + .withContext('slider.valueStart after reset') + .toEqual(0); expect(slider.valueEnd) - .withContext('slider.valueEnd after reset') - .toEqual(10); - } + .withContext('slider.valueEnd after reset') + .toEqual(10); + }, }, { name: 'reset multiple values different names', render: () => - html``, + html``, change(slider) { slider.valueStart = 5; slider.valueEnd = 5; }, assertReset(slider) { expect(slider.valueStart) - .withContext('slider.valueStart after reset') - .toEqual(0); + .withContext('slider.valueStart after reset') + .toEqual(0); expect(slider.valueEnd) - .withContext('slider.valueEnd after reset') - .toEqual(10); - } + .withContext('slider.valueEnd after reset') + .toEqual(10); + }, }, ], restoreTests: [ @@ -492,37 +513,46 @@ describe('', () => { render: () => html``, assertRestored(slider) { expect(slider.value) - .withContext('slider.value after restore') - .toBe(1); - } + .withContext('slider.value after restore') + .toBe(1); + }, }, { name: 'restore multiple values same name', render: () => - html``, + html``, assertRestored(slider) { expect(slider.valueStart) - .withContext('slider.valueStart after restore') - .toEqual(0); + .withContext('slider.valueStart after restore') + .toEqual(0); expect(slider.valueEnd) - .withContext('slider.valueEnd after restore') - .toEqual(10); - } + .withContext('slider.valueEnd after restore') + .toEqual(10); + }, }, { name: 'restore multiple values different names', render: () => - html``, + html``, assertRestored(slider) { expect(slider.valueStart) - .withContext('slider.valueStart after restore') - .toEqual(0); + .withContext('slider.valueStart after restore') + .toEqual(0); expect(slider.valueEnd) - .withContext('slider.valueEnd after restore') - .toEqual(10); - } + .withContext('slider.valueEnd after restore') + .toEqual(10); + }, }, - ] + ], }); }); }); diff --git a/switch/demo/demo.ts b/switch/demo/demo.ts index af2c2a170..eaba47254 100644 --- a/switch/demo/demo.ts +++ b/switch/demo/demo.ts @@ -4,21 +4,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Switch', [ - new Knob('disabled', {defaultValue: false, ui: boolInput()}), - new Knob('selected', {defaultValue: false, ui: boolInput()}), - new Knob('icons', {defaultValue: false, ui: boolInput()}), - new Knob('showOnlySelectedIcon', {defaultValue: false, ui: boolInput()}), - ]); +const collection = new MaterialCollection>( + 'Switch', + [ + new Knob('disabled', {defaultValue: false, ui: boolInput()}), + new Knob('selected', {defaultValue: false, ui: boolInput()}), + new Knob('icons', {defaultValue: false, ui: boolInput()}), + new Knob('showOnlySelectedIcon', {defaultValue: false, ui: boolInput()}), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/switch/demo/stories.ts b/switch/demo/stories.ts index cd2a363dc..57922bf8d 100644 --- a/switch/demo/stories.ts +++ b/switch/demo/stories.ts @@ -6,7 +6,10 @@ import '@material/web/switch/switch.js'; -import {labelStyles, MaterialStoryInit} from './material-collection.js'; +import { + labelStyles, + MaterialStoryInit, +} from './material-collection.js'; import {css, html} from 'lit'; /** Knob types for Switch stories. */ @@ -20,21 +23,20 @@ export interface StoryKnobs { const standard: MaterialStoryInit = { name: 'Switch', render(knobs) { - return html` - `; - } + return html` `; + }, }; const labeled: MaterialStoryInit = { name: 'With labels', styles: [ - labelStyles, css` + labelStyles, + css` .column { display: flex; flex-direction: column; @@ -45,7 +47,7 @@ const labeled: MaterialStoryInit = { label { justify-content: space-between; } - ` + `, ], render(knobs) { return html` @@ -56,8 +58,7 @@ const labeled: MaterialStoryInit = { aria-label="Wi-Fi" .disabled=${knobs.disabled} .icons=${knobs.icons} - .showOnlySelectedIcon=${knobs.showOnlySelectedIcon} - > + .showOnlySelectedIcon=${knobs.showOnlySelectedIcon}>
`; - } + }, }; /** Switch stories. */ diff --git a/switch/internal/switch.ts b/switch/internal/switch.ts index 5f18dbe95..93ec53631 100644 --- a/switch/internal/switch.ts +++ b/switch/internal/switch.ts @@ -7,12 +7,23 @@ import '../../focus/md-focus-ring.js'; import '../../ripple/ripple.js'; -import {html, isServer, LitElement, nothing, PropertyValues, TemplateResult} from 'lit'; +import { + html, + isServer, + LitElement, + nothing, + PropertyValues, + TemplateResult, +} from 'lit'; import {property, query} from 'lit/decorators.js'; import {ClassInfo, classMap} from 'lit/directives/class-map.js'; import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js'; -import {dispatchActivationClick, isActivationClick, redispatchEvent} from '../../internal/controller/events.js'; +import { + dispatchActivationClick, + isActivationClick, + redispatchEvent, +} from '../../internal/controller/events.js'; /** * @fires input {InputEvent} Fired whenever `selected` changes due to user @@ -26,8 +37,10 @@ export class Switch extends LitElement { } /** @nocollapse */ - static override shadowRootOptions: - ShadowRootInit = {mode: 'open', delegatesFocus: true}; + static override shadowRootOptions: ShadowRootInit = { + mode: 'open', + delegatesFocus: true, + }; /** @nocollapse */ static readonly formAssociated = true; @@ -128,12 +141,12 @@ export class Switch extends LitElement { return this.internals.willValidate; } - @query('input') private readonly input!: HTMLInputElement|null; + @query('input') private readonly input!: HTMLInputElement | null; // Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432 // Replace with this.internals.validity.customError when resolved. private hasCustomValidityError = false; - private readonly internals = - (this as HTMLElement /* needed for closure */).attachInternals(); + // Cast needed for closure + private readonly internals = (this as HTMLElement).attachInternals(); constructor() { super(); @@ -218,13 +231,10 @@ export class Switch extends LitElement { ?checked=${this.selected} ?disabled=${this.disabled} ?required=${this.required} - @change=${this.handleChange} - > + @change=${this.handleChange} /> - - ${this.renderHandle()} - + ${this.renderHandle()}
`; } @@ -273,7 +283,8 @@ export class Switch extends LitElement { private renderOnIcon() { return html` - + `; } @@ -284,7 +295,8 @@ export class Switch extends LitElement { private renderOffIcon() { return html` - + `; } @@ -314,7 +326,10 @@ export class Switch extends LitElement { } this.internals.setValidity( - input.validity, input.validationMessage, this.getInput()); + input.validity, + input.validationMessage, + this.getInput(), + ); } private getInput() { diff --git a/switch/internal/switch_test.ts b/switch/internal/switch_test.ts index 398af2fa2..b8a0cf690 100644 --- a/switch/internal/switch_test.ts +++ b/switch/internal/switch_test.ts @@ -14,8 +14,7 @@ import {Harness} from '../../testing/harness.js'; import {Switch} from './switch.js'; @customElement('md-test-switch') -class TestSwitch extends Switch { -} +class TestSwitch extends Switch {} declare global { interface HTMLElementTagNameMap { @@ -34,22 +33,20 @@ function renderSwitch(propsInit: Partial = {}) { } function renderSwitchInForm(propsInit: Partial = {}) { - return html` -
${renderSwitch(propsInit)}
- `; + return html`
${renderSwitch(propsInit)}
`; } function renderSwitchInLabel(propsInit: Partial = {}) { - return html` - - `; + return html` `; } describe('md-switch', () => { const env = new Environment(); async function switchElement( - propsInit: Partial = {}, template = renderSwitch) { + propsInit: Partial = {}, + template = renderSwitch, + ) { const root = env.render(html`
${template(propsInit)}
`); await env.waitForStability(); const element = root.querySelector('md-test-switch'); @@ -168,7 +165,9 @@ describe('md-switch', () => { describe('form submission', () => { async function switchInForm( - propsInit: Partial = {}, template = renderSwitchInForm) { + propsInit: Partial = {}, + template = renderSwitchInForm, + ) { const element = await switchElement(propsInit, template); return new Harness(element); } @@ -180,8 +179,11 @@ describe('md-switch', () => { }); it('does not submit if disabled', async () => { - const harness = - await switchInForm({name: 'foo', selected: true, disabled: true}); + const harness = await switchInForm({ + name: 'foo', + selected: true, + disabled: true, + }); const formData = await harness.submitForm(); expect(formData.get('foo')).toBeNull(); }); @@ -194,8 +196,11 @@ describe('md-switch', () => { }); it('submits under correct conditions', async () => { - const harness = - await switchInForm({name: 'foo', selected: true, value: 'bar'}); + const harness = await switchInForm({ + name: 'foo', + selected: true, + value: 'bar', + }); const formData = await harness.submitForm(); expect(formData.get('foo')).toEqual('bar'); }); @@ -221,8 +226,8 @@ describe('md-switch', () => { toggle.required = true; expect(toggle.validity.valueMissing) - .withContext('toggle.validity.valueMissing') - .toBeTrue(); + .withContext('toggle.validity.valueMissing') + .toBeTrue(); }); it('should not set valueMissing when required and selected', async () => { @@ -230,8 +235,8 @@ describe('md-switch', () => { toggle.selected = true; expect(toggle.validity.valueMissing) - .withContext('toggle.validity.valueMissing') - .toBeFalse(); + .withContext('toggle.validity.valueMissing') + .toBeFalse(); }); }); }); diff --git a/switch/switch_test.ts b/switch/switch_test.ts index 3c53474ee..0d4971c83 100644 --- a/switch/switch_test.ts +++ b/switch/switch_test.ts @@ -20,51 +20,54 @@ describe('', () => { describe('forms', () => { createFormTests({ - queryControl: root => root.querySelector('md-switch'), + queryControl: (root) => root.querySelector('md-switch'), valueTests: [ { name: 'unnamed', render: () => html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form without a name') - .toHaveSize(0); - } + .withContext('should not add anything to form without a name') + .toHaveSize(0); + }, }, { name: 'unselected', render: () => html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when unselected') - .toHaveSize(0); - } + .withContext('should not add anything to form when unselected') + .toHaveSize(0); + }, }, { name: 'selected default value', render: () => html``, assertValue(formData) { expect(formData.get('switch')).toBe('on'); - } + }, }, { name: 'selected custom value', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('switch')).toBe('Custom value'); - } + }, }, { name: 'disabled', render: () => - html``, + html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when disabled') - .toHaveSize(0); - } - } + .withContext('should not add anything to form when disabled') + .toHaveSize(0); + }, + }, ], resetTests: [ { @@ -75,9 +78,9 @@ describe('', () => { }, assertReset(control) { expect(control.selected) - .withContext('control.selected after reset') - .toBeFalse(); - } + .withContext('control.selected after reset') + .toBeFalse(); + }, }, { name: 'reset to selected', @@ -87,9 +90,9 @@ describe('', () => { }, assertReset(control) { expect(control.selected) - .withContext('control.selected after reset') - .toBeTrue(); - } + .withContext('control.selected after reset') + .toBeTrue(); + }, }, ], restoreTests: [ @@ -98,20 +101,20 @@ describe('', () => { render: () => html``, assertRestored(control) { expect(control.selected) - .withContext('control.selected after restore') - .toBeFalse(); - } + .withContext('control.selected after restore') + .toBeFalse(); + }, }, { name: 'restore selected', render: () => html``, assertRestored(control) { expect(control.selected) - .withContext('control.selected after restore') - .toBeTrue(); - } + .withContext('control.selected after restore') + .toBeTrue(); + }, }, - ] + ], }); }); }); diff --git a/tabs/demo/demo.ts b/tabs/demo/demo.ts index 0bfd313cc..4812767ce 100644 --- a/tabs/demo/demo.ts +++ b/tabs/demo/demo.ts @@ -4,45 +4,56 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {MdTabs} from '@material/web/tabs/tabs.js'; -import {boolInput, Knob, numberInput, radioSelector} from './index.js'; +import { + boolInput, + Knob, + numberInput, + radioSelector, +} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Tabs', [ - new Knob('activeTabIndex', { - ui: numberInput(), - defaultValue: 0, - // fire a change event manually to sync tabbed content - wiring: async (knob, value, container) => { - await new Promise(requestAnimationFrame); - const tabs = - Array.from(container.querySelectorAll('md-tabs')); - for (const tab of tabs) { - const event = new Event('change', {bubbles: true, composed: true}); - tab.dispatchEvent(event); - } +const collection = new MaterialCollection>( + 'Tabs', + [ + new Knob('activeTabIndex', { + ui: numberInput(), + defaultValue: 0, + // fire a change event manually to sync tabbed content + wiring: async (knob, value, container) => { + await new Promise(requestAnimationFrame); + const tabs = Array.from(container.querySelectorAll('md-tabs')); + for (const tab of tabs) { + const event = new Event('change', {bubbles: true, composed: true}); + tab.dispatchEvent(event); } + }, + }), + new Knob('autoActivate', {ui: boolInput(), defaultValue: false}), + new Knob('inlineIcon', {ui: boolInput(), defaultValue: false}), + new Knob('content', { + defaultValue: 'icon/label', + ui: radioSelector({ + name: 'contentRadioGroup', + options: [ + {label: 'icon/label', value: 'icon/label'}, + {label: 'icon', value: 'icon'}, + {label: 'label', value: 'label'}, + ], }), - new Knob('autoActivate', {ui: boolInput(), defaultValue: false}), - new Knob('inlineIcon', {ui: boolInput(), defaultValue: false}), - new Knob('content', { - defaultValue: 'icon/label', - ui: radioSelector({ - name: 'contentRadioGroup', - options: [ - {label: 'icon/label', value: 'icon/label'}, - {label: 'icon', value: 'icon'}, - {label: 'label', value: 'label'}, - ] - }) - }), - ]); + }), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/tabs/demo/stories.ts b/tabs/demo/stories.ts index 8ddd99bae..de40268e2 100644 --- a/tabs/demo/stories.ts +++ b/tabs/demo/stories.ts @@ -6,9 +6,9 @@ import '@material/web/icon/icon.js'; import '@material/web/iconbutton/icon-button.js'; -import '@material/web/tabs/tabs.js'; import '@material/web/tabs/primary-tab.js'; import '@material/web/tabs/secondary-tab.js'; +import '@material/web/tabs/tabs.js'; import {MaterialStoryInit} from './material-collection.js'; import {MdTabs} from '@material/web/tabs/tabs.js'; @@ -24,11 +24,11 @@ export interface StoryKnobs { } const styles = css` - [role=tabpanel]:not([hidden]) { + [role='tabpanel']:not([hidden]) { font-family: Roboto, Material Sans, system-ui; } - [role=tabpanel]:not(.subtabs) { + [role='tabpanel']:not(.subtabs) { padding: 16px; } @@ -54,40 +54,60 @@ const primary: MaterialStoryInit = { const inlineIcon = knobs.inlineIcon; return html` - - + + ${tabContent('piano', 'Keyboard')} - + ${tabContent('tune', 'Guitar')} - + ${tabContent('graphic_eq', 'Drums')} - + ${tabContent('speaker', 'Bass')} - + ${tabContent('nightlife', 'Saxophone')} -
Keyboard
- - - - +
Keyboard
+ + + + `; - } + }, }; const secondary: MaterialStoryInit = { @@ -97,11 +117,11 @@ const secondary: MaterialStoryInit = { const tabContent = getTabContentGenerator(knobs); return html` - + ${tabContent('flight', 'Travel')} @@ -117,11 +137,17 @@ const secondary: MaterialStoryInit = {
Travel
- - - + + + `; - } + }, }; const scrolling: MaterialStoryInit = { @@ -131,13 +157,12 @@ const scrolling: MaterialStoryInit = { const tabContent = getTabContentGenerator(knobs); const inlineIcon = knobs.inlineIcon; - return html` - - ${new Array(10).fill(html` + return html` + ${new Array(10).fill(html` ${tabContent('piano', 'Keyboard')} @@ -153,9 +178,9 @@ const scrolling: MaterialStoryInit = { ${tabContent('nightlife', 'Saxophone')} - `)} - `; - } + `)} + `; + }, }; const custom: MaterialStoryInit = { @@ -193,11 +218,11 @@ const custom: MaterialStoryInit = { const tabContent = getTabContentGenerator(knobs); return html` - + ${tabContent('flight', 'Travel')} @@ -213,11 +238,17 @@ const custom: MaterialStoryInit = {
Travel
- - - + + + `; - } + }, }; const primaryAndSecondary: MaterialStoryInit = { @@ -232,8 +263,7 @@ const primaryAndSecondary: MaterialStoryInit = { aria-label="Primary tabs" .activeTabIndex=${knobs.activeTabIndex} .autoActivate=${knobs.autoActivate} - ${setupTabPanels()} - > + ${setupTabPanels()}> ${tabContent('videocam', 'Movies')} @@ -246,59 +276,90 @@ const primaryAndSecondary: MaterialStoryInit = {
- - Star Wars + ${setupTabPanels()}> + Star Wars Avengers Jaws Frozen -
Star Wars
- +
Star Wars
+
- `; - } + }, }; const dynamic: MaterialStoryInit = { @@ -308,8 +369,9 @@ const dynamic: MaterialStoryInit = { const inlineIcon = knobs.inlineIcon; function getTabs(event: Event) { - return ((event.target! as Element).getRootNode() as ShadowRoot) - .querySelector('md-tabs')!; + return ( + (event.target! as Element).getRootNode() as ShadowRoot + ).querySelector('md-tabs')!; } function addTab(event: Event) { @@ -346,32 +408,27 @@ const dynamic: MaterialStoryInit = { } } - return html` -
+ return html`
add - remove - chevron_left - chevron_right + remove + chevron_left + chevron_right
- - Tab 1 - - - Tab 2 - - - Tab 3 - + class="scrolling" + .activeTabIndex=${knobs.activeTabIndex} + .autoActivate=${knobs.autoActivate}> + Tab 1 + Tab 2 + Tab 3 `; - } + }, }; function getTabContentGenerator(knobs: StoryKnobs) { @@ -381,20 +438,19 @@ function getTabContentGenerator(knobs: StoryKnobs) { return (icon: string, label: string) => { const iconTemplate = html`${icon}`; return html` - ${useIcon ? iconTemplate : nothing} - ${useLabel ? html`${label}` : nothing} + ${useIcon ? iconTemplate : nothing} ${useLabel ? html`${label}` : nothing} `; }; } function setupTabPanels() { - return ref(instance => { + return ref((instance) => { if (!instance) { return; } const tabs = instance as MdTabs; - let currentPanel: HTMLElement|null = null; + let currentPanel: HTMLElement | null = null; tabs.addEventListener('change', () => { if (currentPanel) { currentPanel.hidden = true; @@ -411,5 +467,11 @@ function setupTabPanels() { } /** Tabs stories. */ -export const stories = - [primary, secondary, scrolling, custom, primaryAndSecondary, dynamic]; +export const stories = [ + primary, + secondary, + scrolling, + custom, + primaryAndSecondary, + dynamic, +]; diff --git a/tabs/harness.ts b/tabs/harness.ts index f3cbcba31..825385191 100644 --- a/tabs/harness.ts +++ b/tabs/harness.ts @@ -48,14 +48,13 @@ export class TabsHarness extends Harness { } const selectedItemHarness = - (this.element.activeTab as ElementWithHarness).harness as - TabHarness ?? - new TabHarness(this.element.activeTab); + ((this.element.activeTab as ElementWithHarness) + .harness as TabHarness) ?? new TabHarness(this.element.activeTab); return await selectedItemHarness.getInteractiveElement(); } get harnessedItems() { - return (this.element.tabs as Array>).map(item => { + return (this.element.tabs as Array>).map((item) => { return (item.harness ?? new TabHarness(item)) as TabHarness; }); } diff --git a/tabs/internal/tab.ts b/tabs/internal/tab.ts index 9e0698512..5438f9af8 100644 --- a/tabs/internal/tab.ts +++ b/tabs/internal/tab.ts @@ -9,10 +9,19 @@ import '../../focus/md-focus-ring.js'; import '../../ripple/ripple.js'; import {html, isServer, LitElement, nothing} from 'lit'; -import {property, query, queryAssignedElements, queryAssignedNodes, state} from 'lit/decorators.js'; +import { + property, + query, + queryAssignedElements, + queryAssignedNodes, + state, +} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; -import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js'; +import { + polyfillElementInternalsAria, + setupHostAria, +} from '../../internal/aria/aria.js'; import {EASING} from '../../internal/motion/animation.js'; /** @@ -70,14 +79,17 @@ export class Tab extends LitElement { */ @property({type: Boolean, attribute: 'icon-only'}) iconOnly = false; - @query('.indicator') readonly[INDICATOR]!: HTMLElement|null; + @query('.indicator') readonly [INDICATOR]!: HTMLElement | null; @state() protected fullWidthIndicator = false; @queryAssignedNodes({flatten: true}) private readonly assignedDefaultNodes!: Node[]; @queryAssignedElements({slot: 'icon', flatten: true}) private readonly assignedIcons!: HTMLElement[]; private readonly internals = polyfillElementInternalsAria( - this, (this as HTMLElement /* needed for closure */).attachInternals()); + this, + // Cast needed for closure + (this as HTMLElement).attachInternals(), + ); constructor() { super(); @@ -89,20 +101,22 @@ export class Tab extends LitElement { protected override render() { const indicator = html`
`; - return html` - `; + return html` `; } protected getContentClasses() { @@ -142,13 +156,15 @@ export class Tab extends LitElement { return; } - this[INDICATOR].getAnimations().forEach(a => { + this[INDICATOR].getAnimations().forEach((a) => { a.cancel(); }); const frames = this.getKeyframes(previousTab); if (frames !== null) { - this[INDICATOR].animate( - frames, {duration: 250, easing: EASING.EMPHASIZED}); + this[INDICATOR].animate(frames, { + duration: 250, + easing: EASING.EMPHASIZED, + }); } } @@ -160,17 +176,22 @@ export class Tab extends LitElement { const from: Keyframe = {}; const fromRect = - previousTab[INDICATOR]?.getBoundingClientRect() ?? ({} as DOMRect); + previousTab[INDICATOR]?.getBoundingClientRect() ?? ({} as DOMRect); const fromPos = fromRect.left; const fromExtent = fromRect.width; const toRect = this[INDICATOR]!.getBoundingClientRect(); const toPos = toRect.left; const toExtent = toRect.width; const scale = fromExtent / toExtent; - if (!reduceMotion && fromPos !== undefined && toPos !== undefined && - !isNaN(scale)) { - from['transform'] = `translateX(${ - (fromPos - toPos).toFixed(4)}px) scaleX(${scale.toFixed(4)})`; + if ( + !reduceMotion && + fromPos !== undefined && + toPos !== undefined && + !isNaN(scale) + ) { + from['transform'] = `translateX(${(fromPos - toPos).toFixed( + 4, + )}px) scaleX(${scale.toFixed(4)})`; } else { from['opacity'] = 0; } @@ -184,8 +205,9 @@ export class Tab extends LitElement { // Check if there's any label text or elements. If not, then there is only // an icon. for (const node of this.assignedDefaultNodes) { - const hasTextContent = node.nodeType === Node.TEXT_NODE && - !!(node as Text).wholeText.match(/\S/); + const hasTextContent = + node.nodeType === Node.TEXT_NODE && + !!(node as Text).wholeText.match(/\S/); if (node.nodeType === Node.ELEMENT_NODE || hasTextContent) { return; } diff --git a/tabs/internal/tabs.ts b/tabs/internal/tabs.ts index a9426b83e..23d08a2a8 100644 --- a/tabs/internal/tabs.ts +++ b/tabs/internal/tabs.ts @@ -9,7 +9,10 @@ import '../../divider/divider.js'; import {html, isServer, LitElement} from 'lit'; import {property, query, queryAssignedElements} from 'lit/decorators.js'; -import {polyfillElementInternalsAria, setupHostAria} from '../../internal/aria/aria.js'; +import { + polyfillElementInternalsAria, + setupHostAria, +} from '../../internal/aria/aria.js'; import {ANIMATE_INDICATOR, Tab} from './tab.js'; @@ -54,9 +57,9 @@ export class Tabs extends LitElement { * @export */ get activeTab() { - return this.tabs.find(tab => tab.active) ?? null; + return this.tabs.find((tab) => tab.active) ?? null; } - set activeTab(tab: Tab|null) { + set activeTab(tab: Tab | null) { // Ignore setting activeTab to null. As long as there are children, one tab // must be selected. if (tab) { @@ -70,7 +73,7 @@ export class Tabs extends LitElement { * @export */ get activeTabIndex() { - return this.tabs.findIndex(tab => tab.active); + return this.tabs.findIndex((tab) => tab.active); } set activeTabIndex(index: number) { const activateTabAtIndex = () => { @@ -108,14 +111,17 @@ export class Tabs extends LitElement { */ @property({type: Boolean, attribute: 'auto-activate'}) autoActivate = false; - @query('slot') private readonly slotElement!: HTMLSlotElement|null; + @query('slot') private readonly slotElement!: HTMLSlotElement | null; private get focusedTab() { - return this.tabs.find(tab => tab.matches(':focus-within')); + return this.tabs.find((tab) => tab.matches(':focus-within')); } private readonly internals = polyfillElementInternalsAria( - this, (this as HTMLElement /* needed for closure */).attachInternals()); + this, + // Cast needed for closure + (this as HTMLElement).attachInternals(), + ); constructor() { super(); @@ -135,7 +141,7 @@ export class Tabs extends LitElement { * active tab. * @return A Promise that resolves after the tab has been scrolled to. */ - async scrollToTab(tabToScrollTo?: Tab|null) { + async scrollToTab(tabToScrollTo?: Tab | null) { await this.updateComplete; const {tabs} = this; tabToScrollTo ??= this.activeTab; @@ -157,15 +163,16 @@ export class Tabs extends LitElement { const max = offset + extent - hostExtent + scrollMargin; const to = Math.min(min, Math.max(max, scroll)); // TODO(b/299934312): improve focus smoothness - const behavior = !this.focusedTab ? 'smooth' : 'instant' as ScrollBehavior; + const behavior: ScrollBehavior = !this.focusedTab ? 'smooth' : 'instant'; this.scrollTo({behavior, top: 0, left: to}); } protected override render() { return html`
- +
`; @@ -198,7 +205,8 @@ export class Tabs extends LitElement { // Don't dispatch a change event if activating a tab when no previous tabs // were selected, such as when md-tabs auto-selects the first tab. const defaultPrevented = !this.dispatchEvent( - new Event('change', {bubbles: true, cancelable: true})); + new Event('change', {bubbles: true, cancelable: true}), + ); if (defaultPrevented) { for (const tab of tabs) { tab.active = tab === previousTab; @@ -228,7 +236,7 @@ export class Tabs extends LitElement { const isHome = event.key === 'Home'; const isEnd = event.key === 'End'; // Ignore non-navigation keys - if (event.defaultPrevented || !isLeft && !isRight && !isHome && !isEnd) { + if (event.defaultPrevented || (!isLeft && !isRight && !isHome && !isEnd)) { return; } @@ -309,4 +317,4 @@ export class Tabs extends LitElement { function isTab(element: unknown): element is Tab { return element instanceof HTMLElement && element.hasAttribute('md-tab'); -} \ No newline at end of file +} diff --git a/tabs/tabs_test.ts b/tabs/tabs_test.ts index 9f28f1652..0c5d81c10 100644 --- a/tabs/tabs_test.ts +++ b/tabs/tabs_test.ts @@ -19,14 +19,11 @@ interface TabsTestProps { } function getTabsTemplate(props?: TabsTestProps) { - return html` - - A - B - C - `; + return html` + A + B + C + `; } describe('', () => { @@ -121,20 +118,19 @@ describe('', () => { expect(harness.element.activeTab).toBe(firstTab); }); - it('should allow setting activeTabIndex in a lit property binding', - async () => { - const root = env.render(html` - - A - B - - `); + it('should allow setting activeTabIndex in a lit property binding', async () => { + const root = env.render(html` + + A + B + + `); - await env.waitForStability(); - const tabs = root.querySelector('md-tabs')!; - expect(tabs.activeTabIndex).withContext('activeTabIndex').toBe(1); - expect(tabs.activeTab?.textContent).withContext('activeTab').toBe('B'); - }); + await env.waitForStability(); + const tabs = root.querySelector('md-tabs')!; + expect(tabs.activeTabIndex).withContext('activeTabIndex').toBe(1); + expect(tabs.activeTab?.textContent).withContext('activeTab').toBe('B'); + }); }); it('should dispatch "change" when tab changes', async () => { @@ -146,25 +142,23 @@ describe('', () => { expect(changeListener).toHaveBeenCalledTimes(1); }); - it('should not dispatch "change" when changing to unrelated tab', - async () => { - const {harness} = await setupTest(); - const changeListener = jasmine.createSpy('changeListener'); - harness.element.addEventListener('change', changeListener); + it('should not dispatch "change" when changing to unrelated tab', async () => { + const {harness} = await setupTest(); + const changeListener = jasmine.createSpy('changeListener'); + harness.element.addEventListener('change', changeListener); - harness.element.activeTab = document.createElement('md-primary-tab'); - await env.waitForStability(); - expect(changeListener).not.toHaveBeenCalled(); - }); + harness.element.activeTab = document.createElement('md-primary-tab'); + await env.waitForStability(); + expect(changeListener).not.toHaveBeenCalled(); + }); - it('should not dispatch "change" when setting activeTab to itself', - async () => { - const {harness} = await setupTest(); - const changeListener = jasmine.createSpy('changeListener'); - harness.element.addEventListener('change', changeListener); + it('should not dispatch "change" when setting activeTab to itself', async () => { + const {harness} = await setupTest(); + const changeListener = jasmine.createSpy('changeListener'); + harness.element.addEventListener('change', changeListener); - harness.element.activeTab = harness.element.activeTab; - await env.waitForStability(); - expect(changeListener).not.toHaveBeenCalled(); - }); + harness.element.activeTab = harness.element.activeTab; + await env.waitForStability(); + expect(changeListener).not.toHaveBeenCalled(); + }); }); diff --git a/testing/environment.ts b/testing/environment.ts index 60d314719..7a606122b 100644 --- a/testing/environment.ts +++ b/testing/environment.ts @@ -6,7 +6,7 @@ // import 'jasmine'; (google3-only) -import {ReactiveElement, render as litRender, TemplateResult} from 'lit'; +import {ReactiveElement, TemplateResult, render as litRender} from 'lit'; import {installSkipWebAnimations} from './skip-animations.js'; @@ -132,7 +132,7 @@ export class Environment { * @return The current root container or undefined is nothing as been rendered * yet. */ - protected getCurrentRoot(): HTMLElement|undefined { + protected getCurrentRoot(): HTMLElement | undefined { return this.roots[this.roots.length - 1]; } } diff --git a/testing/forms.ts b/testing/forms.ts index 50bb1f52e..a26020d07 100644 --- a/testing/forms.ts +++ b/testing/forms.ts @@ -18,7 +18,7 @@ export interface FormTestsOptions { * @param root The root element to query from. * @return `root.querySelector('md-component')` */ - queryControl(root: Element): T|null; + queryControl(root: Element): T | null; /** * Tests for `setFormValue`. Tests should render a form element, then * assert that the form's `FormData` matches the expected value (or lack of @@ -52,7 +52,8 @@ export interface FormTestsOptions { * @param options Options for creating tests, including use cases. */ export function createFormTests( - options: FormTestsOptions) { + options: FormTestsOptions, +) { // Patch attachInternals in order to spy on `setFormValue()` for simulating // form state restoration. const originalAttachInternals = HTMLElement.prototype.attachInternals; @@ -71,11 +72,11 @@ export function createFormTests( } beforeAll(() => { - HTMLElement.prototype.attachInternals = function(this: HTMLElement) { + HTMLElement.prototype.attachInternals = function (this: HTMLElement) { const internals = originalAttachInternals.call(this); spyOn(internals, 'setFormValue').and.callThrough(); (this as unknown as HTMLElementWithInternals)[INTERNALS] = - internals as SpiedElementInternals; + internals as SpiedElementInternals; return internals; }; }); @@ -84,7 +85,7 @@ export function createFormTests( HTMLElement.prototype.attachInternals = originalAttachInternals; }); - let root: HTMLElement|undefined; + let root: HTMLElement | undefined; beforeEach(() => { root = document.createElement('div'); @@ -106,9 +107,9 @@ export function createFormTests( throw new Error('Could not query rendered
'); } - const control = - options.queryControl(root) as (T & ExpectedFormAssociatedElement) | - null; + const control = options.queryControl(root) as + | (T & ExpectedFormAssociatedElement) + | null; if (!control) { throw new Error('`queryControl` must return an element.'); } @@ -121,8 +122,8 @@ export function createFormTests( const {control} = await setupTest(); expect(control.constructor.formAssociated) - .withContext('control.constructor.formAssociated') - .toBeTrue(); + .withContext('control.constructor.formAssociated') + .toBeTrue(); }); it('should return associated form for `form` property', async () => { @@ -147,38 +148,34 @@ export function createFormTests( form.appendChild(labelParent); expect(control.labels) - .withContext('control.labels') - .toBeInstanceOf(NodeList); + .withContext('control.labels') + .toBeInstanceOf(NodeList); const labels = Array.from(control.labels); expect(labels) - .withContext('should contain parent label element') - .toContain(labelParent); + .withContext('should contain parent label element') + .toContain(labelParent); expect(labels) - .withContext('should contain label element with for attribute') - .toContain(labelFor); + .withContext('should contain label element with for attribute') + .toContain(labelFor); }); - it('should return empty NodeList for `labels` when not part of a ', - async () => { - const {form, control} = await setupTest(); - form.parentElement?.append(control); - expect(control.labels) - .withContext('control.labels') - .toBeInstanceOf(NodeList); - expect(control.labels.length) - .withContext('control.labels.length') - .toBe(0); - }); + it('should return empty NodeList for `labels` when not part of a ', async () => { + const {form, control} = await setupTest(); + form.parentElement?.append(control); + expect(control.labels) + .withContext('control.labels') + .toBeInstanceOf(NodeList); + expect(control.labels.length).withContext('control.labels.length').toBe(0); + }); - it('should have a name property that reflects to the name attribute', - async () => { - const {control} = await setupTest(); - control.name = 'control'; - await control?.updateComplete; - expect(control.getAttribute('name')) - .withContext('"name" reflected attribute') - .toBe('control'); - }); + it('should have a name property that reflects to the name attribute', async () => { + const {control} = await setupTest(); + control.name = 'control'; + await control?.updateComplete; + expect(control.getAttribute('name')) + .withContext('"name" reflected attribute') + .toBe('control'); + }); it('should not add a form value without a name', async () => { const {form, control} = await setupTest(); @@ -208,24 +205,26 @@ export function createFormTests( for (const restoreTest of options.restoreTests) { it(`it should pass the "${restoreTest.name}" restore test`, async () => { const {form} = await setupTest(restoreTest.render()); - const controls = - Array.from(form.elements) as ExpectedFormAssociatedElement[]; + const controls = Array.from( + form.elements, + ) as ExpectedFormAssociatedElement[]; for (const control of controls) { // Simulate restoring a new set of controls. For each control, we // grab its value and state from its internals. Then, we remove it from // the form, add a new control, and simulate restoring the state and // value for that control. - const [value, state] = - getInternals(control).setFormValue.calls.mostRecent()?.args ?? - [null, null]; + const [value, state] = getInternals( + control, + ).setFormValue.calls.mostRecent()?.args ?? [null, null]; - const newControl = document.createElement(control.tagName) as - ExpectedFormAssociatedElement; + const newControl = document.createElement( + control.tagName, + ) as ExpectedFormAssociatedElement; // Include any children for controls like `. Use MouseEvent for browser support. - element.dispatchEvent(new MouseEvent('click', { - ...this.createMouseEventInit(element), - ...init, - })); + element.dispatchEvent( + new MouseEvent('click', { + ...this.createMouseEventInit(element), + ...init, + }), + ); } /** @@ -409,13 +420,17 @@ export class Harness { * @param init Additional event options. */ protected simulateContextmenu( - element: HTMLElement, init: MouseEventInit = {}) { - element.dispatchEvent(new MouseEvent('contextmenu', { - ...this.createMouseEventInit(element), - button: 2, - buttons: 2, - ...init, - })); + element: HTMLElement, + init: MouseEventInit = {}, + ) { + element.dispatchEvent( + new MouseEvent('contextmenu', { + ...this.createMouseEventInit(element), + button: 2, + buttons: 2, + ...init, + }), + ); } /** @@ -439,12 +454,13 @@ export class Harness { */ protected simulatePointerFocus(element: HTMLElement) { this.addPseudoClass(element, ':focus'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.addPseudoClass(el, ':focus-within'); }); element.dispatchEvent(new FocusEvent('focus', {composed: true})); element.dispatchEvent( - new FocusEvent('focusin', {bubbles: true, composed: true})); + new FocusEvent('focusin', {bubbles: true, composed: true}), + ); } /** @@ -455,12 +471,13 @@ export class Harness { protected simulateBlur(element: HTMLElement) { this.removePseudoClass(element, ':focus'); this.removePseudoClass(element, ':focus-visible'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.removePseudoClass(el, ':focus-within'); }); element.dispatchEvent(new FocusEvent('blur', {composed: true})); element.dispatchEvent( - new FocusEvent('focusout', {bubbles: true, composed: true})); + new FocusEvent('focusout', {bubbles: true, composed: true}), + ); } /** @@ -470,8 +487,10 @@ export class Harness { * @param init Additional event options. */ protected simulateStartHover( - element: HTMLElement, init: PointerEventInit = {}) { - this.forEachNodeFrom(element, el => { + element: HTMLElement, + init: PointerEventInit = {}, + ) { + this.forEachNodeFrom(element, (el) => { this.addPseudoClass(el, ':hover'); }); const rect = element.getBoundingClientRect(); @@ -510,8 +529,10 @@ export class Harness { * @param init Additional event options. */ protected simulateEndHover( - element: HTMLElement, init: PointerEventInit = {}) { - this.forEachNodeFrom(element, el => { + element: HTMLElement, + init: PointerEventInit = {}, + ) { + this.forEachNodeFrom(element, (el) => { this.removePseudoClass(el, ':hover'); }); const rect = element.getBoundingClientRect(); @@ -550,9 +571,11 @@ export class Harness { * @param init Additional event options. */ protected simulateMousePress( - element: HTMLElement, init: PointerEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + ) { this.addPseudoClass(element, ':active'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.addPseudoClass(el, ':active'); }); const mouseInit = this.createMouseEventInit(element); @@ -575,9 +598,11 @@ export class Harness { * @param init Additional event options. */ protected simulateMouseRelease( - element: HTMLElement, init: PointerEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + ) { this.removePseudoClass(element, ':active'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.removePseudoClass(el, ':active'); }); const mouseInit = this.createMouseEventInit(element); @@ -599,10 +624,12 @@ export class Harness { * @param init Additional event options. */ protected simulateTouchPress( - element: HTMLElement, init: PointerEventInit = {}, - touchInit: TouchEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + touchInit: TouchEventInit = {}, + ) { this.addPseudoClass(element, ':active'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.addPseudoClass(el, ':active'); }); const mouseInit = this.createMouseEventInit(element); @@ -617,12 +644,14 @@ export class Harness { // Firefox does not support TouchEvent constructor if (window.TouchEvent) { const touch = this.createTouch(element); - element.dispatchEvent(new TouchEvent('touchstart', { - touches: [touch], - targetTouches: [touch], - changedTouches: [touch], - ...touchInit, - })); + element.dispatchEvent( + new TouchEvent('touchstart', { + touches: [touch], + targetTouches: [touch], + changedTouches: [touch], + ...touchInit, + }), + ); } this.simulatePointerFocus(element); } @@ -634,10 +663,12 @@ export class Harness { * @param init Additional event options. */ protected simulateTouchRelease( - element: HTMLElement, init: PointerEventInit = {}, - touchInit: TouchEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + touchInit: TouchEventInit = {}, + ) { this.removePseudoClass(element, ':active'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.removePseudoClass(el, ':active'); }); const mouseInit = this.createMouseEventInit(element); @@ -653,7 +684,8 @@ export class Harness { if (window.TouchEvent) { const touch = this.createTouch(element); element.dispatchEvent( - new TouchEvent('touchend', {changedTouches: [touch], ...touchInit})); + new TouchEvent('touchend', {changedTouches: [touch], ...touchInit}), + ); } } @@ -664,10 +696,12 @@ export class Harness { * @param init Additional event options. */ protected simulateTouchCancel( - element: HTMLElement, init: PointerEventInit = {}, - touchInit: TouchEventInit = {}) { + element: HTMLElement, + init: PointerEventInit = {}, + touchInit: TouchEventInit = {}, + ) { this.removePseudoClass(element, ':active'); - this.forEachNodeFrom(element, el => { + this.forEachNodeFrom(element, (el) => { this.removePseudoClass(el, ':active'); }); const mouseInit = this.createMouseEventInit(element); @@ -682,8 +716,9 @@ export class Harness { // Firefox does not support TouchEvent constructor if (window.TouchEvent) { const touch = this.createTouch(element); - element.dispatchEvent(new TouchEvent( - 'touchcancel', {changedTouches: [touch], ...touchInit})); + element.dispatchEvent( + new TouchEvent('touchcancel', {changedTouches: [touch], ...touchInit}), + ); } } @@ -695,7 +730,10 @@ export class Harness { * @param init Additional event options. */ protected simulateKeypress( - element: EventTarget, key: string, init: KeyboardEventInit = {}) { + element: EventTarget, + key: string, + init: KeyboardEventInit = {}, + ) { this.simulateKeydown(element, key, init); this.simulateKeyup(element, key, init); } @@ -708,14 +746,19 @@ export class Harness { * @param init Additional event options. */ protected simulateKeydown( - element: EventTarget, key: string, init: KeyboardEventInit = {}) { - element.dispatchEvent(new KeyboardEvent('keydown', { - ...init, - key, - bubbles: true, - composed: true, - cancelable: true, - })); + element: EventTarget, + key: string, + init: KeyboardEventInit = {}, + ) { + element.dispatchEvent( + new KeyboardEvent('keydown', { + ...init, + key, + bubbles: true, + composed: true, + cancelable: true, + }), + ); } /** @@ -726,14 +769,19 @@ export class Harness { * @param init Additional keyboard options. */ protected simulateKeyup( - element: EventTarget, key: string, init: KeyboardEventInit = {}) { - element.dispatchEvent(new KeyboardEvent('keyup', { - ...init, - key, - bubbles: true, - composed: true, - cancelable: true, - })); + element: EventTarget, + key: string, + init: KeyboardEventInit = {}, + ) { + element.dispatchEvent( + new KeyboardEvent('keyup', { + ...init, + key, + bubbles: true, + composed: true, + cancelable: true, + }), + ); } /** @@ -797,11 +845,11 @@ export class Harness { * @param parent The last parent element to visit. */ protected forEachNodeFrom( - child: HTMLElement, - callback: (node: HTMLElement) => void, - parent: HTMLElement = this.element, + child: HTMLElement, + callback: (node: HTMLElement) => void, + parent: HTMLElement = this.element, ) { - let nextNode: Node|null = child; + let nextNode: Node | null = child; while (nextNode && nextNode !== this.element) { const currentNode: Node = nextNode; nextNode = currentNode.parentNode || (currentNode as ShadowRoot).host; @@ -816,7 +864,7 @@ export class Harness { const slot = currentNode.getAttribute('slot'); const slotSelector = slot ? `slot[name=${slot}]` : 'slot:not([name])'; const slotElement = - nextNode.shadowRoot.querySelector(slotSelector); + nextNode.shadowRoot.querySelector(slotSelector); if (slotElement) { this.forEachNodeFrom(slotElement, callback, nextNode); } diff --git a/testing/skip-animations.ts b/testing/skip-animations.ts index 4532a95db..b3e4c3ed2 100644 --- a/testing/skip-animations.ts +++ b/testing/skip-animations.ts @@ -8,11 +8,17 @@ export function installSkipWebAnimations() { const nativeAnimate = Element.prototype.animate; function patchedAnimate( - this: Element, ...args: Parameters) { + this: Element, + ...args: Parameters + ) { const animation = nativeAnimate.apply(this, args); if (animation.effect) { - animation.effect.updateTiming( - {delay: 0, duration: 1, easing: 'step-start', iterations: 1}); + animation.effect.updateTiming({ + delay: 0, + duration: 1, + easing: 'step-start', + iterations: 1, + }); } return animation; @@ -22,7 +28,7 @@ export function installSkipWebAnimations() { configurable: true, enumerable: true, writable: true, - value: patchedAnimate + value: patchedAnimate, }); // return uninstall function. @@ -31,7 +37,7 @@ export function installSkipWebAnimations() { configurable: true, enumerable: true, writable: true, - value: nativeAnimate + value: nativeAnimate, }); }; } diff --git a/testing/skip-animations_test.ts b/testing/skip-animations_test.ts index 1789f194c..d163012ee 100644 --- a/testing/skip-animations_test.ts +++ b/testing/skip-animations_test.ts @@ -24,12 +24,9 @@ describe('skip-animations test', () => { it('does not skip animations before installing', () => { // Act - element.animate( - [ - {color: 'rgb(255, 255, 255'}, - {color: 'rgb(0, 0, 0)'}, - ], - {duration: 1000}); + element.animate([{color: 'rgb(255, 255, 255'}, {color: 'rgb(0, 0, 0)'}], { + duration: 1000, + }); // Assert expect(getComputedStyle(element).color).toEqual('rgb(255, 255, 255)'); @@ -40,12 +37,9 @@ describe('skip-animations test', () => { uninstallFunction = installSkipWebAnimations(); // Act - element.animate( - [ - {color: 'rgb(255, 255, 255'}, - {color: 'rgb(0, 0, 0)'}, - ], - {duration: 1000}); + element.animate([{color: 'rgb(255, 255, 255'}, {color: 'rgb(0, 0, 0)'}], { + duration: 1000, + }); // Assert expect(getComputedStyle(element).color).toEqual('rgb(0, 0, 0)'); diff --git a/testing/table/internal/test-table.ts b/testing/table/internal/test-table.ts index 084260aaa..8717dd660 100644 --- a/testing/table/internal/test-table.ts +++ b/testing/table/internal/test-table.ts @@ -12,7 +12,7 @@ import {html, literal} from 'lit/static-html.js'; /** Test table interface. */ export interface TestTableTemplate { /** The row display name. May be a Lit static value for rich HTML. */ - display: string|ReturnType; + display: string | ReturnType; /** * A template's render function. It accepts a state string (the column) and * returns a Lit `TemplateResult`. @@ -20,7 +20,7 @@ export interface TestTableTemplate { * @param state The current state to render in. * @return A `TemplateResult` for the given state. */ - render(state: S): TemplateResult|null; + render(state: S): TemplateResult | null; } /** @@ -41,9 +41,11 @@ export class TestTable extends LitElement { - ${this.states.map(state => html` - ${state} - `)} + ${this.states.map( + (state) => html` + ${state} + `, + )} @@ -57,32 +59,39 @@ export class TestTable extends LitElement { protected renderTemplates() { // Render templates in the light DOM for easier styling access render( - this.templates.map( - (template, rowIndex) => this.states.map((state, colIndex) => { - const renderResult = template.render(state); - const isEmptyTemplate = renderResult === null; - return isEmptyTemplate ? html`` : html` -
- ${renderResult} -
`; - })), - this); + this.templates.map((template, rowIndex) => + this.states.map((state, colIndex) => { + const renderResult = template.render(state); + const isEmptyTemplate = renderResult === null; + return isEmptyTemplate + ? html`` + : html`
+ ${renderResult} +
`; + }), + ), + this, + ); return html` - ${this.templates.map((template, rowIndex) => html` - - - ${this.getVariantName(template.display)} - - ${this.states.map((state, colIndex) => html` - - -
N/A
-
- - `)} - - `)} + ${this.templates.map( + (template, rowIndex) => html` + + + ${this.getVariantName(template.display)} + + ${this.states.map( + (state, colIndex) => html` + + +
N/A
+
+ + `, + )} + + `, + )} `; } diff --git a/testing/table/internal/test-table_test.ts b/testing/table/internal/test-table_test.ts index 994c3b115..18790b89f 100644 --- a/testing/table/internal/test-table_test.ts +++ b/testing/table/internal/test-table_test.ts @@ -20,8 +20,7 @@ declare global { } @customElement('test-test-table') -class TestTestTable extends TestTable { -} +class TestTestTable extends TestTable {} describe('', () => { const env = new Environment(); diff --git a/testing/templates.ts b/testing/templates.ts index 7b32d2f38..7caa4a6fe 100644 --- a/testing/templates.ts +++ b/testing/templates.ts @@ -68,8 +68,10 @@ export enum State { * @template H Optional element harness type. * @template V Variant name types. */ -export class TemplateBuilder { +export class TemplateBuilder< + H extends Harness = never, + V extends string = never, +> { /** * A map of variant names and their template factories. */ @@ -77,7 +79,7 @@ export class TemplateBuilder) => H; + private harnessCtor?: new (element: HarnessElement) => H; /** * The current state callback to invoke after rendering. */ @@ -97,7 +99,7 @@ export class TemplateBuilder { - return testCaseProps.map(props => ({display, render: factory(props)})); + return testCaseProps.map((props) => ({display, render: factory(props)})); }); } @@ -128,7 +130,8 @@ export class TemplateBuilder( - harnessCtor: new(element: HarnessElement) => NewHarness) { + harnessCtor: new (element: HarnessElement) => NewHarness, + ) { const typedThis = this as unknown as TemplateBuilder; typedThis.harnessCtor = harnessCtor; return typedThis; @@ -178,15 +181,18 @@ export class TemplateBuilder|TemplateVariantOptions>) { + variants: Record | TemplateVariantOptions>, + ) { // TODO: clean this up by only allowing TemplateVariantOptions and force // users to specify the display name. for (const variant of Object.keys(variants)) { this.withVariant(variant, variants[variant]); } - return this as unknown as - TemplateBuilder>; + return this as unknown as TemplateBuilder< + H, + V | Extract + >; } /** @@ -199,26 +205,30 @@ export class TemplateBuilder( - variant: NewVariant, - renderOrOptions: TemplateRender|TemplateVariantOptions) { + variant: NewVariant, + renderOrOptions: TemplateRender | TemplateVariantOptions, + ) { // TODO: clean this up by only allowing TemplateVariantOptions and force // users to specify the display name. - const typedThis = this as unknown as TemplateBuilder; - const {display, render} = typeof renderOrOptions === 'function' ? - {display: variant, render: renderOrOptions} : - renderOrOptions; + const typedThis = this as unknown as TemplateBuilder; + const {display, render} = + typeof renderOrOptions === 'function' + ? {display: variant, render: renderOrOptions} + : renderOrOptions; typedThis.variants.set(variant, { display: display ?? variant, - factory: props => { - return state => { - const directive = ref(async element => { + factory: (props) => { + return (state) => { + const directive = ref(async (element) => { if (!element) { return; } const harness = await this.createHarnessAndApplyState( - element as HarnessElement, state); + element as HarnessElement, + state, + ); // Allow the component to apply additional state or perform custom // state logic. @@ -227,7 +237,7 @@ export class TemplateBuilder, state: string): Promise { + element: HarnessElement, + state: string, + ): Promise { if (!this.harnessCtor) { return undefined as never; } - const harness = isElementWithHarness(element) ? - element.harness as H : - new this.harnessCtor(element); + const harness = isElementWithHarness(element) + ? (element.harness as H) + : new this.harnessCtor(element); // Common shared component state harness actions await harness.reset(); switch (state) { @@ -280,7 +292,7 @@ export class TemplateBuilder { /** The variant's display name. */ - display: string|StaticValue; + display: string | StaticValue; /** * A factory function that takes an object of element properties and returns * another a test table template that renders the variant's element for a @@ -298,7 +310,7 @@ export interface TemplateVariantOptions { /** A function to render this variant. */ render: TemplateRender; /** Custom variant display name. Defaults to the name of the variant. */ - display?: string|ReturnType; + display?: string | ReturnType; } // TODO: clean this devx up a bit by swapping props/state args @@ -318,9 +330,11 @@ export interface TemplateVariantOptions { * @param state The current state to render the element in. * @return A `TemplateResult` rendering the element. */ -export type TemplateRender = - (directive: DirectiveResult, props: TemplateProps, state: string) => - TemplateResult|null; +export type TemplateRender = ( + directive: DirectiveResult, + props: TemplateProps, + state: string, +) => TemplateResult | null; /** * A callback that is invoked after the template's element has rendered. It @@ -333,8 +347,10 @@ export type TemplateRender = * @param state The current test table state. * @param harness The rendered element's harness. */ -export type TemplateStateCallback = - (state: string, harness: H) => void; +export type TemplateStateCallback = ( + state: string, + harness: H, +) => void; /** * Element properties for a harness constructor. Returns a partial object with @@ -343,9 +359,10 @@ export type TemplateStateCallback = * * @template H The harness type. */ -export type TemplateProps = Partial, Exclude, keyof HTMLElement>>>& - SharedTemplateProps; +export type TemplateProps = Partial< + Pick, Exclude, keyof HTMLElement>> +> & + SharedTemplateProps; /** * Shared element properties for all harnesses. diff --git a/testing/tokens.ts b/testing/tokens.ts index 1d5a8f163..c488bee18 100644 --- a/testing/tokens.ts +++ b/testing/tokens.ts @@ -103,8 +103,9 @@ export function getUnusedTokens(styles: CSSResult[]) { return unusedTokens; } -function getDefinedTokensFromRule(rule: CSSRule|CSSStyleSheet| - CSSStyleRule): Set { +function getDefinedTokensFromRule( + rule: CSSRule | CSSStyleSheet | CSSStyleRule, +): Set { let defined = new Set(); if ('cssRules' in rule) { // Rule is either a CSSStyleSheet, CSSKeyframesRule, or one of the @@ -125,8 +126,9 @@ function getDefinedTokensFromRule(rule: CSSRule|CSSStyleSheet| return defined; } -function getUsedTokensFromRule(rule: CSSRule|CSSStyleSheet| - CSSStyleRule): Set { +function getUsedTokensFromRule( + rule: CSSRule | CSSStyleSheet | CSSStyleRule, +): Set { let used = new Set(); if ('cssRules' in rule) { // Rule is either a CSSStyleSheet, CSSKeyframesRule, or one of the diff --git a/testing/tokens_test.ts b/testing/tokens_test.ts index 30d896915..85e9080cb 100644 --- a/testing/tokens_test.ts +++ b/testing/tokens_test.ts @@ -41,9 +41,9 @@ describe('testing', () => { `; const unusedTokens = getUnusedTokens([styles]); - expect(unusedTokens).withContext('unused tokens').toEqual([ - '--_unused' - ]); + expect(unusedTokens) + .withContext('unused tokens') + .toEqual(['--_unused']); }); }); @@ -76,9 +76,9 @@ describe('testing', () => { `; const undefinedTokens = getUndefinedTokens([styles]); - expect(undefinedTokens).withContext('undefined tokens').toEqual([ - '--_undefined' - ]); + expect(undefinedTokens) + .withContext('undefined tokens') + .toEqual(['--_undefined']); }); }); }); diff --git a/testing/transform-pseudo-classes.ts b/testing/transform-pseudo-classes.ts index 26f877e5e..0d24882d7 100644 --- a/testing/transform-pseudo-classes.ts +++ b/testing/transform-pseudo-classes.ts @@ -62,8 +62,9 @@ const transformedStyleSheets = new WeakSet(); * @param pseudoClasses An optional array of pseudo class names to transform. */ export function transformPseudoClasses( - stylesheets: Iterable, - pseudoClasses = defaultTransformPseudoClasses) { + stylesheets: Iterable, + pseudoClasses = defaultTransformPseudoClasses, +) { for (const stylesheet of stylesheets) { if (transformedStyleSheets.has(stylesheet)) { continue; @@ -91,8 +92,10 @@ export function transformPseudoClasses( * CSSGroupingRule unlike Chrome and Safari */ function isCSSGroupingRule(rule: CSSRule): rule is CSSGroupingRule { - return !!(rule as CSSGroupingRule)?.cssRules && - !(rule as CSSStyleRule).selectorText; + return ( + !!(rule as CSSGroupingRule)?.cssRules && + !(rule as CSSStyleRule).selectorText + ); } /** @@ -105,8 +108,11 @@ function isCSSGroupingRule(rule: CSSRule): rule is CSSGroupingRule { * @param pseudoClasses An array of pseudo classes to search for and replace. */ function visitRule( - rule: CSSRule, stylesheet: CSSStyleSheet|CSSGroupingRule, index: number, - pseudoClasses: string[]) { + rule: CSSRule, + stylesheet: CSSStyleSheet | CSSGroupingRule, + index: number, + pseudoClasses: string[], +) { if (isCSSGroupingRule(rule)) { for (let i = rule.cssRules.length - 1; i >= 0; i--) { visitRule(rule.cssRules[i], rule, i, pseudoClasses); @@ -123,7 +129,7 @@ function visitRule( // match :foo, ensuring that it does not have a paren at the end // (no pseudo class functions like :foo()) const regex = /(:(?![\w-]+\()[\w-]+)/g; - const matches = Array.from(selectorText.matchAll(regex)).filter(match => { + const matches = Array.from(selectorText.matchAll(regex)).filter((match) => { // don't match pseudo elements like ::foo if (match.index != null && selectorText[match.index - 1] === ':') { return false; @@ -138,9 +144,10 @@ function visitRule( matches.reverse(); selectorText = rearrangePseudoElements(selectorText); for (const match of matches) { - selectorText = selectorText.substring(0, match.index!) + - `.${getTransformedPseudoClass(match[1])}` + - selectorText.substring(match.index! + match[1].length); + selectorText = + selectorText.substring(0, match.index!) + + `.${getTransformedPseudoClass(match[1])}` + + selectorText.substring(match.index! + match[1].length); } const css = `${selectorText} {${rule.style.cssText}}`; @@ -166,19 +173,24 @@ function visitRule( * @return The re-arranged selector text. */ function rearrangePseudoElements(selectorText: string) { - const pseudoElementsBeforeClasses = - Array.from(selectorText.matchAll(/(?:::[\w-]+)+(?=:[\w-])/g)); + const pseudoElementsBeforeClasses = Array.from( + selectorText.matchAll(/(?:::[\w-]+)+(?=:[\w-])/g), + ); pseudoElementsBeforeClasses.reverse(); for (const match of pseudoElementsBeforeClasses) { const pseudoElement = match[0]; const pseudoElementIndex = match.index!; - const endOfCompoundSelector = selectorText.substring(pseudoElementIndex) - .match(/(\s(?!([^\s].)*\))|,|$)/)!; + const endOfCompoundSelector = selectorText + .substring(pseudoElementIndex) + .match(/(\s(?!([^\s].)*\))|,|$)/)!; const index = endOfCompoundSelector.index! + pseudoElementIndex; - selectorText = selectorText.substring(0, index) + pseudoElement + - selectorText.substring(index); - selectorText = selectorText.substring(0, pseudoElementIndex) + - selectorText.substring(pseudoElementIndex + pseudoElement.length); + selectorText = + selectorText.substring(0, index) + + pseudoElement + + selectorText.substring(index); + selectorText = + selectorText.substring(0, pseudoElementIndex) + + selectorText.substring(pseudoElementIndex + pseudoElement.length); } return selectorText; diff --git a/textfield/demo/demo.ts b/textfield/demo/demo.ts index 0ee83bf5f..eb737c626 100644 --- a/textfield/demo/demo.ts +++ b/textfield/demo/demo.ts @@ -4,24 +4,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import './index.js'; import './material-collection.js'; +import './index.js'; -import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js'; +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; import {boolInput, Knob, textInput} from './index.js'; import {stories, StoryKnobs} from './stories.js'; -const collection = - new MaterialCollection>('Textfield', [ - new Knob('label', {ui: textInput(), defaultValue: 'Label'}), - new Knob('placeholder', {ui: textInput(), defaultValue: ''}), - new Knob('disabled', {ui: boolInput(), defaultValue: false}), - new Knob('prefixText', {ui: textInput(), defaultValue: ''}), - new Knob('suffixText', {ui: textInput(), defaultValue: ''}), - new Knob( - 'supportingText', {ui: textInput(), defaultValue: 'Supporting text'}), - ]); +const collection = new MaterialCollection>( + 'Textfield', + [ + new Knob('label', {ui: textInput(), defaultValue: 'Label'}), + new Knob('placeholder', {ui: textInput(), defaultValue: ''}), + new Knob('disabled', {ui: boolInput(), defaultValue: false}), + new Knob('prefixText', {ui: textInput(), defaultValue: ''}), + new Knob('suffixText', {ui: textInput(), defaultValue: ''}), + new Knob('supportingText', { + ui: textInput(), + defaultValue: 'Supporting text', + }), + ], +); collection.addStories(...materialInitsToStoryInits(stories)); diff --git a/textfield/demo/stories.ts b/textfield/demo/stories.ts index 5414bbe3e..805d1144c 100644 --- a/textfield/demo/stories.ts +++ b/textfield/demo/stories.ts @@ -39,11 +39,11 @@ const styles = css` width: 200px; } - [type=textarea] { + [type='textarea'] { min-height: 56px; } - [type=textarea][supporting-text] { + [type='textarea'][supporting-text] { min-height: 76px; } `; @@ -60,8 +60,8 @@ const textfields: MaterialStoryInit = { placeholder=${knobs.placeholder || nothing} prefix-text=${knobs.prefixText || nothing} suffix-text=${knobs.suffixText || nothing} - supporting-text=${knobs.supportingText || nothing} - > + supporting-text=${knobs.supportingText || + nothing}> = { placeholder=${knobs.placeholder || nothing} prefix-text=${knobs.prefixText || nothing} suffix-text=${knobs.suffixText || nothing} - supporting-text=${knobs.supportingText || nothing} - > + supporting-text=${knobs.supportingText || + nothing}>
`; - } + }, }; const textareas: MaterialStoryInit = { @@ -87,19 +87,19 @@ const textareas: MaterialStoryInit = { ?disabled=${knobs.disabled} label=${knobs.label || nothing} placeholder=${knobs.placeholder || nothing} - supporting-text=${knobs.supportingText || nothing} - > + supporting-text=${knobs.supportingText || + nothing}> + supporting-text=${knobs.supportingText || + nothing}>
`; - } + }, }; const icons: MaterialStoryInit = { @@ -115,11 +115,12 @@ const icons: MaterialStoryInit = { value="Value" prefix-text=${knobs.prefixText || nothing} suffix-text=${knobs.suffixText || nothing} - supporting-text=${knobs.supportingText || nothing} - > + supporting-text=${knobs.supportingText || nothing}> search - + clear @@ -131,17 +132,18 @@ const icons: MaterialStoryInit = { value="Value" prefix-text=${knobs.prefixText || nothing} suffix-text=${knobs.suffixText || nothing} - supporting-text=${knobs.supportingText || nothing} - > + supporting-text=${knobs.supportingText || nothing}> search - + clear `; - } + }, }; const validation: MaterialStoryInit = { @@ -156,8 +158,7 @@ const validation: MaterialStoryInit = { value="Value" required supporting-text="* this field is required" - @change=${reportValidity} - > + @change=${reportValidity}> = { min="1" max="10" supporting-text="Enter a number between 1 and 10" - @change=${reportValidity} - > + @change=${reportValidity}> = { minlength="3" maxlength="10" supporting-text="3 to 10 characters" - @change=${reportValidity} - > + @change=${reportValidity}> = { placeholder="username" suffix-text="@gmail.com" supporting-text="Characters only" - @change=${reportValidity} - > + @change=${reportValidity}> `; - } + }, }; const forms: MaterialStoryInit = { @@ -212,14 +210,12 @@ const forms: MaterialStoryInit = { ?disabled=${knobs.disabled} label="First name" name="first-name" - autocomplete="given-name" - > + autocomplete="given-name"> + autocomplete="family-name">
Reset @@ -227,7 +223,7 @@ const forms: MaterialStoryInit = {
`; - } + }, }; function reportValidity(event: Event) { diff --git a/textfield/filled-text-field.ts b/textfield/filled-text-field.ts index 4b1a3a84f..c0d5a6d77 100644 --- a/textfield/filled-text-field.ts +++ b/textfield/filled-text-field.ts @@ -29,8 +29,11 @@ declare global { */ @customElement('md-filled-text-field') export class MdFilledTextField extends FilledTextField { - static override styles = - [sharedStyles, filledStyles, filledForcedColorsStyles]; + static override styles = [ + sharedStyles, + filledStyles, + filledForcedColorsStyles, + ]; protected override readonly fieldTag = literal`md-filled-field`; } diff --git a/textfield/harness.ts b/textfield/harness.ts index 2317d6085..f0f9099dc 100644 --- a/textfield/harness.ts +++ b/textfield/harness.ts @@ -53,7 +53,10 @@ export class TextFieldHarness extends Harness { async deleteValue(beginIndex?: number, endIndex?: number) { this.simulateKeypress(await this.getInteractiveElement(), 'Backspace'); this.simulateDeletion( - await this.getInteractiveElement(), beginIndex, endIndex); + await this.getInteractiveElement(), + beginIndex, + endIndex, + ); } override async reset() { @@ -78,8 +81,10 @@ export class TextFieldHarness extends Harness { } protected simulateInput( - element: HTMLInputElement|HTMLTextAreaElement, charactersToAppend: string, - init?: InputEventInit) { + element: HTMLInputElement | HTMLTextAreaElement, + charactersToAppend: string, + init?: InputEventInit, + ) { element.value += charactersToAppend; if (!init) { init = { @@ -95,11 +100,15 @@ export class TextFieldHarness extends Harness { } protected simulateDeletion( - element: HTMLInputElement|HTMLTextAreaElement, beginIndex?: number, - endIndex?: number, init?: InputEventInit) { + element: HTMLInputElement | HTMLTextAreaElement, + beginIndex?: number, + endIndex?: number, + init?: InputEventInit, + ) { const deletedCharacters = element.value.slice(beginIndex, endIndex); - element.value = element.value.substring(0, beginIndex ?? 0) + - element.value.substring(endIndex ?? element.value.length); + element.value = + element.value.substring(0, beginIndex ?? 0) + + element.value.substring(endIndex ?? element.value.length); if (!init) { init = { inputType: 'deleteContentBackward', @@ -113,8 +122,9 @@ export class TextFieldHarness extends Harness { element.dispatchEvent(new InputEvent('input', init)); } - protected simulateChangeIfNeeded(element: HTMLInputElement| - HTMLTextAreaElement) { + protected simulateChangeIfNeeded( + element: HTMLInputElement | HTMLTextAreaElement, + ) { if (this.valueBeforeChange === element.value) { return; } @@ -125,7 +135,8 @@ export class TextFieldHarness extends Harness { protected override async getInteractiveElement() { await this.element.updateComplete; - return this.element.renderRoot.querySelector('.input') as HTMLInputElement | - HTMLTextAreaElement; + return this.element.renderRoot.querySelector('.input') as + | HTMLInputElement + | HTMLTextAreaElement; } } diff --git a/textfield/harness_test.ts b/textfield/harness_test.ts index 8b247629b..438f0c176 100644 --- a/textfield/harness_test.ts +++ b/textfield/harness_test.ts @@ -18,8 +18,9 @@ describe('TextFieldHarness', () => { const env = new Environment(); async function setupTest() { - const root = - env.render(html``); + const root = env.render( + html``, + ); const instance = root.querySelector('md-filled-text-field'); if (!instance) { throw new Error('Failed to query md-filled-text-field.'); @@ -72,9 +73,11 @@ describe('TextFieldHarness', () => { // Assertion. expect(keydownHandler).toHaveBeenCalledTimes(1); expect(keydownHandler).toHaveBeenCalledWith(jasmine.any(KeyboardEvent)); - expect(keydownHandler).toHaveBeenCalledWith(jasmine.objectContaining({ - key: 'Backspace' - })); + expect(keydownHandler).toHaveBeenCalledWith( + jasmine.objectContaining({ + key: 'Backspace', + }), + ); }); it('should delete the entire value by default', async () => { @@ -121,19 +124,18 @@ describe('TextFieldHarness', () => { }); describe('simulating change events', () => { - it('should dispatch change if value changes after focus and blur', - async () => { - // Setup. - const harness = await setupTest(); - const changeHandler = jasmine.createSpy('changeHandler'); - harness.element.addEventListener('change', changeHandler); - // Test case. - await harness.focusWithKeyboard(); - await harness.inputValue('value'); - await harness.blur(); - // Assertion. - expect(changeHandler).toHaveBeenCalledTimes(1); - }); + it('should dispatch change if value changes after focus and blur', async () => { + // Setup. + const harness = await setupTest(); + const changeHandler = jasmine.createSpy('changeHandler'); + harness.element.addEventListener('change', changeHandler); + // Test case. + await harness.focusWithKeyboard(); + await harness.inputValue('value'); + await harness.blur(); + // Assertion. + expect(changeHandler).toHaveBeenCalledTimes(1); + }); it('should not dispatch change if value does not change', async () => { // Setup. diff --git a/textfield/internal/text-field.ts b/textfield/internal/text-field.ts index b7a3624e3..3127b30be 100644 --- a/textfield/internal/text-field.ts +++ b/textfield/internal/text-field.ts @@ -21,19 +21,39 @@ import {stringConverter} from '../../internal/controller/string-converter.js'; * Input types that are compatible with the text field. */ export type TextFieldType = - 'email'|'number'|'password'|'search'|'tel'|'text'|'url'|'textarea'; + | 'email' + | 'number' + | 'password' + | 'search' + | 'tel' + | 'text' + | 'url' + | 'textarea'; /** * Input types that are not fully supported for the text field. */ export type UnsupportedTextFieldType = - 'color'|'date'|'datetime-local'|'file'|'month'|'time'|'week'; + | 'color' + | 'date' + | 'datetime-local' + | 'file' + | 'month' + | 'time' + | 'week'; /** * Input types that are incompatible with the text field. */ export type InvalidTextFieldType = - 'button'|'checkbox'|'hidden'|'image'|'radio'|'range'|'reset'|'submit'; + | 'button' + | 'checkbox' + | 'hidden' + | 'image' + | 'radio' + | 'range' + | 'reset' + | 'submit'; /** * A text field component. @@ -44,8 +64,10 @@ export abstract class TextField extends LitElement { } /** @nocollapse */ - static override shadowRootOptions: - ShadowRootInit = {...LitElement.shadowRootOptions, delegatesFocus: true}; + static override shadowRootOptions: ShadowRootInit = { + ...LitElement.shadowRootOptions, + delegatesFocus: true, + }; /** @nocollapse */ static readonly formAssociated = true; @@ -195,7 +217,7 @@ export abstract class TextField extends LitElement { get selectionDirection() { return this.getInputOrTextarea().selectionDirection; } - set selectionDirection(value: 'forward'|'backward'|'none'|null) { + set selectionDirection(value: 'forward' | 'backward' | 'none' | null) { this.getInputOrTextarea().selectionDirection = value; } @@ -205,7 +227,7 @@ export abstract class TextField extends LitElement { get selectionEnd() { return this.getInputOrTextarea().selectionEnd; } - set selectionEnd(value: number|null) { + set selectionEnd(value: number | null) { this.getInputOrTextarea().selectionEnd = value; } @@ -215,7 +237,7 @@ export abstract class TextField extends LitElement { get selectionStart() { return this.getInputOrTextarea().selectionStart; } - set selectionStart(value: number|null) { + set selectionStart(value: number | null) { this.getInputOrTextarea().selectionStart = value; } @@ -247,7 +269,7 @@ export abstract class TextField extends LitElement { * for more details on each input type. */ @property({reflect: true}) - type: TextFieldType|UnsupportedTextFieldType = 'text'; + type: TextFieldType | UnsupportedTextFieldType = 'text'; /** * Describes what, if any, type of autocomplete functionality the input @@ -310,7 +332,7 @@ export abstract class TextField extends LitElement { return input.valueAsDate; } - set valueAsDate(value: Date|null) { + set valueAsDate(value: Date | null) { const input = this.getInput(); if (!input) { return; @@ -354,8 +376,11 @@ export abstract class TextField extends LitElement { } @query('.input') - private readonly inputOrTextarea?: HTMLInputElement|HTMLTextAreaElement|null; - @query('.field') private readonly field?: Field|null; + private readonly inputOrTextarea?: + | HTMLInputElement + | HTMLTextAreaElement + | null; + @query('.field') private readonly field?: Field | null; @queryAssignedElements({slot: 'leading-icon'}) private readonly leadingIcons!: Element[]; @queryAssignedElements({slot: 'trailing-icon'}) @@ -363,8 +388,8 @@ export abstract class TextField extends LitElement { // Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432 // Replace with this.internals.validity.customError when resolved. private hasCustomValidityError = false; - private readonly internals = - (this as HTMLElement /* needed for closure */).attachInternals(); + // Cast needed for closure + private readonly internals = (this as HTMLElement).attachInternals(); /** * Checks the text field's native validation and returns whether or not the @@ -400,10 +425,14 @@ export abstract class TextField extends LitElement { * @return true if the text field is valid, or false if not. */ reportValidity() { - let invalidEvent: Event|undefined; - this.addEventListener('invalid', event => { - invalidEvent = event; - }, {once: true}); + let invalidEvent: Event | undefined; + this.addEventListener( + 'invalid', + (event) => { + invalidEvent = event; + }, + {once: true}, + ); const valid = this.checkValidity(); if (invalidEvent?.defaultPrevented) { @@ -444,7 +473,10 @@ export abstract class TextField extends LitElement { setCustomValidity(error: string) { this.hasCustomValidityError = !!error; this.internals.setValidity( - {customError: !!error}, error, this.getInputOrTextarea()); + {customError: !!error}, + error, + this.getInputOrTextarea(), + ); } /** @@ -454,13 +486,17 @@ export abstract class TextField extends LitElement { */ setRangeText(replacement: string): void; setRangeText( - replacement: string, start: number, end: number, - selectionMode?: SelectionMode): void; + replacement: string, + start: number, + end: number, + selectionMode?: SelectionMode, + ): void; setRangeText(...args: unknown[]) { // Calling setRangeText with 1 vs 3-4 arguments has different behavior. // Use spread syntax and type casting to ensure correct usage. this.getInputOrTextarea().setRangeText( - ...args as Parameters); + ...(args as Parameters), + ); this.value = this.getInputOrTextarea().value; } @@ -474,8 +510,10 @@ export abstract class TextField extends LitElement { * @param direction The direction in which the selection is performed. */ setSelectionRange( - start: number|null, end: number|null, - direction?: 'forward'|'backward'|'none') { + start: number | null, + end: number | null, + direction?: 'forward' | 'backward' | 'none', + ) { this.getInputOrTextarea().setSelectionRange(start, end, direction); } @@ -526,7 +564,10 @@ export abstract class TextField extends LitElement { } override attributeChangedCallback( - attribute: string, newValue: string|null, oldValue: string|null) { + attribute: string, + newValue: string | null, + oldValue: string | null, + ) { if (attribute === 'value' && this.dirty) { // After user input, changing the value attribute no longer updates the // text field's value (until reset). This matches native behavior. @@ -544,10 +585,10 @@ export abstract class TextField extends LitElement { }; return html` - - ${this.renderField()} - - `; + + ${this.renderField()} + + `; } protected override updated(changedProperties: PropertyValues) { @@ -595,24 +636,24 @@ export abstract class TextField extends LitElement { private renderLeadingIcon() { return html` - - - - `; + + + + `; } private renderTrailingIcon() { return html` - - - - `; + + + + `; } private renderInputOrTextarea() { const style = {direction: this.textDirection}; const ariaLabel = - (this as ARIAMixinStrict).ariaLabel || this.label || nothing; + (this as ARIAMixinStrict).ariaLabel || this.label || nothing; // lit-anaylzer `autocomplete` types are too strict // tslint:disable-next-line:no-any const autocomplete = this.autocomplete as any; @@ -639,8 +680,7 @@ export abstract class TextField extends LitElement { @focusin=${this.handleFocusin} @focusout=${this.handleFocusout} @input=${this.handleInput} - @select=${this.redispatchEvent} - > + @select=${this.redispatchEvent}> `; } @@ -679,8 +719,7 @@ export abstract class TextField extends LitElement { @focusin=${this.handleFocusin} @focusout=${this.handleFocusout} @input=${this.handleInput} - @select=${this.redispatchEvent} - > + @select=${this.redispatchEvent} /> ${suffix} `; @@ -776,7 +815,10 @@ export abstract class TextField extends LitElement { } this.internals.setValidity( - input.validity, input.validationMessage, this.getInputOrTextarea()); + input.validity, + input.validationMessage, + this.getInputOrTextarea(), + ); } private handleIconChange() { diff --git a/textfield/internal/text-field_test.ts b/textfield/internal/text-field_test.ts index 36c4528bc..359e11630 100644 --- a/textfield/internal/text-field_test.ts +++ b/textfield/internal/text-field_test.ts @@ -30,8 +30,10 @@ class TestTextField extends TextField { async getHasError() { await this.updateComplete; - return this.renderRoot.querySelector('input')?.getAttribute( - 'aria-invalid') === 'true'; + return ( + this.renderRoot.querySelector('input')?.getAttribute('aria-invalid') === + 'true' + ); } async getErrorTextValue() { @@ -44,7 +46,8 @@ describe('TextField', () => { const env = new Environment(); async function setupTest( - template = html``) { + template = html``, + ) { // Variant type does not matter for shared tests const element = env.render(template).querySelector('md-test-text-field'); if (!element) { @@ -73,21 +76,18 @@ describe('TextField', () => { expect(input.matches(':focus')).withContext('is input:focus').toBeTrue(); }); - it('should NOT focus the input when elements inside text field are clicked', - async () => { - const {harness, input} = await setupTest(); - // Add a trailing icon button to click on - render(html``, harness.element); - const button = harness.element.querySelector('button'); + it('should NOT focus the input when elements inside text field are clicked', async () => { + const {harness, input} = await setupTest(); + // Add a trailing icon button to click on + render(html``, harness.element); + const button = harness.element.querySelector('button'); - expect(button).toBeDefined(); - const buttonHarness = new Harness(button!); - await buttonHarness.clickWithMouse(); + expect(button).toBeDefined(); + const buttonHarness = new Harness(button!); + await buttonHarness.clickWithMouse(); - expect(input.matches(':focus')) - .withContext('is input:focus') - .toBeFalse(); - }); + expect(input.matches(':focus')).withContext('is input:focus').toBeFalse(); + }); it('should not focus the input when disabled', async () => { const {harness, input} = await setupTest(); @@ -97,8 +97,8 @@ describe('TextField', () => { harness.element.focus(); expect(input.matches(':focus')) - .withContext('not input:focus') - .toBeFalse(); + .withContext('not input:focus') + .toBeFalse(); }); it('focus() should focus input', async () => { @@ -116,8 +116,8 @@ describe('TextField', () => { harness.element.blur(); expect(input.matches(':focus')) - .withContext('not input:focus') - .toBeFalse(); + .withContext('not input:focus') + .toBeFalse(); }); }); @@ -205,23 +205,22 @@ describe('TextField', () => { expect(harness.element.value).toBe('Value'); }); - it('should render `value` instead of default value attribute when `value` changes', - async () => { - const {harness, input} = await setupTest(); + it('should render `value` instead of default value attribute when `value` changes', async () => { + const {harness, input} = await setupTest(); - harness.element.setAttribute('value', 'Default'); - await env.waitForStability(); - expect(input.value).toBe('Default'); + harness.element.setAttribute('value', 'Default'); + await env.waitForStability(); + expect(input.value).toBe('Default'); - harness.element.value = 'Value'; - await env.waitForStability(); - expect(input.value).toBe('Value'); + harness.element.value = 'Value'; + await env.waitForStability(); + expect(input.value).toBe('Value'); - harness.element.value = ''; - await env.waitForStability(); - expect(input.value).toBe(''); - expect(harness.element.getAttribute('value')).toBe('Default'); - }); + harness.element.value = ''; + await env.waitForStability(); + expect(input.value).toBe(''); + expect(harness.element.getAttribute('value')).toBe('Default'); + }); }); describe('valueAsDate', () => { @@ -261,8 +260,11 @@ describe('TextField', () => { describe('valueAsNumber', () => { it('should get input.valueAsNumber', async () => { const {testElement, input} = await setupTest(); - const spy = - spyOnProperty(input, 'valueAsNumber', 'get').and.callThrough(); + const spy = spyOnProperty( + input, + 'valueAsNumber', + 'get', + ).and.callThrough(); expect(testElement.valueAsNumber).toEqual(NaN); @@ -273,8 +275,11 @@ describe('TextField', () => { const {testElement, input} = await setupTest(); testElement.type = 'number'; await env.waitForStability(); - const spy = - spyOnProperty(input, 'valueAsNumber', 'set').and.callThrough(); + const spy = spyOnProperty( + input, + 'valueAsNumber', + 'set', + ).and.callThrough(); testElement.valueAsNumber = 100; @@ -342,8 +347,8 @@ describe('TextField', () => { await harness.deleteValue(); expect(isValidDuringInput) - .withContext('validity.valid result during input event') - .toBeFalse(); + .withContext('validity.valid result during input event') + .toBeFalse(); }); it('should return validity during change event', async () => { @@ -354,16 +359,20 @@ describe('TextField', () => { await harness.blur(); let isValidDuringChange = true; - testElement.addEventListener('change', () => { - isValidDuringChange = testElement.validity.valid; - }, {once: true}); + testElement.addEventListener( + 'change', + () => { + isValidDuringChange = testElement.validity.valid; + }, + {once: true}, + ); await harness.deleteValue(); await harness.blur(); expect(isValidDuringChange) - .withContext('validity.valid result during change event') - .toBeFalse(); + .withContext('validity.valid result during change event') + .toBeFalse(); }); }); @@ -375,8 +384,8 @@ describe('TextField', () => { expect(valid).withContext('valid').toBeTrue(); expect(await testElement.getHasError()) - .withContext('testElement.getHasError()') - .toBeFalse(); + .withContext('testElement.getHasError()') + .toBeFalse(); }); it('should return false when invalid and set error to true', async () => { @@ -387,8 +396,8 @@ describe('TextField', () => { expect(valid).withContext('valid').toBeFalse(); expect(await testElement.getHasError()) - .withContext('testElement.getHasError()') - .toBeTrue(); + .withContext('testElement.getHasError()') + .toBeTrue(); }); it('should update error text to validationMessage', async () => { @@ -402,23 +411,22 @@ describe('TextField', () => { expect(await testElement.getErrorTextValue()).toEqual(errorMessage); }); - it('should not update error or error text if invalid event is canceled', - async () => { - const {testElement} = await setupTest(); - testElement.addEventListener('invalid', e => { - e.preventDefault(); - }); - const errorMessage = 'Error message'; - testElement.setCustomValidity(errorMessage); + it('should not update error or error text if invalid event is canceled', async () => { + const {testElement} = await setupTest(); + testElement.addEventListener('invalid', (e) => { + e.preventDefault(); + }); + const errorMessage = 'Error message'; + testElement.setCustomValidity(errorMessage); - const valid = testElement.reportValidity(); + const valid = testElement.reportValidity(); - expect(valid).withContext('valid').toBeFalse(); - expect(await testElement.getHasError()) - .withContext('testElement.getHasError()') - .toBeFalse(); - expect(await testElement.getErrorTextValue()).toEqual(''); - }); + expect(valid).withContext('valid').toBeFalse(); + expect(await testElement.getHasError()) + .withContext('testElement.getHasError()') + .toBeFalse(); + expect(await testElement.getErrorTextValue()).toEqual(''); + }); it('should be overridden by error and errorText', async () => { const {testElement} = await setupTest(); @@ -429,8 +437,8 @@ describe('TextField', () => { const valid = testElement.reportValidity(); expect(valid).withContext('native validity should be valid').toBeTrue(); expect(await testElement.getHasError()) - .withContext('testElement.getHasError()') - .toBeTrue(); + .withContext('testElement.getHasError()') + .toBeTrue(); expect(await testElement.getErrorTextValue()).toEqual(errorMessage); }); }); @@ -442,8 +450,8 @@ describe('TextField', () => { const errorMessage = 'Error message'; testElement.setCustomValidity(errorMessage); expect(testElement.validationMessage) - .withContext('validationMessage') - .toEqual(errorMessage); + .withContext('validationMessage') + .toEqual(errorMessage); }); }); @@ -455,11 +463,11 @@ describe('TextField', () => { await env.waitForStability(); expect(input.getAttribute('minLength')) - .withContext('minLength') - .toEqual('2'); + .withContext('minLength') + .toEqual('2'); expect(input.getAttribute('maxLength')) - .withContext('maxLength') - .toEqual('5'); + .withContext('maxLength') + .toEqual('5'); }); it('should not set attribute if value is -1', async () => { @@ -469,22 +477,22 @@ describe('TextField', () => { await env.waitForStability(); expect(input.hasAttribute('minlength')) - .withContext('should have minlength') - .toBeTrue(); + .withContext('should have minlength') + .toBeTrue(); expect(input.hasAttribute('maxlength')) - .withContext('should have maxlength') - .toBeTrue(); + .withContext('should have maxlength') + .toBeTrue(); testElement.minLength = -1; testElement.maxLength = -1; await env.waitForStability(); expect(input.hasAttribute('minlength')) - .withContext('should not have minlength') - .toBeFalse(); + .withContext('should not have minlength') + .toBeFalse(); expect(input.hasAttribute('maxlength')) - .withContext('should not have maxlength') - .toBeFalse(); + .withContext('should not have maxlength') + .toBeFalse(); }); }); @@ -511,14 +519,14 @@ describe('TextField', () => { await env.waitForStability(); expect(input.hasAttribute('min')) - .withContext('should have min') - .toBeTrue(); + .withContext('should have min') + .toBeTrue(); expect(input.hasAttribute('max')) - .withContext('should have max') - .toBeTrue(); + .withContext('should have max') + .toBeTrue(); expect(input.hasAttribute('step')) - .withContext('should have step') - .toBeTrue(); + .withContext('should have step') + .toBeTrue(); testElement.min = ''; testElement.max = ''; @@ -526,14 +534,14 @@ describe('TextField', () => { await env.waitForStability(); expect(input.hasAttribute('min')) - .withContext('should not have min') - .toBeFalse(); + .withContext('should not have min') + .toBeFalse(); expect(input.hasAttribute('max')) - .withContext('should not have max') - .toBeFalse(); + .withContext('should not have max') + .toBeFalse(); expect(input.hasAttribute('step')) - .withContext('should not have step') - .toBeFalse(); + .withContext('should not have step') + .toBeFalse(); }); }); @@ -544,8 +552,8 @@ describe('TextField', () => { await env.waitForStability(); expect(input.getAttribute('pattern')) - .withContext('pattern') - .toEqual('foo'); + .withContext('pattern') + .toEqual('foo'); }); it('should not set attribute if value is empty', async () => { @@ -554,15 +562,15 @@ describe('TextField', () => { await env.waitForStability(); expect(input.hasAttribute('pattern')) - .withContext('should have pattern') - .toBeTrue(); + .withContext('should have pattern') + .toBeTrue(); testElement.pattern = ''; await env.waitForStability(); expect(input.hasAttribute('pattern')) - .withContext('should not have pattern') - .toBeFalse(); + .withContext('should not have pattern') + .toBeFalse(); }); }); }); @@ -595,85 +603,94 @@ describe('TextField', () => { describe('forms', () => { createFormTests({ - queryControl: root => root.querySelector('md-test-text-field'), + queryControl: (root) => root.querySelector('md-test-text-field'), valueTests: [ { name: 'unnamed', render: () => - html``, + html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form without a name') - .toHaveSize(0); - } + .withContext('should not add anything to form without a name') + .toHaveSize(0); + }, }, { name: 'should add empty value', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('input')).toBe(''); - } + }, }, { name: 'with value', render: () => - html``, + html``, assertValue(formData) { expect(formData.get('input')).toBe('Value'); - } + }, }, { name: 'disabled', render: () => - html``, + html``, assertValue(formData) { expect(formData) - .withContext('should not add anything to form when disabled') - .toHaveSize(0); - } - } + .withContext('should not add anything to form when disabled') + .toHaveSize(0); + }, + }, ], resetTests: [ { name: 'reset to empty value', render: () => - html``, + html``, change(textField) { textField.value = 'Value'; }, assertReset(textField) { expect(textField.value) - .withContext('textField.value after reset') - .toBe(''); - } + .withContext('textField.value after reset') + .toBe(''); + }, }, { name: 'reset value', render: () => - html``, + html``, change(textField) { textField.value = 'Second'; }, assertReset(textField) { expect(textField.value) - .withContext('textField.value after reset') - .toBe('First'); - } + .withContext('textField.value after reset') + .toBe('First'); + }, }, ], restoreTests: [ { name: 'restore value', render: () => - html``, + html``, assertRestored(textField) { expect(textField.value) - .withContext('textField.value after restore') - .toBe('Value'); - } + .withContext('textField.value after restore') + .toBe('Value'); + }, }, - ] + ], }); }); }); diff --git a/textfield/outlined-text-field.ts b/textfield/outlined-text-field.ts index 3f48c2870..938f25fa2 100644 --- a/textfield/outlined-text-field.ts +++ b/textfield/outlined-text-field.ts @@ -29,8 +29,11 @@ declare global { */ @customElement('md-outlined-text-field') export class MdOutlinedTextField extends OutlinedTextField { - static override styles = - [sharedStyles, outlinedStyles, outlinedForcedColorsStyles]; + static override styles = [ + sharedStyles, + outlinedStyles, + outlinedForcedColorsStyles, + ]; protected override readonly fieldTag = literal`md-outlined-field`; } diff --git a/web-test-runner.config.js b/web-test-runner.config.js index f300e6615..3bb3c9603 100644 --- a/web-test-runner.config.js +++ b/web-test-runner.config.js @@ -11,13 +11,14 @@ export default { ...jasmineTestRunnerConfig(), nodeResolve: true, files: ['**/*test.js', '!node_modules/', '!.wireit/'], - browsers: [ - playwrightLauncher({ - product: 'chromium', - // TODO Firefox errors with "Touch is not defined" - // product: 'firefox', - // TODO Webkit errors with "Unknown error" - // product: 'webkit', - }), - ], + browsers: + [ + playwrightLauncher({ + product: 'chromium', + // TODO Firefox errors with "Touch is not defined" + // product: 'firefox', + // TODO Webkit errors with "Unknown error" + // product: 'webkit', + }), + ], };