chore: format files with prettier

PiperOrigin-RevId: 576601342
This commit is contained in:
Elizabeth Mitchell 2023-10-25 11:58:21 -07:00 committed by Copybara-Service
parent d09bdc47e6
commit c390291687
216 changed files with 6352 additions and 5081 deletions

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('Button', [
new Knob('label', {ui: textInput(), defaultValue: ''}),
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
]);
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
'Button',
[
new Knob('label', {ui: textInput(), defaultValue: ''}),
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
],
);
collection.addStories(...materialInitsToStoryInits(stories));

View File

@ -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<StoryKnobs> = {
</div>
</div>
`;
}
},
};
const links: MaterialStoryInit<StoryKnobs> = {
@ -101,40 +101,35 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-filled-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
${label || 'Filled'}
</md-filled-button>
<md-outlined-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
${label || 'Outlined'}
</md-outlined-button>
<md-elevated-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
${label || 'Elevated'}
</md-elevated-button>
<md-filled-tonal-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
${label || 'Tonal'}
</md-filled-tonal-button>
<md-text-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
${label || 'Text'}
</md-text-button>
</div>
@ -142,8 +137,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-filled-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
<md-icon slot="icon">open_in_new</md-icon>
${label || 'Filled'}
</md-filled-button>
@ -151,8 +145,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-outlined-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
<md-icon slot="icon">open_in_new</md-icon>
${label || 'Outlined'}
</md-outlined-button>
@ -160,8 +153,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-elevated-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
<md-icon slot="icon">open_in_new</md-icon>
${label || 'Elevated'}
</md-elevated-button>
@ -169,8 +161,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-filled-tonal-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
<md-icon slot="icon">open_in_new</md-icon>
${label || 'Tonal'}
</md-filled-tonal-button>
@ -178,15 +169,14 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-text-button
href="https://google.com"
target="_blank"
trailing-icon
>
trailing-icon>
<md-icon slot="icon">open_in_new</md-icon>
${label || 'Text'}
</md-text-button>
</div>
</div>
`;
}
},
};
/** Button stories. */

View File

@ -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,
];
}

View File

@ -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`<slot name="icon" @slotchange="${this.handleSlotChange}"></slot>`;
const icon = html`<slot
name="icon"
@slotchange="${this.handleSlotChange}"></slot>`;
return html`
${this.renderElevation?.()}
${this.renderOutline?.()}
${this.renderElevation?.()} ${this.renderOutline?.()}
<md-focus-ring part="focus-ring"></md-focus-ring>
<md-ripple class="button__ripple" ?disabled="${isDisabled}"></md-ripple>
<span class="touch"></span>
@ -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();

View File

@ -57,4 +57,4 @@ export class CatalogComponentHeaderTitle extends LitElement {
}
}
`;
}
}

View File

@ -17,13 +17,10 @@ import {customElement} from 'lit/decorators.js';
@customElement('catalog-component-header')
export class CatalogComponentHeader extends LitElement {
render() {
return html`
<div>
<slot class="title" name="title"></slot>
<slot
class="image"
></slot>
</div>`;
return html` <div>
<slot class="title" name="title"></slot>
<slot class="image"></slot>
</div>`;
}
static styles = css`

View File

@ -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}>
<md-icon>content_copy</md-icon>
<md-icon slot="selected">checkmark</md-icon>
</md-icon-button>

View File

@ -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`<section>
<span id="label" class="color-on-surface-text">${this.label}</span>
<md-slider
id="source"
labeled
aria-label=${this.label}
.min=${range[0]}
.max=${range[1]}
.value=${this.value}
@input=${this.onInput}
></md-slider>
id="source"
labeled
aria-label=${this.label}
.min=${range[0]}
.max=${range[1]}
.value=${this.value}
@input=${this.onInput}></md-slider>
<div
id="gradient"
class=${this.type}
style=${styleMap({
background: this.buildGradient(),
})}
></div>
background: this.buildGradient(),
})}></div>
</section>`;
}
@ -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);

View File

@ -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`
<div class="root">
@ -65,8 +66,7 @@ import {SignalElement} from '../signals/signal-element.js';
},
in: fadeIn,
out: fadeOut,
})}
></div>`
})}></div>`
: nothing}
<aside
?inert=${this.isCollapsible && !drawerOpenSignal.value}
@ -76,18 +76,16 @@ import {SignalElement} from '../signals/signal-element.js';
duration: drawerSlideAnimationDuration,
easing: drawerSlideAnimationEasing,
},
})}
>
<div class="scroll-wrapper">
<slot
${animate({
properties: ['opacity'],
keyframeOptions: {
duration: drawerContentOpacityDuration,
easing: 'linear',
},
})}
></slot>
})}>
<div class="scroll-wrapper">
<slot
${animate({
properties: ['opacity'],
keyframeOptions: {
duration: drawerContentOpacityDuration,
easing: 'linear',
},
})}></slot>
</div>
</aside>
</div>
@ -102,8 +100,7 @@ import {SignalElement} from '../signals/signal-element.js';
private renderContent(showModal: boolean) {
return html` <div
class="pane content-pane"
?inert=${showModal || inertContentSignal.value}
>
?inert=${showModal || inertContentSignal.value}>
<div class="scroll-wrapper">
<div class="content">
<slot name="app-content"></slot>
@ -119,8 +116,7 @@ import {SignalElement} from '../signals/signal-element.js';
return html` <div
class="pane toc"
?inert=${showModal || inertContentSignal.value}
>
?inert=${showModal || inertContentSignal.value}>
<div class="scroll-wrapper">
<p>On this page:</p>
<h2>${this.pageTitle}</h2>
@ -148,11 +144,16 @@ import {SignalElement} from '../signals/signal-element.js';
updated(changed: PropertyValues<this>) {
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();
}
}

View File

@ -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`
<div id="head-wrapper">
<h2>
Theme Controls
</h2>
<h2> Theme Controls </h2>
<copy-code-button
button-title="Copy current theme to clipboard"
label="Copy current theme"
.getCopyText=${getCurrentThemeString}>
button-title="Copy current theme to clipboard"
label="Copy current theme"
.getCopyText=${getCurrentThemeString}>
</copy-code-button>
</div>
${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)} />
</div>
<md-focus-ring for="color-input"></md-focus-ring>
</span>
@ -111,27 +111,24 @@ export class ThemeChanger extends LitElement {
private renderHctPicker() {
return html`<div class="sliders">
<hct-slider
.value=${live(this.hue)}
type="hue"
label="Hue"
max="360"
@input=${this.onSliderInput}
></hct-slider>
.value=${live(this.hue)}
type="hue"
label="Hue"
max="360"
@input=${this.onSliderInput}></hct-slider>
<hct-slider
.value=${live(this.chroma)}
.color=${this.hexColor}
type="chroma"
label="Chroma"
max="150"
@input=${this.onSliderInput}
></hct-slider>
.value=${live(this.chroma)}
.color=${this.hexColor}
type="chroma"
label="Chroma"
max="150"
@input=${this.onSliderInput}></hct-slider>
<hct-slider
.value=${live(this.tone)}
type="tone"
label="Tone"
max="100"
@input=${this.onSliderInput}
></hct-slider>
.value=${live(this.tone)}
type="tone"
label="Tone"
max="100"
@input=${this.onSliderInput}></hct-slider>
</div>`;
}
@ -140,9 +137,8 @@ export class ThemeChanger extends LitElement {
*/
private renderColorModePicker() {
return html`<md-outlined-segmented-button-set
@segmented-button-set-selection=${this.onColorModeSelection}
aria-label="Color mode"
>
@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}>
<md-icon slot="icon">${icon}</md-icon>
</md-outlined-segmented-button>`;
}
@ -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;

View File

@ -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}>
<md-icon slot="selected">menu</md-icon>
<md-icon>menu_open</md-icon>
</md-icon-button>
@ -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}
</md-icon-button>
</section>
@ -71,16 +70,14 @@ import {materialDesign} from '../svg/material-design-logo.js';
<lit-island
on:interaction="pointerenter,focusin,pointerdown"
import="/js/hydration-entrypoints/menu.js"
id="menu-island"
>
id="menu-island">
<md-icon-button
id="theme-button"
@click="${this.onPaletteClick}"
title="Page theme controls"
aria-label="Page theme controls"
aria-haspopup="dialog"
aria-expanded=${this.menuOpen ? 'true' : 'false'}
>
aria-expanded=${this.menuOpen ? 'true' : 'false'}>
<md-icon>palette</md-icon>
</md-icon-button>
<md-menu
@ -93,8 +90,7 @@ import {materialDesign} from '../svg/material-design-logo.js';
.open=${this.menuOpen}
@opened=${this.onMenuOpened}
@closed=${this.onMenuClosed}
@keydown=${this.onKeydown}
>
@keydown=${this.onKeydown}>
<theme-changer></theme-changer>
</md-menu>
</lit-island>

View File

@ -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';
import '@material/web/button/text-button.js';

View File

@ -4,4 +4,4 @@
* SPDX-License-Identifier: Apache-2.0
*/
import '@material/web/checkbox/checkbox.js';
import '@material/web/checkbox/checkbox.js';

View File

@ -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';

View File

@ -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';

View File

@ -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';
import '@material/web/menu/sub-menu.js';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';
import 'playground-elements/playground-project.js';

View File

@ -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

View File

@ -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()!);
});
}
/**

View File

@ -9,4 +9,4 @@ import {signal} from './signal-element.js';
/**
* Whether or not the sidebar drawer should be open.
*/
export const drawerOpenSignal = signal(false);
export const drawerOpenSignal = signal(false);

View File

@ -15,4 +15,4 @@ export const inertContentSignal = signal(false);
/**
* Whether or not the sidebar should be inert.
*/
export const inertSidebarSignal = signal(false);
export const inertSidebarSignal = signal(false);

View File

@ -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<T extends ReactiveElementConstructor>(Base: T):
T {
export function SignalElement<T extends ReactiveElementConstructor>(
Base: T,
): T {
return class SignalElement extends Base {
private _disposeEffect?: () => void;
@ -39,4 +40,4 @@ export function SignalElement<T extends ReactiveElementConstructor>(Base: T):
});
}
};
}
}

View File

@ -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');
document.body.removeAttribute('dsd-pending');

View File

@ -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';
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';

View File

@ -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() {}
},
);

View File

@ -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';
import '@material/web/labs/item/item.js';

View File

@ -11,8 +11,7 @@ import {html} from 'lit';
*
* Source: Internal google symbols search.
*/
export const materialDesign = html`
<svg
export const materialDesign = html` <svg
viewBox="0 96 960 960"
fill="currentColor">
<path

View File

@ -32,7 +32,7 @@ export class ChangeDarkModeEvent extends Event {
/**
* @param mode The new color mode to apply.
*/
constructor(public mode: 'light'|'dark'|'auto') {
constructor(public mode: 'light' | 'dark' | 'auto') {
super('change-mode', {bubbles: true, composed: true});
}
}

View File

@ -8,4 +8,4 @@ declare module '@11ty/is-land' {
export class Island extends HTMLElement {
forceFallback(): void;
}
}
}

View File

@ -4,8 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
type WithStylesheet =
typeof globalThis&{[stylesheetName: string]: CSSStyleSheet | undefined};
type WithStylesheet = typeof globalThis & {
[stylesheetName: string]: CSSStyleSheet | undefined;
};
/**
* Applies a stringified CSS theme to a document or shadowroot by creating or
@ -23,7 +24,10 @@ type WithStylesheet =
* used to generate the localstorage name.
*/
export function applyThemeString(
doc: DocumentOrShadowRoot, themeString: string, ssName = 'material-theme') {
doc: DocumentOrShadowRoot,
themeString: string,
ssName = 'material-theme',
) {
// Get constructable stylesheet
let sheet = (globalThis as WithStylesheet)[ssName];
// Create a new sheet if it doesn't exist already and save it globally.
@ -34,11 +38,13 @@ export function applyThemeString(
}
// Set the color of the URL bar because we are cool like that.
const surfaceContainer =
themeString.match(/--md-sys-color-surface-container:(.+?);/)?.[1];
const surfaceContainer = themeString.match(
/--md-sys-color-surface-container:(.+?);/,
)?.[1];
if (surfaceContainer) {
document.querySelector('meta[name="theme-color"]')
?.setAttribute('content', surfaceContainer);
document
.querySelector('meta[name="theme-color"]')
?.setAttribute('content', surfaceContainer);
}
sheet.replaceSync(themeString);

View File

@ -4,7 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {argbFromHex, Hct, hexFromArgb, MaterialDynamicColors, SchemeContent} from '@material/material-color-utilities';
import {
argbFromHex,
Hct,
hexFromArgb,
MaterialDynamicColors,
SchemeContent,
} from '@material/material-color-utilities';
import type {Theme} from '../types/color-events.js';
@ -110,7 +116,10 @@ export function themeFromSourceColor(color: string, isDark: boolean): Theme {
* used to generate the localstorage name.
*/
export function applyMaterialTheme(
doc: DocumentOrShadowRoot, theme: Theme, ssName = 'material-theme') {
doc: DocumentOrShadowRoot,
theme: Theme,
ssName = 'material-theme',
) {
let styleString = ':root,:host{';
for (const [key, value] of Object.entries(theme)) {
styleString += `--md-sys-color-${key}:${value};`;

View File

@ -4,9 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {applyMaterialTheme, themeFromSourceColor} from './material-color-helpers.js';
import {
applyMaterialTheme,
themeFromSourceColor,
} from './material-color-helpers.js';
export type ColorMode = 'light'|'dark'|'auto';
/** Color mode, either overriding light/dark or the user's preference. */
export type ColorMode = 'light' | 'dark' | 'auto';
/**
* Generates a Material Theme from a given color and dark mode boolean, and
@ -56,7 +60,7 @@ export function isModeDark(mode: ColorMode, saveAutoMode = true) {
*
* @return The current stringified material theme css string.
*/
export function getCurrentThemeString(): string|null {
export function getCurrentThemeString(): string | null {
return localStorage.getItem('material-theme');
}
@ -65,7 +69,7 @@ export function getCurrentThemeString(): string|null {
*
* @return The current color mode.
*/
export function getCurrentMode(): ColorMode|null {
export function getCurrentMode(): ColorMode | null {
return localStorage.getItem('color-mode') as ColorMode | null;
}
@ -83,7 +87,7 @@ export function saveColorMode(mode: ColorMode) {
*
* @return The current seed color.
*/
export function getCurrentSeedColor(): string|null {
export function getCurrentSeedColor(): string | null {
return localStorage.getItem('seed-color');
}
@ -102,8 +106,10 @@ export function saveSeedColor(color: string) {
* @return The last applied color mode while in "auto".
*/
export function getLastSavedAutoColorMode() {
return localStorage.getItem('last-auto-color-mode') as | 'light' | 'dark' |
null;
return localStorage.getItem('last-auto-color-mode') as
| 'light'
| 'dark'
| null;
}
/**
@ -112,7 +118,7 @@ export function getLastSavedAutoColorMode() {
* @param mode The last applied color mode while in "auto" to be saved to local
* storage.
*/
export function saveLastSavedAutoColorMode(mode: 'light'|'dark') {
export function saveLastSavedAutoColorMode(mode: 'light' | 'dark') {
localStorage.setItem('last-auto-color-mode', mode);
}

View File

@ -8,10 +8,10 @@
import '@material/web/button/outlined-button.js';
import '@material/web/checkbox/checkbox.js';
import '@material/web/radio/radio.js';
import '@material/web/ripple/ripple.js';
import '@material/web/select/filled-select.js';
import '@material/web/select/select-option.js';
import '@material/web/textfield/filled-text-field.js';
import '@material/web/ripple/ripple.js';
import {css, html, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@ -36,8 +36,7 @@ export function boolInput(): KnobUi<boolean> {
touch-target="none"
style="margin-inline-end: 16px;"
.checked=${!!knob.latestValue}
@change="${valueChanged}"
>
@change="${valueChanged}">
</md-checkbox>
${knob.name}
</label>
@ -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 {
</span>
<md-outlined-button
@click=${() => {
this.hasAlpha = !this.hasAlpha;
}}
>
this.hasAlpha = !this.hasAlpha;
}}>
${this.hasAlpha ? 'rgba' : 'rgb'}
</md-outlined-button>
</span>`;
@ -139,8 +143,7 @@ export class KnobColorSelector extends LitElement {
style=${styleMap(sharedTextFieldStyles)}
.value=${this.value}
@change=${this.propagateEvt}
@input=${this.onInput}
></md-filled-text-field>`;
@input=${this.onInput}></md-filled-text-field>`;
}
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<string> {
<knob-color-selector
.value="${knob.latestValue ?? ''}"
.hasAlpha="${config.hasAlpha}"
@input=${valueChanged}
></knob-color-selector>
@input=${valueChanged}></knob-color-selector>
${knob.name}
</label>
</div>
@ -269,8 +272,7 @@ export function textInput<T>(options?: TextInputOptions<T>): KnobUi<T> {
<md-filled-text-field
style=${styleMap(sharedTextFieldStyles)}
.value="${(knob.latestValue ?? '') as unknown as string}"
@input="${valueChanged}"
></md-filled-text-field>
@input="${valueChanged}"></md-filled-text-field>
${knob.name}
</label>
</div>
@ -311,8 +313,7 @@ export function numberInput(opts?: NumberInputOpts): KnobUi<number> {
type="number"
step="${config.step}"
.value="${knob.latestValue ? knob.latestValue.toString() : '0'}"
@input="${valueChanged}"
></md-filled-text-field>
@input="${valueChanged}"></md-filled-text-field>
${knob.name}
</label>
</div>
@ -343,7 +344,7 @@ export function button(): KnobUi<number> {
}
interface RadioSelectorConfig<T extends string> {
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<T extends string>({
name="${name}"
value="${value}"
@change="${valueChanged}"
?checked="${knob.latestValue === option.value}"
></md-radio>
?checked="${knob.latestValue === option.value}"></md-radio>
${option.label}
</label>`;
});
@ -375,15 +375,15 @@ export function radioSelector<T extends string>({
}
interface SelectDropdownConfig<T extends string> {
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<T extends string>({
options,
}: SelectDropdownConfig<T>): KnobUi<T|undefined> {
}: SelectDropdownConfig<T>): KnobUi<T | undefined> {
return {
render(knob: Knob<T|undefined>, onChange: (val: T) => void) {
render(knob: Knob<T | undefined>, onChange: (val: T) => void) {
const valueChanged = (e: Event) => {
onChange((e.target as HTMLInputElement).value as T);
};
@ -391,15 +391,14 @@ export function selectDropdown<T extends string>({
return html`<md-select-option
?selected="${knob.latestValue === option.value}"
.value="${option.value}"
.headline=${option.label}
></md-select-option>`;
.headline=${option.label}></md-select-option>`;
});
return html`
<label>
<md-filled-select
@change="${valueChanged}"
menu-positioning="fixed"
style=${styleMap(sharedTextFieldStyles)}>
@change="${valueChanged}"
menu-positioning="fixed"
style=${styleMap(sharedTextFieldStyles)}>
${listItems}
</md-filled-select>
${knob.name}
@ -414,7 +413,10 @@ export function selectDropdown<T extends string>({
* to the updated value.
*/
export function cssCustomProperty(
knob: Knob<string>, val: string, containerOfRenderedStory: HTMLElement) {
knob: Knob<string>,
val: string,
containerOfRenderedStory: HTMLElement,
) {
const value = knob.isUnset ? knob.defaultValue : val;
if (value) {
containerOfRenderedStory.style.setProperty(knob.name, value);

View File

@ -66,10 +66,10 @@ export class StoriesRenderer extends LitElement {
@property({type: Boolean}) hideLabels = false;
@property({type: Boolean, reflect: true}) hasKnobs = false;
@state() knobsOpen = true;
@state() knobsPanelType: 'modal'|'inline' = 'inline';
@state() knobsPanelType: 'modal' | 'inline' = 'inline';
private observedKnobs: undefined|KnobValues<PolymorphicArrayOfKnobs> =
undefined;
private observedKnobs: undefined | KnobValues<PolymorphicArrayOfKnobs> =
undefined;
override render() {
const collection = this.collection;
@ -90,11 +90,12 @@ export class StoriesRenderer extends LitElement {
private renderStories(stories: Story[]): TemplateResult[] {
return stories.map((story) => {
let label: string|TemplateResult = '';
let label: string | TemplateResult = '';
if (!this.hideLabels) {
const description =
story.description ? html`<small>${story.description}</small>` : '';
const description = story.description
? html`<small>${story.description}</small>`
: '';
label = html`
<h3 class="m-headline5">${story.name}</h3>
${description}
@ -113,7 +114,7 @@ export class StoriesRenderer extends LitElement {
private renderKnobs(collection: Collection) {
const knobs = collection.knobs;
let knobsSection: string|TemplateResult = '';
let knobsSection: string | TemplateResult = '';
this.hasKnobs = !this.hideKnobs && !knobs.empty;
@ -126,8 +127,7 @@ export class StoriesRenderer extends LitElement {
<story-knob-panel
.open=${this.knobsOpen}
.type=${this.knobsPanelType}
@open-changed=${onOpenChanged}
>
@open-changed=${onOpenChanged}>
${knobs.renderUI()}
</story-knob-panel>
`;
@ -163,7 +163,7 @@ export class StoriesRenderer extends LitElement {
private updateObservedKnobs() {
if (this.collection?.knobs === this.observedKnobs) {
return; // nothing to do;
return; // nothing to do;
}
// Stop watching the knobs that we're currently observing.
this.unobserveKnobs();
@ -183,8 +183,9 @@ export class StoriesRenderer extends LitElement {
for (const story of this.focusStories) {
if (!allowedStories.has(story)) {
console.error(
`A stories renderer can only render stories ` +
`from its collection.`);
`A stories renderer can only render stories ` +
`from its collection.`,
);
} else {
storiesToRender.push(story);
}
@ -199,7 +200,9 @@ export class StoriesRenderer extends LitElement {
private unobserveKnobs() {
if (this.observedKnobs !== undefined) {
this.observedKnobs.removeEventListener(
'changed', this.boundRequestUpdate);
'changed',
this.boundRequestUpdate,
);
}
}

View File

@ -6,9 +6,9 @@
/* Fork of Lit stories story-knob-renderer with m3 components and theming */
import '@material/web/iconbutton/icon-button.js';
import '@material/web/icon/icon.js';
import '@material/web/elevation/elevation.js';
import '@material/web/icon/icon.js';
import '@material/web/iconbutton/icon-button.js';
import {css, html, LitElement, PropertyValues} from 'lit';
import {customElement, property, query} from 'lit/decorators.js';
@ -29,13 +29,13 @@ export const DEFAULT_DIMENSIONS = {
*/
@customElement('story-knob-panel')
export class StoryKnobPanel extends LitElement {
@query('.dragBar') dragBar!: HTMLElement|null;
@query('.dragBar') dragBar!: HTMLElement | null;
@property({type: Boolean}) showCloseIcon = true;
@property({type: Boolean, reflect: true}) open = false;
@property({type: Boolean, reflect: true}) override draggable = false;
@property({type: Boolean}) hideDragIcon = false;
@property({type: String, reflect: true}) type: 'modal'|'inline' = 'inline';
@property({type: String, reflect: true}) type: 'modal' | 'inline' = 'inline';
private isDragging = false;
private previousX = 0;
private currentX = 0;
@ -84,8 +84,7 @@ export class StoryKnobPanel extends LitElement {
class="dragBar"
@pointerdown=${this.onDragStart}
@pointermove=${this.onDrag}
@pointerup=${this.onDragEnd}
>
@pointerup=${this.onDragEnd}>
<md-icon>drag_handle</md-icon>
</div>
`;
@ -99,8 +98,7 @@ export class StoryKnobPanel extends LitElement {
<md-icon-button
class="dragIconButton"
aria-label=${iconLabel}
@click=${this.onDragIconClick}
>
@click=${this.onDragIconClick}>
<md-icon>${iconSvg}</md-icon>
</md-icon-button>
`;
@ -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) {

View File

@ -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<void>|undefined = undefined;
private storyRenderComplete: Promise<void> | undefined = undefined;
override updated(propertiesChanged: PropertyValues) {
super.updated(propertiesChanged);

View File

@ -10,4 +10,4 @@ export * from './knobs.js';
export * from './story.js';
// This file is resolved by base.json
import './components/stories-renderer.js';
import './components/stories-renderer.js';

View File

@ -53,8 +53,9 @@ export class Knob<T, Name extends string = string> extends EventTarget {
private readonly onReset = () => {
this.reset();
};
private readonly renderedStoryContainers =
new Set<HTMLElement|DocumentFragment>();
private readonly renderedStoryContainers = new Set<
HTMLElement | DocumentFragment
>();
constructor(readonly name: Name, init: KnobInit<T>) {
super();
@ -80,25 +81,28 @@ export class Knob<T, Name extends string = string> 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 extends PolymorphicArrayOfKnobs> = Knobs[number]['name'];
*
* This type operator would return `number`.
*/
type TypeOfKnobWithName<Knobs extends PolymorphicArrayOfKnobs,
SearchName extends string> =
Extract<Knobs[number], {name: SearchName}> extends Knob<infer U>?
U|undefined :
never;
type TypeOfKnobWithName<
Knobs extends PolymorphicArrayOfKnobs,
SearchName extends string,
> = Extract<Knobs[number], {name: SearchName}> extends Knob<infer U>
? U | undefined
: never;
/** A helper class for getting the latest value for a knob by name. */
export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> extends
EventTarget {
export class KnobValues<
Knobs extends PolymorphicArrayOfKnobs,
> extends EventTarget {
private readonly byName: ReadonlyMap<string, Knob<unknown>>;
constructor(knobsArray: PolymorphicArrayOfKnobs) {
@ -156,25 +162,32 @@ export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> 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<SearchName extends KnobKeys<Knobs>>(knobName: SearchName):
TypeOfKnobWithName<Knobs, SearchName> {
return this.byName.get(knobName)?.latestValue as
TypeOfKnobWithName<Knobs, SearchName>;
get<SearchName extends KnobKeys<Knobs>>(
knobName: SearchName,
): TypeOfKnobWithName<Knobs, SearchName> {
return this.byName.get(knobName)?.latestValue as TypeOfKnobWithName<
Knobs,
SearchName
>;
}
set<SearchName extends KnobKeys<Knobs>>(
knobName: SearchName, newValue: TypeOfKnobWithName<Knobs, SearchName>) {
knobName: SearchName,
newValue: TypeOfKnobWithName<Knobs, SearchName>,
) {
const knob = this.byName.get(knobName);
if (knob === undefined) {
throw new Error(`No knob with name ${knobName}`);
@ -210,7 +223,7 @@ export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> 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<Knobs extends PolymorphicArrayOfKnobs> 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<T> {
* 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<T>, onChange: (val: T) => void, onReset: () => void):
TemplateResult;
render(
knob: Knob<T>,
onChange: (val: T) => void,
onReset: () => void,
): TemplateResult;
}
/**
@ -276,6 +292,9 @@ export interface KnobUi<T> {
* wired up by just applying styles to the containerOfRenderedStory.
*/
export interface KnobWiring<T> {
(knob: Knob<T>, val: T,
containerOfRenderedStory: HTMLElement|DocumentFragment): void;
(
knob: Knob<T>,
val: T,
containerOfRenderedStory: HTMLElement | DocumentFragment,
): void;
}

View File

@ -52,11 +52,11 @@ export function title(): KnobUi<void> {
* ```
*/
export type KnobTypesToKnobs<
// tslint:disable-next-line:no-any No way to represent this type clearly.
T extends {[name: string]: any},
Names extends Extract<keyof T, string> = Extract<keyof T, string>,
// tslint:disable-next-line:no-any We need to "map" the union type to knobs.
> = ReadonlyArray<Names extends any ? Knob<T[Names], Names>: never>;
// tslint:disable-next-line:no-any No way to represent this type clearly.
T extends {[name: string]: any},
Names extends Extract<keyof T, string> = Extract<keyof T, string>,
// tslint:disable-next-line:no-any We need to "map" the union type to knobs.
> = ReadonlyArray<Names extends any ? Knob<T[Names], Names> : never>;
/**
* An init object for Material Stories. This should be exposed to the user.
@ -86,7 +86,7 @@ export type KnobTypesToKnobs<
export interface MaterialStoryInit<T extends {[name: string]: any}> {
name: string;
render: (knobs: T) => TemplateResult | Promise<TemplateResult>;
styles?: CSSResult|CSSResult[];
styles?: CSSResult | CSSResult[];
}
/**
@ -94,8 +94,8 @@ export interface MaterialStoryInit<T extends {[name: string]: any}> {
*/
// tslint:disable-next-line:no-any No way to represent this type clearly.
export function materialInitsToStoryInits<T extends {[name: string]: any}>(
inits: Array<MaterialStoryInit<T>>):
Array<LitStoryInit<KnobValues<KnobTypesToKnobs<T>>>> {
inits: Array<MaterialStoryInit<T>>,
): Array<LitStoryInit<KnobValues<KnobTypesToKnobs<T>>>> {
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);
}
}

View File

@ -24,22 +24,23 @@ export interface BaseStoryInit {
type GenericKnobValues = KnobValues<PolymorphicArrayOfKnobs>;
/** A story with an arbitrary render function. */
export interface StoryInit<
KV extends GenericKnobValues = GenericKnobValues> extends BaseStoryInit {
render(container: HTMLElement|DocumentFragment, knobs: KV): Promise<void>;
export interface StoryInit<KV extends GenericKnobValues = GenericKnobValues>
extends BaseStoryInit {
render(container: HTMLElement | DocumentFragment, knobs: KV): Promise<void>;
styles?: CSSStyleSheet[];
}
class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
PolymorphicArrayOfKnobs> {
class StoryImpl<
Knobs extends PolymorphicArrayOfKnobs = PolymorphicArrayOfKnobs,
> {
readonly name: string;
readonly id: string;
readonly description: string|undefined;
readonly render: (container: HTMLElement|DocumentFragment) => Promise<void>;
readonly dispose: (container: HTMLElement|DocumentFragment) => void;
readonly description: string | undefined;
readonly render: (container: HTMLElement | DocumentFragment) => Promise<void>;
readonly dispose: (container: HTMLElement | DocumentFragment) => void;
readonly knobs: KnobValues<Knobs>;
private readonly initStyles: CSSStyleSheet[]|undefined;
private readonly initStyles: CSSStyleSheet[] | undefined;
get styles() {
let styles = [...this.collection.customStyles];
@ -54,19 +55,22 @@ class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
this.description = init.description;
this.id = init.id || this.name.replace(/ /g, '_').replace(/,/g, '');
if (this.id.includes('/')) {
const message = `A story id can't contain a '/' character. ` +
`The name can, but if so you have to give an ` +
`explicit id that doesn't.`;
const message =
`A story id can't contain a '/' character. ` +
`The name can, but if so you have to give an ` +
`explicit id that doesn't.`;
throw new Error(message);
}
const wrapperDivMap =
new WeakMap<HTMLElement|DocumentFragment, HTMLDivElement>();
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<Knobs extends PolymorphicArrayOfKnobs =
await init.render(wrapperDiv, this.knobs);
};
this.dispose = (container: HTMLElement|DocumentFragment) => {
this.dispose = (container: HTMLElement | DocumentFragment) => {
wrapperDivMap.delete(container);
render(nothing, container);
};
@ -93,8 +97,9 @@ class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
* The type is exposed here, but the implementation isn't, because a Story
* must be constructed via a Collection.
*/
export type Story<Knobs extends PolymorphicArrayOfKnobs =
PolymorphicArrayOfKnobs> = StoryImpl<Knobs>;
export type Story<
Knobs extends PolymorphicArrayOfKnobs = PolymorphicArrayOfKnobs,
> = StoryImpl<Knobs>;
/**
* A tree of related stories and sub-collections.
@ -106,9 +111,10 @@ export type Story<Knobs extends PolymorphicArrayOfKnobs =
* either a toplevel collection for a named component, or it is a member of
* exactly one collection.
*/
export class Collection<T extends PolymorphicArrayOfKnobs =
ReadonlyArray<Knob<unknown>>> {
private readonly children = new Map<string, Story|Collection>();
export class Collection<
T extends PolymorphicArrayOfKnobs = ReadonlyArray<Knob<unknown>>,
> {
private readonly children = new Map<string, Story | Collection>();
readonly customStyles: CSSStyleSheet[] = [];
private static readonly collectionsByName = new Map<string, Collection>();
readonly knobs: KnobValues<T>;
@ -136,7 +142,7 @@ export class Collection<T extends PolymorphicArrayOfKnobs =
return stories;
}
get tree(): ReadonlyMap<string, Story<T>|Collection<T>> {
get tree(): ReadonlyMap<string, Story<T> | Collection<T>> {
return this.children;
}
@ -148,8 +154,9 @@ export class Collection<T extends PolymorphicArrayOfKnobs =
for (const init of inits) {
const story = new StoryImpl(init, this);
if (this.children.has(story.id)) {
const message = `A story or subcollection already exists with the id ${
JSON.stringify(story.id)}`;
const message = `A story or subcollection already exists with the id ${JSON.stringify(
story.id,
)}`;
// Don't throw an error, as that will disrupt live_reload / hot reload,
// by halting this module's initialization.
console.error(message);
@ -168,10 +175,10 @@ export class Collection<T extends PolymorphicArrayOfKnobs =
* Describes a single configuration of a specific web UI component.
*/
export interface LitStoryInit<
KV extends KnobValues<PolymorphicArrayOfKnobs> =
KnobValues<PolymorphicArrayOfKnobs>> extends BaseStoryInit {
renderLit(knobs: KV): TemplateResult|Promise<TemplateResult>;
litStyles?: CSSResult|CSSResult[];
KV extends KnobValues<PolymorphicArrayOfKnobs> = KnobValues<PolymorphicArrayOfKnobs>,
> extends BaseStoryInit {
renderLit(knobs: KV): TemplateResult | Promise<TemplateResult>;
litStyles?: CSSResult | CSSResult[];
}
function isLitStoryInit(init: Partial<LitStoryInit>): init is LitStoryInit {
@ -181,19 +188,21 @@ function isLitStoryInit(init: Partial<LitStoryInit>): init is LitStoryInit {
/**
* A collection with convenience methods for rendering lit-html templates.
*/
export class LitCollection<T extends PolymorphicArrayOfKnobs =
ReadonlyArray<Knob<unknown>>> extends
Collection<T> {
export class LitCollection<
T extends PolymorphicArrayOfKnobs = ReadonlyArray<Knob<unknown>>,
> extends Collection<T> {
override addStories(
...inits: Array<StoryInit<KnobValues<T>>|LitStoryInit<KnobValues<T>>>) {
...inits: Array<StoryInit<KnobValues<T>> | LitStoryInit<KnobValues<T>>>
) {
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,

View File

@ -44,7 +44,6 @@ const postdoc = new PostDoc({
onMessage,
});
await postdoc.handshake;
// Request the initial theme.

View File

@ -20,63 +20,68 @@ describe('<md-checkbox>', () => {
describe('forms', () => {
createFormTests({
queryControl: root => root.querySelector('md-checkbox'),
queryControl: (root) => root.querySelector('md-checkbox'),
valueTests: [
{
name: 'unnamed',
render: () => html`<md-checkbox checked></md-checkbox>`,
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`<md-checkbox name="checkbox"></md-checkbox>`,
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`<md-checkbox name="checkbox" checked></md-checkbox>`,
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
assertValue(formData) {
expect(formData.get('checkbox')).toBe('on');
}
},
},
{
name: 'checked custom value',
render: () =>
html`<md-checkbox name="checkbox" checked value="Custom value"></md-checkbox>`,
html`<md-checkbox
name="checkbox"
checked
value="Custom value"></md-checkbox>`,
assertValue(formData) {
expect(formData.get('checkbox')).toBe('Custom value');
}
},
},
{
name: 'indeterminate',
render: () =>
html`<md-checkbox name="checkbox" checked indeterminate></md-checkbox>`,
html`<md-checkbox
name="checkbox"
checked
indeterminate></md-checkbox>`,
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`<md-checkbox name="checkbox" checked disabled></md-checkbox>`,
html`<md-checkbox name="checkbox" checked disabled></md-checkbox>`,
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('<md-checkbox>', () => {
},
assertReset(checkbox) {
expect(checkbox.checked)
.withContext('checkbox.checked after reset')
.toBeFalse();
}
.withContext('checkbox.checked after reset')
.toBeFalse();
},
},
{
name: 'reset to checked',
render: () =>
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
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`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
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('<md-checkbox>', () => {
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
assertRestored(checkbox) {
expect(checkbox.checked)
.withContext('checkbox.checked after restore')
.toBeFalse();
}
.withContext('checkbox.checked after restore')
.toBeFalse();
},
},
{
name: 'restore checked',
render: () =>
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
assertRestored(checkbox) {
expect(checkbox.checked)
.withContext('checkbox.checked after restore')
.toBeTrue();
}
.withContext('checkbox.checked after restore')
.toBeTrue();
},
},
{
name: 'restore indeterminate',
render: () =>
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
assertRestored(checkbox) {
expect(checkbox.indeterminate)
.withContext('checkbox.indeterminate should not be restored')
.toBeFalse();
}
}
]
.withContext('checkbox.indeterminate should not be restored')
.toBeFalse();
},
},
],
});
});
});

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('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<KnobTypesToKnobs<StoryKnobs>>(
'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));

View File

@ -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<StoryKnobs> = {
?checked=${checked}
?disabled=${disabled}
?indeterminate=${indeterminate}
touch-target="wrapper"
></md-checkbox>
touch-target="wrapper"></md-checkbox>
`;
},
};
@ -53,8 +55,7 @@ const withLabels: MaterialStoryInit<StoryKnobs> = {
<md-checkbox
?disabled=${disabled}
aria-label="Cats"
touch-target="wrapper"
></md-checkbox>
touch-target="wrapper"></md-checkbox>
Cats
</label>
<label>
@ -62,8 +63,7 @@ const withLabels: MaterialStoryInit<StoryKnobs> = {
checked
?disabled=${disabled}
aria-label="dogs"
touch-target="wrapper"
></md-checkbox>
touch-target="wrapper"></md-checkbox>
Dogs
</label>
<label>
@ -71,8 +71,7 @@ const withLabels: MaterialStoryInit<StoryKnobs> = {
indeterminate
?disabled=${disabled}
aria-label="Birds"
touch-target="wrapper"
></md-checkbox>
touch-target="wrapper"></md-checkbox>
Birds
</label>
</div>

View File

@ -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<Checkbox>) {
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`
<div class="container ${containerClasses}">
<input type="checkbox"
<input
type="checkbox"
id="input"
aria-checked=${isIndeterminate ? 'mixed' : nothing}
aria-label=${ariaLabel || nothing}
@ -244,8 +252,7 @@ export class Checkbox extends LitElement {
?required=${this.required}
.indeterminate=${this.indeterminate}
.checked=${this.checked}
@change=${this.handleChange}
>
@change=${this.handleChange} />
<div class="outline"></div>
<div class="background"></div>
@ -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() {

View File

@ -23,7 +23,8 @@ describe('checkbox', () => {
const env = new Environment();
async function setupTest(
template = html`<md-test-checkbox></md-test-checkbox>`) {
template = html`<md-test-checkbox></md-test-checkbox>`,
) {
const element = env.render(template).querySelector('md-test-checkbox');
if (!element) {
throw new Error('Could not query rendered <md-test-checkbox>.');
@ -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<Checkbox> = {}) {
return await setupTest(html`
<form>
<md-test-checkbox
.checked=${propsInit.checked === true}
.disabled=${propsInit.disabled === true}
.name=${propsInit.name ?? ''}
.value=${propsInit.value ?? ''}
></md-test-checkbox>
</form>`);
return await setupTest(html` <form>
<md-test-checkbox
.checked=${propsInit.checked === true}
.disabled=${propsInit.disabled === true}
.name=${propsInit.name ?? ''}
.value=${propsInit.value ?? ''}></md-test-checkbox>
</form>`);
}
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`
<label>
<md-test-checkbox></md-test-checkbox>
</label>
`);
const label = (test.harness.element.getRootNode() as HTMLElement)
.querySelector<HTMLLabelElement>('label')!;
<label>
<md-test-checkbox></md-test-checkbox>
</label>
`);
const label = (
test.harness.element.getRootNode() as HTMLElement
).querySelector<HTMLLabelElement>('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();
});
});
});

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('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<KnobTypesToKnobs<StoryKnobs>>(
'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));

View File

@ -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<StoryKnobs> = {
<md-assist-chip
label=${label || 'Assist chip'}
?disabled=${disabled}
?elevated=${elevated}
></md-assist-chip>
?elevated=${elevated}></md-assist-chip>
<md-assist-chip
label=${label || 'Assist chip with icon'}
?disabled=${disabled}
?elevated=${elevated}
>
?elevated=${elevated}>
<md-icon slot="icon">local_laundry_service</md-icon>
</md-assist-chip>
<md-assist-chip
@ -67,16 +65,16 @@ const assist: MaterialStoryInit<StoryKnobs> = {
?elevated=${elevated}
href="https://google.com"
target="_blank"
>${GOOGLE_LOGO}</md-assist-chip>
>${GOOGLE_LOGO}</md-assist-chip
>
<md-assist-chip
label=${label || 'Disabled assist chip (focusable)'}
disabled
always-focusable
?elevated=${elevated}
></md-assist-chip>
?elevated=${elevated}></md-assist-chip>
</md-chip-set>
`;
}
},
};
const filters: MaterialStoryInit<StoryKnobs> = {
@ -85,36 +83,31 @@ const filters: MaterialStoryInit<StoryKnobs> = {
render({label, elevated, disabled, scrolling}) {
const classes = {'scrolling': scrolling};
return html`
<md-chip-set class=${classMap(classes)}
aria-label="Filter chips">
<md-chip-set class=${classMap(classes)} aria-label="Filter chips">
<md-filter-chip
label=${label || 'Filter chip'}
?disabled=${disabled}
?elevated=${elevated}
></md-filter-chip>
?elevated=${elevated}></md-filter-chip>
<md-filter-chip
label=${label || 'Filter chip with icon'}
?disabled=${disabled}
?elevated=${elevated}
>
?elevated=${elevated}>
<md-icon slot="icon">local_laundry_service</md-icon>
</md-filter-chip>
<md-filter-chip
label=${label || 'Removable filter chip'}
?disabled=${disabled}
?elevated=${elevated}
removable
></md-filter-chip>
removable></md-filter-chip>
<md-filter-chip
label=${label || 'Disabled filter chip (focusable)'}
disabled
always-focusable
?elevated=${elevated}
removable
></md-filter-chip>
removable></md-filter-chip>
</md-chip-set>
`;
}
},
};
const inputs: MaterialStoryInit<StoryKnobs> = {
@ -126,39 +119,37 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
<md-chip-set class=${classMap(classes)} aria-label="Input chips">
<md-input-chip
label=${label || 'Input chip'}
?disabled=${disabled}
></md-input-chip>
?disabled=${disabled}></md-input-chip>
<md-input-chip
label=${label || 'Input chip with icon'}
?disabled=${disabled}
>
?disabled=${disabled}>
<md-icon slot="icon">local_laundry_service</md-icon>
</md-input-chip>
<md-input-chip
label=${label || 'Input chip with avatar'}
?disabled=${disabled}
avatar
>
<img slot="icon" src="https://lh3.googleusercontent.com/a/default-user=s48">
avatar>
<img
slot="icon"
src="https://lh3.googleusercontent.com/a/default-user=s48" />
</md-input-chip>
<md-input-chip
label=${label || 'Input link chip'}
href="https://google.com"
target="_blank"
>${GOOGLE_LOGO}</md-input-chip>
>${GOOGLE_LOGO}</md-input-chip
>
<md-input-chip
label=${label || 'Remove-only input chip'}
?disabled=${disabled}
remove-only
></md-input-chip>
remove-only></md-input-chip>
<md-input-chip
label=${label || 'Disabled input chip (focusable)'}
disabled
always-focusable
></md-input-chip>
always-focusable></md-input-chip>
</md-chip-set>
`;
}
},
};
const suggestions: MaterialStoryInit<StoryKnobs> = {
@ -171,13 +162,11 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
<md-suggestion-chip
label=${label || 'Suggestion chip'}
?disabled=${disabled}
?elevated=${elevated}
></md-suggestion-chip>
?elevated=${elevated}></md-suggestion-chip>
<md-suggestion-chip
label=${label || 'Suggestion chip with icon'}
?disabled=${disabled}
?elevated=${elevated}
>
?elevated=${elevated}>
<md-icon slot="icon">local_laundry_service</md-icon>
</md-suggestion-chip>
<md-suggestion-chip
@ -185,16 +174,16 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
?elevated=${elevated}
href="https://google.com"
target="_blank"
>${GOOGLE_LOGO}</md-suggestion-chip>
>${GOOGLE_LOGO}</md-suggestion-chip
>
<md-suggestion-chip
label=${label || 'Disabled suggestion chip (focusable)'}
disabled
always-focusable
?elevated=${elevated}
></md-suggestion-chip>
?elevated=${elevated}></md-suggestion-chip>
</md-chip-set>
`;
}
},
};
/** Chips stories. */

View File

@ -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,
];
}

View File

@ -12,16 +12,18 @@ import {Chip} from './internal/chip.js';
* Test harness for chips.
*/
export class ChipHarness extends Harness<Chip> {
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<HTMLElement>(`#${primaryId}`);
const primaryAction =
primaryId &&
this.element.renderRoot.querySelector<HTMLElement>(`#${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<Chip> {
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<Chip> {
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;

View File

@ -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,
];
}

View File

@ -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`
<a class="primary action"
<a
class="primary action"
id="link"
aria-label=${ariaLabel || nothing}
href=${this.href}
target=${this.target || nothing}
>${content}</a>
>${content}</a
>
`;
}
return html`
<button class="primary action"
<button
class="primary action"
id="button"
aria-label=${ariaLabel || nothing}
?disabled=${this.disabled && !this.alwaysFocusable}
type="button"
>${content}</button>
>${content}</button
>
`;
}

View File

@ -31,8 +31,8 @@ describe('Assist chip', () => {
await chip.updateComplete;
expect(chip.renderRoot.querySelector('a'))
.withContext('should have a rendered <a> link')
.toBeTruthy();
.withContext('should have a rendered <a> 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();
});
});
});

View File

@ -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;
}

View File

@ -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();
});
});
});

View File

@ -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()}
<md-focus-ring part="focus-ring"
for=${this.primaryId}></md-focus-ring>
<md-ripple for=${this.primaryId}
?disabled=${this.rippleDisabled}></md-ripple>
<md-focus-ring part="focus-ring" for=${this.primaryId}></md-focus-ring>
<md-ripple
for=${this.primaryId}
?disabled=${this.rippleDisabled}></md-ripple>
${this.renderPrimaryAction(this.renderPrimaryContent())}
`;
}

View File

@ -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();
});
});

View File

@ -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`
<button class="primary action"
<button
class="primary action"
id="button"
aria-label=${ariaLabel || nothing}
aria-pressed=${this.selected}
?disabled=${this.disabled && !this.alwaysFocusable}
@click=${this.handleClick}
>${content}</button>
>${content}</button
>
`;
}
@ -60,7 +63,8 @@ export class FilterChip extends MultiActionChip {
return html`
<svg class="checkmark" viewBox="0 0 18 18" aria-hidden="true">
<path d="M6.75012 12.1274L3.62262 8.99988L2.55762 10.0574L6.75012 14.2499L15.7501 5.24988L14.6926 4.19238L6.75012 12.1274Z" />
<path
d="M6.75012 12.1274L3.62262 8.99988L2.55762 10.0574L6.75012 14.2499L15.7501 5.24988L14.6926 4.19238L6.75012 12.1274Z" />
</svg>
`;
}
@ -70,7 +74,7 @@ export class FilterChip extends MultiActionChip {
return renderRemoveButton({
focusListener,
ariaLabel: this.ariaLabelRemove,
disabled: this.disabled
disabled: this.disabled,
});
}

View File

@ -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`
<a class="primary action"
<a
class="primary action"
id="link"
aria-label=${ariaLabel || nothing}
href=${this.href}
target=${this.target || nothing}
>${content}</a>
>${content}</a
>
`;
}
@ -86,12 +88,14 @@ export class InputChip extends MultiActionChip {
}
return html`
<button class="primary action"
<button
class="primary action"
id="button"
aria-label=${ariaLabel || nothing}
?disabled=${this.disabled && !this.alwaysFocusable}
type="button"
>${content}</button>
>${content}</button
>
`;
}

View File

@ -31,8 +31,8 @@ describe('Input chip', () => {
await chip.updateComplete;
expect(chip.renderRoot.querySelector('a'))
.withContext('should have a rendered <a> link')
.toBeTruthy();
.withContext('should have a rendered <a> 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();
});
});
});

View File

@ -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},
);
}
}

View File

@ -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();

View File

@ -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`
<button class="trailing action"
<button
class="trailing action"
aria-label=${ariaLabel}
tabindex=${!tabbable ? -1 : nothing}
@click=${handleRemoveClick}
@focus=${focusListener}
>
@focus=${focusListener}>
<md-focus-ring part="trailing-focus-ring"></md-focus-ring>
<md-ripple ?disabled=${disabled}></md-ripple>
<svg class="trailing icon" viewBox="0 96 960 960" aria-hidden="true">
<path d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
<path
d="m249 849-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
</svg>
<span class="touch"></span>
</button>
@ -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;
}

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('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<KnobTypesToKnobs<StoryKnobs>>(
'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));

View File

@ -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<StoryKnobs> = {
name: 'Dialog',
render({icon, headline, supportingText}) {
return html`
<md-filled-button @click=${showDialog}
aria-label="Open a dialog">Open</md-filled-button>
<md-filled-button @click=${showDialog} aria-label="Open a dialog"
>Open</md-filled-button
>
<md-dialog aria-label=${headline ? nothing : 'A simple dialog'}>
${icon ? html`<md-icon slot="icon">${icon}</md-icon>` : nothing}
@ -47,15 +48,16 @@ const standard: MaterialStoryInit<StoryKnobs> = {
</div>
</md-dialog>
`;
}
},
};
const alert: MaterialStoryInit<StoryKnobs> = {
name: 'Alert',
render() {
return html`
<md-filled-button @click=${showDialog}
aria-label="Open an alert dialog">Alert</md-filled-button>
<md-filled-button @click=${showDialog} aria-label="Open an alert dialog"
>Alert</md-filled-button
>
<md-dialog type="alert">
<div slot="headline">Alert dialog</div>
@ -68,15 +70,18 @@ const alert: MaterialStoryInit<StoryKnobs> = {
</div>
</md-dialog>
`;
}
},
};
const confirm: MaterialStoryInit<StoryKnobs> = {
name: 'Confirm',
render() {
return html`
<md-filled-button @click=${showDialog}
aria-label="Open a confirmation dialog">Confirm</md-filled-button>
<md-filled-button
@click=${showDialog}
aria-label="Open a confirmation dialog"
>Confirm</md-filled-button
>
<md-dialog style="max-width: 320px;">
<div slot="headline">Permanently delete?</div>
@ -87,12 +92,13 @@ const confirm: MaterialStoryInit<StoryKnobs> = {
</form>
<div slot="actions">
<md-text-button form="form" value="delete">Delete</md-text-button>
<md-filled-tonal-button form="form" value="cancel"
autofocus>Cancel</md-filled-tonal-button>
<md-filled-tonal-button form="form" value="cancel" autofocus
>Cancel</md-filled-tonal-button
>
</div>
</md-dialog>
`;
}
},
};
const choose: MaterialStoryInit<StoryKnobs> = {
@ -105,22 +111,36 @@ const choose: MaterialStoryInit<StoryKnobs> = {
`,
render() {
return html`
<md-filled-button @click=${showDialog}
aria-label="Open a choice dialog">Choice</md-filled-button>
<md-filled-button @click=${showDialog} aria-label="Open a choice dialog"
>Choice</md-filled-button
>
<md-dialog>
<div slot="headline">Choose your favorite pet</div>
<form id="form" slot="content" method="dialog">
<label>
<md-radio name="pet" value="cats" aria-label="Cats" touch-target="wrapper" checked></md-radio>
<md-radio
name="pet"
value="cats"
aria-label="Cats"
touch-target="wrapper"
checked></md-radio>
<span aria-hidden="true">Cats</span>
</label>
<label>
<md-radio name="pet" value="dogs" aria-label="Dogs" touch-target="wrapper"></md-radio>
<md-radio
name="pet"
value="dogs"
aria-label="Dogs"
touch-target="wrapper"></md-radio>
<span aria-hidden="true">Dogs</span>
</label>
<label>
<md-radio name="pet" value="birds" aria-label="Birds" touch-target="wrapper"></md-radio>
<md-radio
name="pet"
value="birds"
aria-label="Birds"
touch-target="wrapper"></md-radio>
<span aria-hidden="true">Birds</span>
</label>
</form>
@ -130,7 +150,7 @@ const choose: MaterialStoryInit<StoryKnobs> = {
</div>
</md-dialog>
`;
}
},
};
const contacts: MaterialStoryInit<StoryKnobs> = {
@ -140,7 +160,7 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
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<StoryKnobs> = {
flex: 1;
}
.contact-content, .contact-row {
.contact-content,
.contact-row {
display: flex;
gap: 8px;
}
@ -165,8 +186,9 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
`,
render() {
return html`
<md-filled-button @click=${showDialog}
aria-label="Open a form dialog">Form</md-filled-button>
<md-filled-button @click=${showDialog} aria-label="Open a form dialog"
>Form</md-filled-button
>
<md-dialog class="contacts">
<span slot="headline">
@ -177,7 +199,9 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
</span>
<form id="form" slot="content" method="dialog" class="contact-content">
<div class="contact-row">
<md-filled-text-field autofocus label="First Name"></md-filled-text-field>
<md-filled-text-field
autofocus
label="First Name"></md-filled-text-field>
<md-filled-text-field label="Last Name"></md-filled-text-field>
</div>
<div class="contact-row">
@ -188,24 +212,23 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
<md-filled-text-field label="Phone"></md-filled-text-field>
</form>
<div slot="actions">
<md-text-button form="form" value="reset" type="reset">Reset</md-text-button>
<md-text-button form="form" value="reset" type="reset"
>Reset</md-text-button
>
<div style="flex: 1"></div>
<md-text-button form="form" value="cancel">Cancel</md-text-button>
<md-text-button form="form" value="save">Save</md-text-button>
</div>
</md-dialog>
`;
}
},
};
const floatingSheet: MaterialStoryInit<StoryKnobs> = {
name: 'Floating sheet',
render() {
return html`
<md-filled-button
@click=${showDialog}
aria-label="Open a floating sheet"
>
<md-filled-button @click=${showDialog} aria-label="Open a floating sheet">
Floating sheet
</md-filled-button>
@ -217,16 +240,21 @@ const floatingSheet: MaterialStoryInit<StoryKnobs> = {
</md-icon-button>
</span>
<form id="form" slot="content" method="dialog">
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.
</form>
</md-dialog>
`;
}
},
};
/** Dialog stories. */
export const stories =
[standard, alert, confirm, choose, contacts, floatingSheet];
export const stories = [
standard,
alert,
confirm,
choose,
contacts,
floatingSheet,
];

View File

@ -20,7 +20,7 @@ describe('<md-dialog>', () => {
<md-dialog>
<form id="form" method="dialog" slot="content">
Content
<input autofocus>
<input autofocus />
</form>
<div slot="actions">
<button form="form" value="button">Close</button>
@ -107,14 +107,19 @@ describe('<md-dialog>', () => {
it('closes when element with action is clicked', async () => {
const {harness} = await setupTest();
await harness.element.show();
const closedPromise = new Promise<void>(resolve => {
harness.element.addEventListener('closed', () => {
resolve();
}, {once: true});
const closedPromise = new Promise<void>((resolve) => {
harness.element.addEventListener(
'closed',
() => {
resolve();
},
{once: true},
);
});
harness.element.querySelector<HTMLButtonElement>(
'[value="button"]')!.click();
harness.element
.querySelector<HTMLButtonElement>('[value="button"]')!
.click();
await closedPromise;
expect(harness.element.open).toBeFalse();
expect(harness.element.returnValue).toBe('button');
@ -132,19 +137,18 @@ describe('<md-dialog>', () => {
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('<md-dialog>', () => {
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('<md-dialog>', () => {
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('<md-dialog>', () => {
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('<md-dialog>', () => {
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();
});
});

View File

@ -14,7 +14,8 @@ import {Dialog} from './internal/dialog.js';
export class DialogHarness extends Harness<Dialog> {
override async getInteractiveElement() {
await this.element.updateComplete;
return this.element.querySelector<HTMLElement>('[autocomplete]') ??
this.element;
return (
this.element.querySelector<HTMLElement>('[autocomplete]') ?? this.element
);
}
}

View File

@ -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'},
],
],
};

View File

@ -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}
>
<div class="container"
@click=${this.handleContentClick}
>
.returnValue=${this.returnValue || nothing}>
<div class="container" @click=${this.handleContentClick}>
<div class="headline">
<div class="icon" aria-hidden="true">
<slot name="icon" @slotchange=${this.handleIconChange}></slot>
</div>
<h2 id="headline" aria-hidden=${!this.hasHeadline || nothing}>
<slot name="headline"
@slotchange=${this.handleHeadlineChange}></slot>
<slot
name="headline"
@slotchange=${this.handleHeadlineChange}></slot>
</h2>
<md-divider></md-divider>
</div>
@ -260,8 +265,7 @@ export class Dialog extends LitElement {
</div>
<div class="actions">
<md-divider></md-divider>
<slot name="actions"
@slotchange=${this.handleActionsChange}></slot>
<slot name="actions" @slotchange=${this.handleActionsChange}></slot>
</div>
</div>
</dialog>
@ -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 `<dialog>`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<void>(resolve => {
return new Promise<void>((resolve) => {
this.isConnectedPromiseResolve = resolve;
});
}

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('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<KnobTypesToKnobs<StoryKnobs>>(
'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));

View File

@ -35,25 +35,24 @@ const standard: MaterialStoryInit<StoryKnobs> = {
`,
render(knobs) {
return html`
<ul aria-label="A list of items with decorative and non-decorative separators">
<ul
aria-label="A list of items with decorative and non-decorative separators">
<li>List item one</li>
<md-divider
?inset=${knobs.inset}
?inset-start=${knobs['inset (start)']}
?inset-end=${knobs['inset (end)']}
></md-divider>
?inset-end=${knobs['inset (end)']}></md-divider>
<li>List item two</li>
<md-divider role="separator"></md-divider>
<li>List item three</li>
<md-divider
?inset=${knobs.inset}
?inset-start=${knobs['inset (start)']}
?inset-end=${knobs['inset (end)']}
></md-divider>
?inset-end=${knobs['inset (end)']}></md-divider>
<li>List item four</li>
</ul>
`;
}
},
};
/** Divider stories. */

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('Elevation', [
new Knob('level', {defaultValue: 1, ui: numberInput()}),
]);
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
'Elevation',
[new Knob('level', {defaultValue: 1, ui: numberInput()})],
);
collection.addStories(...materialInitsToStoryInits(stories));

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('FAB', [
{label: 'medium', value: 'medium'},
{label: 'small', value: 'small'},
{label: 'large', value: 'large'},
]
})
],
}),
}),
]);

View File

@ -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<StoryKnobs> = {
variant="surface"
.lowered=${lowered}
.label=${label}
.size=${size!}
>
.size=${size!}>
<md-icon slot="icon">${icon}</md-icon>
</md-fab>
</label>
@ -59,8 +61,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
variant="primary"
.lowered=${lowered}
.label=${label}
.size=${size!}
>
.size=${size!}>
<md-icon slot="icon">${icon}</md-icon>
</md-fab>
</label>
@ -72,8 +73,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
variant="secondary"
.lowered=${lowered}
.label=${label}
.size=${size!}
>
.size=${size!}>
<md-icon slot="icon">${icon}</md-icon>
</md-fab>
</label>
@ -85,8 +85,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
variant="tertiary"
.lowered=${lowered}
.label=${label}
.size=${size!}
>
.size=${size!}>
<md-icon slot="icon">${icon}</md-icon>
</md-fab>
</label>
@ -97,8 +96,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
aria-label=${label ? nothing : 'An example branded FAB'}
.lowered=${lowered}
.label=${label}
.size=${size!}
>
.size=${size!}>
<svg slot="icon" width="36" height="36" viewBox="0 0 36 36">
<path fill="#34A853" d="M16 16v14h4V20z"></path>
<path fill="#4285F4" d="M30 16H20l-4 4h14z"></path>
@ -110,7 +108,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
</label>
</div>
`;
}
},
};
/** Checkbox stories. */

View File

@ -156,8 +156,9 @@ describe('<md-branded-fab>', () => {
const env = new Environment();
async function setupTest() {
const element = env.render(html`<md-branded-fab></md-branded-fab>`)
.querySelector('md-branded-fab');
const element = env
.render(html`<md-branded-fab></md-branded-fab>`)
.querySelector('md-branded-fab');
if (!element) {
throw new Error('Could not query rendered <md-branded-fab>.');
}

View File

@ -14,7 +14,6 @@ import {Fab} from './internal/fab.js';
export class FabHarness extends Harness<Fab> {
override async getInteractiveElement() {
await this.element.updateComplete;
return this.element.renderRoot.querySelector('.fab') as
HTMLButtonElement;
return this.element.renderRoot.querySelector('.fab') as HTMLButtonElement;
}
}

View File

@ -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 {

View File

@ -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`
<button
class="fab ${classMap(this.getRenderClasses())}"
aria-label=${ariaLabel || nothing}
>
aria-label=${ariaLabel || nothing}>
<md-elevation></md-elevation>
<md-focus-ring part="focus-ring"></md-focus-ring>
<md-ripple class="ripple"></md-ripple>
${this.renderTouchTarget()}
${this.renderIcon()}
${this.renderLabel()}
${this.renderTouchTarget()} ${this.renderIcon()} ${this.renderLabel()}
</button>
`;
}
@ -90,12 +86,13 @@ export abstract class SharedFab extends LitElement {
private renderIcon() {
const {ariaLabel} = this as ARIAMixinStrict;
return html`<span class="icon">
<slot
name="icon"
aria-hidden=${
ariaLabel || this.label ? 'true' : nothing as unknown as 'false'}>
<span></span>
</slot>
</span>`;
<slot
name="icon"
aria-hidden=${ariaLabel || this.label
? 'true'
: (nothing as unknown as 'false')}>
<span></span>
</slot>
</span>`;
}
}

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('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<KnobTypesToKnobs<StoryKnobs>>(
'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));

View File

@ -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<StoryKnobs> = {
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`<textarea rows="1" ?disabled=${disabled}></textarea>` :
html`<input ?disabled=${disabled}>`;
const content = resizable
? html`<textarea rows="1" ?disabled=${disabled}></textarea>`
: html`<input ?disabled=${disabled} />`;
const styles = {resize: resizable ? 'both' : null};
return html`
@ -78,14 +78,12 @@ const filled: MaterialStoryInit<StoryKnobs> = {
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}
</md-filled-field>
`;
}
},
};
const outlined: MaterialStoryInit<StoryKnobs> = {
@ -101,16 +99,18 @@ const outlined: MaterialStoryInit<StoryKnobs> = {
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`<textarea rows="1" ?disabled=${
disabled} aria-describedby="description"></textarea>` :
html`<input ?disabled=${disabled} aria-describedby="description">`;
const content = resizable
? html`<textarea
rows="1"
?disabled=${disabled}
aria-describedby="description"></textarea>`
: html`<input ?disabled=${disabled} aria-describedby="description" />`;
const styles = {resize: resizable ? 'both' : null};
return html`
@ -127,15 +127,13 @@ const outlined: MaterialStoryInit<StoryKnobs> = {
supporting-text=${supportingText}
error-text=${errorText}
count=${count}
max=${max}
>
max=${max}>
<div id="description" slot="aria-describedby" hidden></div>
${hasStart ? START_CONTENT : nothing}
${content}
${hasStart ? START_CONTENT : nothing} ${content}
${hasEnd ? END_CONTENT : nothing}
</md-outlined-field>
`;
}
},
};
/** Field stories. */

View File

@ -30,6 +30,6 @@ export class FieldHarness extends Harness<Field> {
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;
}
}

View File

@ -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<Field>) {
// 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`
<div class="field ${classMap(classes)}">
<div class="container-overflow">
${this.renderBackground?.()}
${this.renderIndicator?.()}
${outline}
${this.renderBackground?.()} ${this.renderIndicator?.()} ${outline}
<div class="container">
<div class="start">
<slot name="start"></slot>
</div>
<div class="middle">
<div class="label-wrapper">
${restingLabel}
${outline ? nothing : floatingLabel}
${restingLabel} ${outline ? nothing : floatingLabel}
</div>
<div class="content">
<slot></slot>
@ -145,8 +150,12 @@ export class Field extends LitElement {
}
protected override updated(changed: PropertyValues<Field>) {
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`<span>${supportingOrErrorText}</span>`;
// Conditionally render counter so we don't render the extra `gap`.
// TODO(b/244473435): add aria-label and announcements
const end = counterText ?
html`<span class="counter">${counterText}</span>` :
nothing;
const end = counterText
? html`<span class="counter">${counterText}</span>`
: 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`
<div class="supporting-text" role=${role}>${start}${end}</div>
<slot name="aria-describedby" @slotchange=${
this.updateSlottedAriaDescribedBy}></slot>
<slot
name="aria-describedby"
@slotchange=${this.updateSlottedAriaDescribedBy}></slot>
`;
}
@ -230,15 +240,18 @@ export class Field extends LitElement {
const labelText = `${this.label}${this.required ? '*' : ''}`;
return html`
<span class="label ${classMap(classes)}"
aria-hidden=${!visible}
>${labelText}</span>
<span class="label ${classMap(classes)}" aria-hidden=${!visible}
>${labelText}</span
>
`;
}
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},
];
}

View File

@ -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 ?? ''}
>
<input>
.errorText=${props.errorText ?? ''}>
<input />
</md-test-field>
`;
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();
});
});
});

View File

@ -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<KnobTypesToKnobs<StoryKnobs>>('Focus', [
new Knob('inward', {ui: boolInput(), defaultValue: false}),
]);
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
'Focus',
[new Knob('inward', {ui: boolInput(), defaultValue: false})],
);
collection.addStories(...materialInitsToStoryInits(stories));

View File

@ -55,13 +55,13 @@ const standard: MaterialStoryInit<StoryKnobs> = {
<md-focus-ring ?inward=${inward}></md-focus-ring>
</button>
`;
}
},
};
const multiAction: MaterialStoryInit<StoryKnobs> = {
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<StoryKnobs> = {
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<StoryKnobs> = {
--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<StoryKnobs> = {
<md-focus-ring for="primary" ?inward=${inward}></md-focus-ring>
<div role="listitem">
<button id="primary" aria-label="The primary action for a multi-action component">
<button
id="primary"
aria-label="The primary action for a multi-action component">
Action
</button>
</div>
<div role="listitem">
<button id="secondary" aria-label="The secondary action for a multi-action component">
<button
id="secondary"
aria-label="The secondary action for a multi-action component">
X
<md-focus-ring ?inward=${inward}></md-focus-ring>
</button>
</div>
</div>
`;
}
},
};
/** Focus ring stories. */

View File

@ -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) {

View File

@ -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`
<button id="first"></button>
<button id="second"></button>
<test-focus-ring for="first"></test-focus-ring>
`);
it('should update a referenced element when for attribute changes', async () => {
const {root, focusRing} = setupTest(html`
<button id="first"></button>
<button id="second"></button>
<test-focus-ring for="first"></test-focus-ring>
`);
const secondButton = root.querySelector<HTMLElement>('#second');
if (!secondButton) {
throw new Error('Could not query rendered <button id="second">');
}
const secondButton = root.querySelector<HTMLElement>('#second');
if (!secondButton) {
throw new Error('Could not query rendered <button id="second">');
}
focusRing.setAttribute('for', 'second');
expect(focusRing.control)
.withContext('focusRing.control')
.toBe(secondButton);
await new Harness(secondButton).focusWithKeyboard();
focusRing.setAttribute('for', 'second');
expect(focusRing.control)
.withContext('focusRing.control')
.toBe(secondButton);
await new Harness(secondButton).focusWithKeyboard();
expect(focusRing.visible).withContext('focusRing.visible').toBeTrue();
});
expect(focusRing.visible).withContext('focusRing.visible').toBeTrue();
});
it('should be able to be imperatively attached', () => {
const {button, focusRing} = setupTest(html`
@ -104,12 +103,12 @@ describe('focus ring', () => {
`);
expect(focusRing.control)
.withContext('focusRing.control before attach')
.toBe(button);
.withContext('focusRing.control before attach')
.toBe(button);
focusRing.attach(button);
expect(focusRing.control)
.withContext('focusRing.control after attach')
.toBe(button);
.withContext('focusRing.control after attach')
.toBe(button);
});
it('should detach previous control when attaching a new one', async () => {
@ -170,8 +169,8 @@ describe('focus ring', () => {
await harness.clickWithMouse();
expect(focusRing.visible)
.withContext('focusRing.visible after clickWithMouse')
.toBeFalse();
.withContext('focusRing.visible after clickWithMouse')
.toBeFalse();
});
it('should be visible on keyboard focus', async () => {
@ -183,8 +182,8 @@ describe('focus ring', () => {
await harness.focusWithKeyboard();
expect(focusRing.visible)
.withContext('focusRing.visible after focusWithKeyboard')
.toBeTrue();
.withContext('focusRing.visible after focusWithKeyboard')
.toBeTrue();
});
it('should hide on blur', async () => {
@ -197,7 +196,7 @@ describe('focus ring', () => {
focusRing.visible = true;
await harness.blur();
expect(focusRing.visible)
.withContext('focusRing.visible after blur')
.toBeFalse();
.withContext('focusRing.visible after blur')
.toBeFalse();
});
});

View File

@ -23,8 +23,7 @@ describe('<md-icon>', () => {
describe('accessiblity', () => {
it('sets aria-hidden to true by default', async () => {
const root = env.render(html`
<md-icon>check</md-icon>`);
const root = env.render(html` <md-icon>check</md-icon>`);
const icon = root.querySelector('md-icon')!;
await env.waitForStability();
@ -33,8 +32,9 @@ describe('<md-icon>', () => {
});
it('sets aria-hidden is removed when initalized as false', async () => {
const root = env.render(html`
<md-icon aria-hidden="false">check</md-icon>`);
const root = env.render(html` <md-icon aria-hidden="false"
>check</md-icon
>`);
const icon = root.querySelector('md-icon')!;
await env.waitForStability();
@ -43,8 +43,7 @@ describe('<md-icon>', () => {
});
it('allows overriding aria-hidden after first render', async () => {
const root = env.render(html`
<md-icon>check</md-icon>`);
const root = env.render(html` <md-icon>check</md-icon>`);
const icon = root.querySelector('md-icon')!;
await env.waitForStability();
@ -58,9 +57,9 @@ describe('<md-icon>', () => {
});
it('overrides invalid aria-hidden values to true', async () => {
const root = env.render(html`
<!-- @ts-ignore:disable-next-line:no-incompatible-type-binding -->
<md-icon aria-hidden="foo">check</md-icon>`);
const root =
env.render(html` <!-- @ts-ignore:disable-next-line:no-incompatible-type-binding -->
<md-icon aria-hidden="foo">check</md-icon>`);
const icon = root.querySelector('md-icon')!;
await env.waitForStability();

View File

@ -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, textInput} from './index.js';
import {stories, StoryKnobs} from './stories.js';
const collection =
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Icon Button', [
new Knob('icon', {ui: textInput(), defaultValue: 'check'}),
new Knob('selectedIcon', {ui: textInput(), defaultValue: 'close'}),
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
]);
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
'Icon Button',
[
new Knob('icon', {ui: textInput(), defaultValue: 'check'}),
new Knob('selectedIcon', {ui: textInput(), defaultValue: 'close'}),
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
],
);
collection.addStories(...materialInitsToStoryInits(stories));

View File

@ -7,8 +7,8 @@
import '@material/web/icon/icon.js';
import '@material/web/iconbutton/filled-icon-button.js';
import '@material/web/iconbutton/filled-tonal-icon-button.js';
import '@material/web/iconbutton/outlined-icon-button.js';
import '@material/web/iconbutton/icon-button.js';
import '@material/web/iconbutton/outlined-icon-button.js';
import {MaterialStoryInit} from './material-collection.js';
import {css, html} from 'lit';
@ -40,10 +40,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
return html`
<div class="column">
<div class="row">
<md-icon-button
aria-label="Standard icon"
?disabled=${disabled}
>
<md-icon-button aria-label="Standard icon" ?disabled=${disabled}>
<md-icon>${icon}</md-icon>
</md-icon-button>
@ -53,10 +50,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
<md-icon>${icon}</md-icon>
</md-outlined-icon-button>
<md-filled-icon-button
aria-label="Filled icon"
?disabled=${disabled}
>
<md-filled-icon-button aria-label="Filled icon" ?disabled=${disabled}>
<md-icon>${icon}</md-icon>
</md-filled-icon-button>
@ -68,7 +62,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
</div>
</div>
`;
}
},
};
const toggles: MaterialStoryInit<StoryKnobs> = {
@ -81,8 +75,7 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
<md-icon-button
aria-label="Standard icon"
toggle
?disabled=${disabled}
>
?disabled=${disabled}>
<md-icon>${icon}</md-icon>
<md-icon slot="selected">${selectedIcon}</md-icon>
</md-icon-button>
@ -90,8 +83,7 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
<md-outlined-icon-button
aria-label="Outlined icon"
toggle
?disabled=${disabled}
>
?disabled=${disabled}>
<md-icon>${icon}</md-icon>
<md-icon slot="selected">${selectedIcon}</md-icon>
</md-outlined-icon-button>
@ -99,8 +91,7 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
<md-filled-icon-button
aria-label="Filled icon"
toggle
?disabled=${disabled}
>
?disabled=${disabled}>
<md-icon>${icon}</md-icon>
<md-icon slot="selected">${selectedIcon}</md-icon>
</md-filled-icon-button>
@ -108,15 +99,14 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
<md-filled-tonal-icon-button
aria-label="Filled tonal icon"
toggle
?disabled=${disabled}
>
?disabled=${disabled}>
<md-icon>${icon}</md-icon>
<md-icon slot="selected">${selectedIcon}</md-icon>
</md-filled-tonal-icon-button>
</div>
</div>
`;
}
},
};
const links: MaterialStoryInit<StoryKnobs> = {
@ -129,38 +119,34 @@ const links: MaterialStoryInit<StoryKnobs> = {
<md-icon-button
aria-label="Standard icon"
href="https://google.com"
target="_blank"
>
target="_blank">
<md-icon>${icon}</md-icon>
</md-icon-button>
<md-outlined-icon-button
aria-label="Outlined icon"
href="https://google.com"
target="_blank"
>
target="_blank">
<md-icon>${icon}</md-icon>
</md-outlined-icon-button>
<md-filled-icon-button
aria-label="Filled icon"
href="https://google.com"
target="_blank"
>
target="_blank">
<md-icon>${icon}</md-icon>
</md-filled-icon-button>
<md-filled-tonal-icon-button
aria-label="Filled tonal icon"
href="https://google.com"
target="_blank"
>
target="_blank">
<md-icon>${icon}</md-icon>
</md-filled-tonal-icon-button>
</div>
</div>
`;
}
},
};
/** Icon button stories. */

View File

@ -14,4 +14,4 @@ describe('<md-filled-icon-button>', () => {
describe('.styles', () => {
createTokenTests(MdFilledIconButton.styles);
});
});
});

View File

@ -14,4 +14,4 @@ describe('<md-filled-tonal-icon-button', () => {
describe('.styles', () => {
createTokenTests(MdFilledTonalIconButton.styles);
});
});
});

View File

@ -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`
<div dir="rtl">
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
star
</md-icon-button>
</div>`;
const template = html` <div dir="rtl">
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
star
</md-icon-button>
</div>`;
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`
<div dir="ltr">
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
star
</md-icon-button>
</div>`;
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` <div dir="ltr">
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
star
</md-icon-button>
</div>`;
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();
});
});

View File

@ -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`
<a class="link"
<a
class="link"
id="link"
href="${this.href}"
target="${this.target || nothing}"
aria-label="${ariaLabel || nothing}"
></a>
aria-label="${ariaLabel || nothing}"></a>
`;
}
@ -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`<span class="icon icon--selected"><slot name="selected"><slot></slot></slot></span>`;
return html`<span class="icon icon--selected"
><slot name="selected"><slot></slot></slot
></span>`;
}
private renderTouchTarget() {
@ -177,15 +186,15 @@ export class IconButton extends LitElement implements FormSubmitter {
}
private renderFocusRing() {
return html`<md-focus-ring part="focus-ring" for=${
this.href ? 'link' : 'button'}></md-focus-ring>`;
return html`<md-focus-ring
part="focus-ring"
for=${this.href ? 'link' : 'button'}></md-focus-ring>`;
}
private renderRipple() {
return html`<md-ripple
for=${this.href ? 'link' : nothing}
?disabled="${!this.href && this.disabled}"
></md-ripple>`;
?disabled="${!this.href && this.disabled}"></md-ripple>`;
}
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 <input> & <select>
// Additionally, native change event is not an InputEvent.
this.dispatchEvent(new Event('change', {bubbles: true}));

View File

@ -14,4 +14,4 @@ describe('<md-outlined-icon-button>', () => {
describe('.styles', () => {
createTokenTests(MdOutlinedIconButton.styles);
});
});
});

View File

@ -86,20 +86,25 @@ export function isAriaAttribute(attribute: string): attribute is ARIAAttribute {
* @param property The aria property.
* @return The aria attribute.
*/
export function ariaPropertyToAttribute<K extends ARIAProperty|'role'>(
property: K) {
return property
.replace('aria', 'aria-')
// IDREF attributes also include an "Element" or "Elements" suffix
.replace(/Elements?/g, '')
.toLowerCase() as ARIAPropertyToAttribute<K>;
export function ariaPropertyToAttribute<K extends ARIAProperty | 'role'>(
property: K,
) {
return (
property
.replace('aria', 'aria-')
// IDREF attributes also include an "Element" or "Elements" suffix
.replace(/Elements?/g, '')
.toLowerCase() as ARIAPropertyToAttribute<K>
);
}
// Converts an `ariaFoo` string type to an `aria-foo` string type.
type ARIAPropertyToAttribute<K extends string> =
K extends `aria${infer Suffix}Element${infer OptS}` ?
`aria-${Lowercase < Suffix >}` :
K extends `aria${infer Suffix}` ? `aria-${Lowercase < Suffix >}` : K;
K extends `aria${infer Suffix}Element${infer OptS}`
? `aria-${Lowercase<Suffix>}`
: K extends `aria${infer Suffix}`
? `aria-${Lowercase<Suffix>}`
: K;
/**
* An extension of `ARIAMixin` that enforces strict value types for aria
@ -118,68 +123,176 @@ type ARIAPropertyToAttribute<K extends string> =
* }
*/
export interface ARIAMixinStrict extends ARIAMixin {
ariaAtomic: 'true'|'false'|null;
ariaAutoComplete: 'none'|'inline'|'list'|'both'|null;
ariaBusy: 'true'|'false'|null;
ariaChecked: 'true'|'false'|null;
ariaColCount: `${number}`|null;
ariaColIndex: `${number}`|null;
ariaColSpan: `${number}`|null;
ariaCurrent: 'page'|'step'|'location'|'date'|'time'|'true'|'false'|null;
ariaDisabled: 'true'|'false'|null;
ariaExpanded: 'true'|'false'|null;
ariaHasPopup: 'false'|'true'|'menu'|'listbox'|'tree'|'grid'|'dialog'|null;
ariaHidden: 'true'|'false'|null;
ariaInvalid: 'true'|'false'|null;
ariaKeyShortcuts: string|null;
ariaLabel: string|null;
ariaLevel: `${number}`|null;
ariaLive: 'assertive'|'off'|'polite'|null;
ariaModal: 'true'|'false'|null;
ariaMultiLine: 'true'|'false'|null;
ariaMultiSelectable: 'true'|'false'|null;
ariaOrientation: 'horizontal'|'vertical'|'undefined'|null;
ariaPlaceholder: string|null;
ariaPosInSet: `${number}`|null;
ariaPressed: 'true'|'false'|null;
ariaReadOnly: 'true'|'false'|null;
ariaRequired: 'true'|'false'|null;
ariaRoleDescription: string|null;
ariaRowCount: `${number}`|null;
ariaRowIndex: `${number}`|null;
ariaRowSpan: `${number}`|null;
ariaSelected: 'true'|'false'|null;
ariaSetSize: `${number}`|null;
ariaSort: 'ascending'|'descending'|'none'|'other'|null;
ariaValueMax: `${number}`|null;
ariaValueMin: `${number}`|null;
ariaValueNow: `${number}`|null;
ariaValueText: string|null;
role: ARIARole|null;
ariaAtomic: 'true' | 'false' | null;
ariaAutoComplete: 'none' | 'inline' | 'list' | 'both' | null;
ariaBusy: 'true' | 'false' | null;
ariaChecked: 'true' | 'false' | null;
ariaColCount: `${number}` | null;
ariaColIndex: `${number}` | null;
ariaColSpan: `${number}` | null;
ariaCurrent:
| 'page'
| 'step'
| 'location'
| 'date'
| 'time'
| 'true'
| 'false'
| null;
ariaDisabled: 'true' | 'false' | null;
ariaExpanded: 'true' | 'false' | null;
ariaHasPopup:
| 'false'
| 'true'
| 'menu'
| 'listbox'
| 'tree'
| 'grid'
| 'dialog'
| null;
ariaHidden: 'true' | 'false' | null;
ariaInvalid: 'true' | 'false' | null;
ariaKeyShortcuts: string | null;
ariaLabel: string | null;
ariaLevel: `${number}` | null;
ariaLive: 'assertive' | 'off' | 'polite' | null;
ariaModal: 'true' | 'false' | null;
ariaMultiLine: 'true' | 'false' | null;
ariaMultiSelectable: 'true' | 'false' | null;
ariaOrientation: 'horizontal' | 'vertical' | 'undefined' | null;
ariaPlaceholder: string | null;
ariaPosInSet: `${number}` | null;
ariaPressed: 'true' | 'false' | null;
ariaReadOnly: 'true' | 'false' | null;
ariaRequired: 'true' | 'false' | null;
ariaRoleDescription: string | null;
ariaRowCount: `${number}` | null;
ariaRowIndex: `${number}` | null;
ariaRowSpan: `${number}` | null;
ariaSelected: 'true' | 'false' | null;
ariaSetSize: `${number}` | null;
ariaSort: 'ascending' | 'descending' | 'none' | 'other' | null;
ariaValueMax: `${number}` | null;
ariaValueMin: `${number}` | null;
ariaValueNow: `${number}` | null;
ariaValueText: string | null;
role: ARIARole | null;
}
/**
* Valid values for `role`.
*/
export type ARIARole =
'alert'|'alertdialog'|'button'|'checkbox'|'dialog'|'gridcell'|'link'|'log'|
'marquee'|'menuitem'|'menuitemcheckbox'|'menuitemradio'|'option'|
'progressbar'|'radio'|'scrollbar'|'searchbox'|'slider'|'spinbutton'|
'status'|'switch'|'tab'|'tabpanel'|'textbox'|'timer'|'tooltip'|'treeitem'|
'combobox'|'grid'|'listbox'|'menu'|'menubar'|'radiogroup'|'tablist'|'tree'|
'treegrid'|'application'|'article'|'cell'|'columnheader'|'definition'|
'directory'|'document'|'feed'|'figure'|'group'|'heading'|'img'|'list'|
'listitem'|'math'|'none'|'note'|'presentation'|'region'|'row'|'rowgroup'|
'rowheader'|'separator'|'table'|'term'|'text'|'toolbar'|'banner'|
'complementary'|'contentinfo'|'form'|'main'|'navigation'|'region'|'search'|
'doc-abstract'|'doc-acknowledgments'|'doc-afterword'|'doc-appendix'|
'doc-backlink'|'doc-biblioentry'|'doc-bibliography'|'doc-biblioref'|
'doc-chapter'|'doc-colophon'|'doc-conclusion'|'doc-cover'|'doc-credit'|
'doc-credits'|'doc-dedication'|'doc-endnote'|'doc-endnotes'|'doc-epigraph'|
'doc-epilogue'|'doc-errata'|'doc-example'|'doc-footnote'|'doc-foreword'|
'doc-glossary'|'doc-glossref'|'doc-index'|'doc-introduction'|'doc-noteref'|
'doc-notice'|'doc-pagebreak'|'doc-pagelist'|'doc-part'|'doc-preface'|
'doc-prologue'|'doc-pullquote'|'doc-qna'|'doc-subtitle'|'doc-tip'|'doc-toc';
| 'alert'
| 'alertdialog'
| 'button'
| 'checkbox'
| 'dialog'
| 'gridcell'
| 'link'
| 'log'
| 'marquee'
| 'menuitem'
| 'menuitemcheckbox'
| 'menuitemradio'
| 'option'
| 'progressbar'
| 'radio'
| 'scrollbar'
| 'searchbox'
| 'slider'
| 'spinbutton'
| 'status'
| 'switch'
| 'tab'
| 'tabpanel'
| 'textbox'
| 'timer'
| 'tooltip'
| 'treeitem'
| 'combobox'
| 'grid'
| 'listbox'
| 'menu'
| 'menubar'
| 'radiogroup'
| 'tablist'
| 'tree'
| 'treegrid'
| 'application'
| 'article'
| 'cell'
| 'columnheader'
| 'definition'
| 'directory'
| 'document'
| 'feed'
| 'figure'
| 'group'
| 'heading'
| 'img'
| 'list'
| 'listitem'
| 'math'
| 'none'
| 'note'
| 'presentation'
| 'region'
| 'row'
| 'rowgroup'
| 'rowheader'
| 'separator'
| 'table'
| 'term'
| 'text'
| 'toolbar'
| 'banner'
| 'complementary'
| 'contentinfo'
| 'form'
| 'main'
| 'navigation'
| 'region'
| 'search'
| 'doc-abstract'
| 'doc-acknowledgments'
| 'doc-afterword'
| 'doc-appendix'
| 'doc-backlink'
| 'doc-biblioentry'
| 'doc-bibliography'
| 'doc-biblioref'
| 'doc-chapter'
| 'doc-colophon'
| 'doc-conclusion'
| 'doc-cover'
| 'doc-credit'
| 'doc-credits'
| 'doc-dedication'
| 'doc-endnote'
| 'doc-endnotes'
| 'doc-epigraph'
| 'doc-epilogue'
| 'doc-errata'
| 'doc-example'
| 'doc-footnote'
| 'doc-foreword'
| 'doc-glossary'
| 'doc-glossref'
| 'doc-index'
| 'doc-introduction'
| 'doc-noteref'
| 'doc-notice'
| 'doc-pagebreak'
| 'doc-pagelist'
| 'doc-part'
| 'doc-preface'
| 'doc-prologue'
| 'doc-pullquote'
| 'doc-qna'
| 'doc-subtitle'
| 'doc-tip'
| 'doc-toc';
/**
* Enables a host custom element to be the target for aria roles and attributes.
@ -196,9 +309,11 @@ export type ARIARole =
* @param options Options to configure the element's host aria.
*/
export function setupHostAria(
ctor: typeof ReactiveElement, {focusable}: SetupHostAriaOptions = {}) {
ctor: typeof ReactiveElement,
{focusable}: SetupHostAriaOptions = {},
) {
if (focusable !== false) {
ctor.addInitializer(host => {
ctor.addInitializer((host) => {
host.addController({
hostConnected() {
if (host.hasAttribute('tabindex')) {
@ -206,7 +321,7 @@ export function setupHostAria(
}
host.tabIndex = 0;
}
},
});
});
}
@ -262,7 +377,9 @@ export interface SetupHostAriaOptions {
* }
*/
export function polyfillElementInternalsAria(
host: ReactiveElement, internals: ElementInternals) {
host: ReactiveElement,
internals: ElementInternals,
) {
if (checkIfElementInternalsSupportsAria(internals)) {
return internals;
}
@ -271,25 +388,29 @@ export function polyfillElementInternalsAria(
throw new Error('Missing setupHostAria()');
}
let firstConnectedCallbacks:
Array<{property: ARIAProperty | 'role', callback: () => void}> = [];
let firstConnectedCallbacks: Array<{
property: ARIAProperty | 'role';
callback: () => void;
}> = [];
let hasBeenConnected = false;
// Add support for Firefox, which has not yet implement ElementInternals aria
for (const ariaProperty of ARIA_PROPERTIES) {
let internalAriaValue: string|null = null;
let internalAriaValue: string | null = null;
Object.defineProperty(internals, ariaProperty, {
enumerable: true,
configurable: true,
get() {
return internalAriaValue;
},
set(value: string|null) {
set(value: string | null) {
const setValue = () => {
internalAriaValue = value;
if (!hasBeenConnected) {
firstConnectedCallbacks.push(
{property: ariaProperty, callback: setValue});
firstConnectedCallbacks.push({
property: ariaProperty,
callback: setValue,
});
return;
}
@ -303,14 +424,14 @@ export function polyfillElementInternalsAria(
});
}
let internalRoleValue: string|null = null;
let internalRoleValue: string | null = null;
Object.defineProperty(internals, 'role', {
enumerable: true,
configurable: true,
get() {
return internalRoleValue;
},
set(value: string|null) {
set(value: string | null) {
const setRole = () => {
internalRoleValue = value;
@ -341,17 +462,17 @@ export function polyfillElementInternalsAria(
hasBeenConnected = true;
const propertiesSetByUser = new Set<ARIAProperty|'role'>();
const propertiesSetByUser = new Set<ARIAProperty | 'role'>();
// See which properties were set by the user on host before we apply
// internals values as attributes to host. Needs to be done in another
// for loop because the callbacks set these attributes on host.
for (const {property} of firstConnectedCallbacks) {
const wasSetByUser =
host.getAttribute(ariaPropertyToAttribute(property)) !== null ||
// Dynamic lookup rather than hardcoding all properties.
// tslint:disable-next-line:no-dict-access-on-struct-type
host[property] !== undefined;
host.getAttribute(ariaPropertyToAttribute(property)) !== null ||
// Dynamic lookup rather than hardcoding all properties.
// tslint:disable-next-line:no-dict-access-on-struct-type
host[property] !== undefined;
if (wasSetByUser) {
propertiesSetByUser.add(property);
@ -370,13 +491,12 @@ export function polyfillElementInternalsAria(
// Remove strong callback references
firstConnectedCallbacks = [];
}
},
});
return internals;
}
// Separate function so that typescript doesn't complain about internals being
// "never".
function checkIfElementInternalsSupportsAria(internals: ElementInternals) {

View File

@ -9,20 +9,26 @@
import {html, LitElement} from 'lit';
import {customElement} from 'lit/decorators.js';
import {ARIAProperty, ariaPropertyToAttribute, isAriaAttribute, polyfillElementInternalsAria, setupHostAria} from './aria.js';
import {
ARIAProperty,
ariaPropertyToAttribute,
isAriaAttribute,
polyfillElementInternalsAria,
setupHostAria,
} from './aria.js';
describe('aria', () => {
describe('isAriaAttribute()', () => {
it('should return true for aria value attributes', () => {
expect(isAriaAttribute('aria-label'))
.withContext('aria-label input')
.toBeTrue();
.withContext('aria-label input')
.toBeTrue();
});
it('should return true for aria idref attributes', () => {
expect(isAriaAttribute('aria-labelledby'))
.withContext('aria-labelledby input')
.toBeTrue();
.withContext('aria-labelledby input')
.toBeTrue();
});
it('should return false for role', () => {
@ -40,8 +46,9 @@ describe('aria', () => {
});
it('should convert aria idref properties', () => {
expect(ariaPropertyToAttribute('ariaLabelledByElements' as ARIAProperty))
.toBe('aria-labelledby');
expect(
ariaPropertyToAttribute('ariaLabelledByElements' as ARIAProperty),
).toBe('aria-labelledby');
});
});
@ -60,16 +67,16 @@ describe('aria', () => {
it('should not hydrate tabindex attribute on creation', () => {
const element = new TestElement();
expect(element.hasAttribute('tabindex'))
.withContext('has tabindex attribute')
.toBeFalse();
.withContext('has tabindex attribute')
.toBeFalse();
});
it('should set tabindex="0" on element once connected', () => {
const element = new TestElement();
document.body.appendChild(element);
expect(element.getAttribute('tabindex'))
.withContext('tabindex attribute value')
.toEqual('0');
.withContext('tabindex attribute value')
.toEqual('0');
element.remove();
});
@ -79,8 +86,8 @@ describe('aria', () => {
element.tabIndex = -1;
document.body.appendChild(element);
expect(element.getAttribute('tabindex'))
.withContext('tabindex attribute value')
.toEqual('-1');
.withContext('tabindex attribute value')
.toEqual('-1');
element.remove();
});
@ -92,24 +99,23 @@ describe('aria', () => {
element.remove();
document.body.appendChild(element);
expect(element.getAttribute('tabindex'))
.withContext('tabindex attribute value')
.toEqual('-1');
.withContext('tabindex attribute value')
.toEqual('-1');
});
if (!('role' in Element.prototype)) {
describe('polyfill', () => {
it('should hydrate aria attributes when ARIAMixin is not supported',
async () => {
const element = new TestElement();
document.body.appendChild(element);
element.role = 'button';
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value')
.toEqual('button');
it('should hydrate aria attributes when ARIAMixin is not supported', async () => {
const element = new TestElement();
document.body.appendChild(element);
element.role = 'button';
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value')
.toEqual('button');
element.remove();
});
element.remove();
});
});
}
});
@ -138,8 +144,8 @@ describe('aria', () => {
const element = new TestElement();
document.body.appendChild(element);
expect(element.hasAttribute('role'))
.withContext('has role attribute')
.toBeFalse();
.withContext('has role attribute')
.toBeFalse();
element.remove();
});
@ -148,43 +154,41 @@ describe('aria', () => {
const element = new TestElement();
// TestElement() sets role in constructor
expect(element.internals.role)
.withContext('ElementInternals.role')
.toEqual('button');
.withContext('ElementInternals.role')
.toEqual('button');
});
it('should preserve aria values when set before connected', () => {
const element = new TestElement();
element.internals.ariaLabel = 'Foo';
expect(element.internals.ariaLabel)
.withContext('ElementInternals.ariaLabel')
.toEqual('Foo');
.withContext('ElementInternals.ariaLabel')
.toEqual('Foo');
});
it('should hydrate role attributes when set before connection',
async () => {
const element = new TestElement();
// TestElement() sets role in constructor
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value')
.toEqual('button');
it('should hydrate role attributes when set before connection', async () => {
const element = new TestElement();
// TestElement() sets role in constructor
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value')
.toEqual('button');
element.remove();
});
element.remove();
});
it('should hydrate aria attributes when set before connection',
async () => {
const element = new TestElement();
element.internals.ariaLabel = 'Foo';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value')
.toEqual('Foo');
it('should hydrate aria attributes when set before connection', async () => {
const element = new TestElement();
element.internals.ariaLabel = 'Foo';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value')
.toEqual('Foo');
element.remove();
});
element.remove();
});
it('should set aria attributes when set after connection', async () => {
const element = new TestElement();
@ -192,180 +196,171 @@ describe('aria', () => {
element.internals.ariaLabel = 'Value after construction';
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value')
.toEqual('Value after construction');
.withContext('aria-label attribute value')
.toEqual('Value after construction');
element.remove();
});
it('should not override aria attributes on host when set before connection',
async () => {
const element = new TestElement();
element.setAttribute('aria-label', 'Value set by user');
element.internals.ariaLabel = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value on host')
.toEqual('Value set by user');
expect(element.internals.ariaLabel)
.withContext('ariaLabel internals property still the same')
.toEqual('Value set on internals');
it('should not override aria attributes on host when set before connection', async () => {
const element = new TestElement();
element.setAttribute('aria-label', 'Value set by user');
element.internals.ariaLabel = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value on host')
.toEqual('Value set by user');
expect(element.internals.ariaLabel)
.withContext('ariaLabel internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
element.remove();
});
it('should not override aria properties on host when set before connection',
async () => {
const element = new TestElement();
element.ariaLabel = 'Value set by user';
element.internals.ariaLabel = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value on host')
.toEqual('Value set by user');
expect(element.ariaLabel)
.withContext('ariaLabel property value on host')
.toEqual('Value set by user');
expect(element.internals.ariaLabel)
.withContext('ariaLabel internals property still the same')
.toEqual('Value set on internals');
it('should not override aria properties on host when set before connection', async () => {
const element = new TestElement();
element.ariaLabel = 'Value set by user';
element.internals.ariaLabel = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('aria-label'))
.withContext('aria-label attribute value on host')
.toEqual('Value set by user');
expect(element.ariaLabel)
.withContext('ariaLabel property value on host')
.toEqual('Value set by user');
expect(element.internals.ariaLabel)
.withContext('ariaLabel internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
element.remove();
});
it('should not override role attribute on host when set before connection',
async () => {
const element = new TestElement();
element.setAttribute('role', 'Value set by user');
element.internals.role = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value on host')
.toEqual('Value set by user');
expect(element.internals.role)
.withContext('role internals property still the same')
.toEqual('Value set on internals');
it('should not override role attribute on host when set before connection', async () => {
const element = new TestElement();
element.setAttribute('role', 'Value set by user');
element.internals.role = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value on host')
.toEqual('Value set by user');
expect(element.internals.role)
.withContext('role internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
element.remove();
});
it('should not override role property on host when set before connection',
async () => {
const element = new TestElement();
element.role = 'Value set by user';
element.internals.role = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value on host')
.toEqual('Value set by user');
expect(element.role)
.withContext('role property value on host')
.toEqual('Value set by user');
expect(element.internals.role)
.withContext('role internals property still the same')
.toEqual('Value set on internals');
it('should not override role property on host when set before connection', async () => {
const element = new TestElement();
element.role = 'Value set by user';
element.internals.role = 'Value set on internals';
document.body.appendChild(element);
await element.updateComplete;
expect(element.getAttribute('role'))
.withContext('role attribute value on host')
.toEqual('Value set by user');
expect(element.role)
.withContext('role property value on host')
.toEqual('Value set by user');
expect(element.internals.role)
.withContext('role internals property still the same')
.toEqual('Value set on internals');
element.remove();
});
element.remove();
});
it('should handle setting role multiple times before connection',
async () => {
const element = new TestElement();
element.internals.role = 'button';
element.internals.role = 'checkbox';
it('should handle setting role multiple times before connection', async () => {
const element = new TestElement();
element.internals.role = 'button';
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role before connection')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after connection')
.toEqual('checkbox');
expect(element.internals.role)
.withContext('internals.role before connection')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after connection')
.toEqual('checkbox');
element.remove();
});
element.remove();
});
it('should handle setting role multiple times before connection when property is set on host',
async () => {
const element = new TestElement();
element.role = 'radio';
element.internals.role = 'button';
element.internals.role = 'checkbox';
it('should handle setting role multiple times before connection when property is set on host', async () => {
const element = new TestElement();
element.role = 'radio';
element.internals.role = 'button';
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role before connection')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after connection')
.toEqual('checkbox');
expect(element.internals.role)
.withContext('internals.role before connection')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after connection')
.toEqual('checkbox');
element.remove();
});
element.remove();
});
it('should handle setting aria properties multiple times before connection',
async () => {
const element = new TestElement();
element.internals.ariaLabel = 'First';
element.internals.ariaLabel = 'Second';
it('should handle setting aria properties multiple times before connection', async () => {
const element = new TestElement();
element.internals.ariaLabel = 'First';
element.internals.ariaLabel = 'Second';
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel before connection')
.toEqual('Second');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel after connection')
.toEqual('Second');
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel before connection')
.toEqual('Second');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel after connection')
.toEqual('Second');
element.remove();
});
element.remove();
});
it('should handle setting aria properties multiple times before connection when property is set on host',
async () => {
const element = new TestElement();
element.ariaLabel = 'First';
element.internals.ariaLabel = 'First';
element.internals.ariaLabel = 'Second';
it('should handle setting aria properties multiple times before connection when property is set on host', async () => {
const element = new TestElement();
element.ariaLabel = 'First';
element.internals.ariaLabel = 'First';
element.internals.ariaLabel = 'Second';
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel before connection')
.toEqual('Second');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel after connection')
.toEqual('Second');
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel before connection')
.toEqual('Second');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.ariaLabel)
.withContext('internals.ariaLabel after connection')
.toEqual('Second');
element.remove();
});
element.remove();
});
it('should handle setting role after first connection while disconnected',
async () => {
const element = new TestElement();
element.internals.role = 'button';
document.body.appendChild(element);
await element.updateComplete;
it('should handle setting role after first connection while disconnected', async () => {
const element = new TestElement();
element.internals.role = 'button';
document.body.appendChild(element);
await element.updateComplete;
element.remove();
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role after connected and disconnected')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after reconnected')
.toEqual('checkbox');
element.remove();
element.internals.role = 'checkbox';
expect(element.internals.role)
.withContext('internals.role after connected and disconnected')
.toEqual('checkbox');
document.body.appendChild(element);
await element.updateComplete;
expect(element.internals.role)
.withContext('internals.role after reconnected')
.toEqual('checkbox');
element.remove();
});
element.remove();
});
}
});
});

Some files were not shown because too many files have changed in this diff Show More