mirror of
https://github.com/material-components/material-web.git
synced 2024-08-16 17:10:22 +03:00
chore: format files with prettier
PiperOrigin-RevId: 576601342
This commit is contained in:
parent
d09bdc47e6
commit
c390291687
@ -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', [
|
||||
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||
'Button',
|
||||
[
|
||||
new Knob('label', {ui: textInput(), defaultValue: ''}),
|
||||
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
||||
]);
|
||||
],
|
||||
);
|
||||
|
||||
collection.addStories(...materialInitsToStoryInits(stories));
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -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.
|
||||
@ -87,8 +96,8 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
||||
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();
|
||||
|
@ -17,12 +17,9 @@ import {customElement} from 'lit/decorators.js';
|
||||
@customElement('catalog-component-header')
|
||||
export class CatalogComponentHeader extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
return html` <div>
|
||||
<slot class="title" name="title"></slot>
|
||||
<slot
|
||||
class="image"
|
||||
></slot>
|
||||
<slot class="image"></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
@ -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>
|
||||
|
@ -68,15 +68,13 @@ export class HCTSlider extends LitElement {
|
||||
.min=${range[0]}
|
||||
.max=${range[1]}
|
||||
.value=${this.value}
|
||||
@input=${this.onInput}
|
||||
></md-slider>
|
||||
@input=${this.onInput}></md-slider>
|
||||
<div
|
||||
id="gradient"
|
||||
class=${this.type}
|
||||
style=${styleMap({
|
||||
background: this.buildGradient(),
|
||||
})}
|
||||
></div>
|
||||
})}></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);
|
||||
|
@ -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,8 +76,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
||||
duration: drawerSlideAnimationDuration,
|
||||
easing: drawerSlideAnimationEasing,
|
||||
},
|
||||
})}
|
||||
>
|
||||
})}>
|
||||
<div class="scroll-wrapper">
|
||||
<slot
|
||||
${animate({
|
||||
@ -86,8 +85,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
||||
duration: drawerContentOpacityDuration,
|
||||
easing: 'linear',
|
||||
},
|
||||
})}
|
||||
></slot>
|
||||
})}></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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
||||
@ -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}>
|
||||
</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>
|
||||
@ -115,23 +115,20 @@ export class ThemeChanger extends LitElement {
|
||||
type="hue"
|
||||
label="Hue"
|
||||
max="360"
|
||||
@input=${this.onSliderInput}
|
||||
></hct-slider>
|
||||
@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>
|
||||
@input=${this.onSliderInput}></hct-slider>
|
||||
<hct-slider
|
||||
.value=${live(this.tone)}
|
||||
type="tone"
|
||||
label="Tone"
|
||||
max="100"
|
||||
@input=${this.onSliderInput}
|
||||
></hct-slider>
|
||||
@input=${this.onSliderInput}></hct-slider>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@ -141,8 +138,7 @@ export class ThemeChanger extends LitElement {
|
||||
private renderColorModePicker() {
|
||||
return html`<md-outlined-segmented-button-set
|
||||
@segmented-button-set-selection=${this.onColorModeSelection}
|
||||
aria-label="Color mode"
|
||||
>
|
||||
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;
|
||||
|
@ -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>
|
||||
|
@ -4,5 +4,5 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import '@material/web/fab/fab.js';
|
||||
import '@material/web/fab/branded-fab.js';
|
||||
import '@material/web/fab/fab.js';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -4,6 +4,6 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import 'playground-elements/playground-project.js';
|
||||
import 'playground-elements/playground-preview.js';
|
||||
import 'playground-elements/playground-file-editor.js';
|
||||
import 'playground-elements/playground-preview.js';
|
||||
import 'playground-elements/playground-project.js';
|
||||
|
@ -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
|
||||
|
@ -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,7 +38,8 @@ 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)')
|
||||
window
|
||||
.matchMedia('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', () => {
|
||||
if (getCurrentMode() !== 'auto') {
|
||||
return;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -6,7 +6,10 @@
|
||||
|
||||
import {Island} from '@11ty/is-land';
|
||||
|
||||
customElements.define('lit-island', class extends Island {
|
||||
customElements.define(
|
||||
'lit-island',
|
||||
class extends Island {
|
||||
// Removes the feature in which 11ty island removes DOM to render a fallback.
|
||||
override forceFallback() {}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -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';
|
@ -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
|
||||
|
@ -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,10 +38,12 @@ 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"]')
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute('content', surfaceContainer);
|
||||
}
|
||||
|
||||
|
@ -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};`;
|
||||
|
@ -4,8 +4,12 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import {applyMaterialTheme, themeFromSourceColor} from './material-color-helpers.js';
|
||||
import {
|
||||
applyMaterialTheme,
|
||||
themeFromSourceColor,
|
||||
} from './material-color-helpers.js';
|
||||
|
||||
/** Color mode, either overriding light/dark or the user's preference. */
|
||||
export type ColorMode = 'light' | 'dark' | 'auto';
|
||||
|
||||
/**
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -127,8 +132,7 @@ export class KnobColorSelector extends LitElement {
|
||||
<md-outlined-button
|
||||
@click=${() => {
|
||||
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,7 +375,7 @@ 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. */
|
||||
@ -391,8 +391,7 @@ 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>
|
||||
@ -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);
|
||||
|
@ -93,8 +93,9 @@ export class StoriesRenderer extends LitElement {
|
||||
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}
|
||||
@ -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>
|
||||
`;
|
||||
@ -184,7 +184,8 @@ export class StoriesRenderer extends LitElement {
|
||||
if (!allowedStories.has(story)) {
|
||||
console.error(
|
||||
`A stories renderer can only render stories ` +
|
||||
`from its collection.`);
|
||||
`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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
@ -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', {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('open-changed', {
|
||||
detail: {
|
||||
open: this.open,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,10 +181,11 @@ 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 -
|
||||
const bottomBound =
|
||||
window.innerHeight -
|
||||
(DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET);
|
||||
|
||||
// do not allow drag outside right bound
|
||||
|
@ -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';
|
||||
|
@ -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();
|
||||
@ -85,14 +86,17 @@ export class Knob<T, Name extends string = string> extends EventTarget {
|
||||
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 ||
|
||||
if (
|
||||
this.dirty ||
|
||||
(this.latestValue !== undefined &&
|
||||
this.latestValue === this.defaultValue)) {
|
||||
this.latestValue === this.defaultValue)
|
||||
) {
|
||||
this.wiring?.(this, this.latestValue!, 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}`);
|
||||
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -24,14 +24,15 @@ export interface BaseStoryInit {
|
||||
type GenericKnobValues = KnobValues<PolymorphicArrayOfKnobs>;
|
||||
|
||||
/** A story with an arbitrary render function. */
|
||||
export interface StoryInit<
|
||||
KV extends GenericKnobValues = GenericKnobValues> extends BaseStoryInit {
|
||||
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;
|
||||
@ -54,13 +55,16 @@ 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. ` +
|
||||
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;
|
||||
|
||||
@ -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,8 +111,9 @@ 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>>> {
|
||||
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>();
|
||||
@ -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,8 +175,8 @@ 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 {
|
||||
KV extends KnobValues<PolymorphicArrayOfKnobs> = KnobValues<PolymorphicArrayOfKnobs>,
|
||||
> extends BaseStoryInit {
|
||||
renderLit(knobs: KV): TemplateResult | Promise<TemplateResult>;
|
||||
litStyles?: CSSResult | CSSResult[];
|
||||
}
|
||||
@ -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,
|
||||
|
@ -44,7 +44,6 @@ const postdoc = new PostDoc({
|
||||
onMessage,
|
||||
});
|
||||
|
||||
|
||||
await postdoc.handshake;
|
||||
|
||||
// Request the initial theme.
|
||||
|
@ -20,7 +20,7 @@ describe('<md-checkbox>', () => {
|
||||
|
||||
describe('forms', () => {
|
||||
createFormTests({
|
||||
queryControl: root => root.querySelector('md-checkbox'),
|
||||
queryControl: (root) => root.querySelector('md-checkbox'),
|
||||
valueTests: [
|
||||
{
|
||||
name: 'unnamed',
|
||||
@ -29,7 +29,7 @@ describe('<md-checkbox>', () => {
|
||||
expect(formData)
|
||||
.withContext('should not add anything to form without a name')
|
||||
.toHaveSize(0);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'unchecked',
|
||||
@ -38,7 +38,7 @@ describe('<md-checkbox>', () => {
|
||||
expect(formData)
|
||||
.withContext('should not add anything to form when unchecked')
|
||||
.toHaveSize(0);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'checked default value',
|
||||
@ -46,26 +46,31 @@ describe('<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')
|
||||
.withContext('should not add anything to form when indeterminate')
|
||||
.toHaveSize(0);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
@ -75,8 +80,8 @@ describe('<md-checkbox>', () => {
|
||||
expect(formData)
|
||||
.withContext('should not add anything to form when disabled')
|
||||
.toHaveSize(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
resetTests: [
|
||||
{
|
||||
@ -89,7 +94,7 @@ describe('<md-checkbox>', () => {
|
||||
expect(checkbox.checked)
|
||||
.withContext('checkbox.checked after reset')
|
||||
.toBeFalse();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reset to checked',
|
||||
@ -102,7 +107,7 @@ describe('<md-checkbox>', () => {
|
||||
expect(checkbox.checked)
|
||||
.withContext('checkbox.checked after reset')
|
||||
.toBeTrue();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'reset to indeterminate',
|
||||
@ -115,8 +120,8 @@ describe('<md-checkbox>', () => {
|
||||
expect(checkbox.indeterminate)
|
||||
.withContext('checkbox.indeterminate should not be reset')
|
||||
.toBeFalse();
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
restoreTests: [
|
||||
{
|
||||
@ -126,7 +131,7 @@ describe('<md-checkbox>', () => {
|
||||
expect(checkbox.checked)
|
||||
.withContext('checkbox.checked after restore')
|
||||
.toBeFalse();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'restore checked',
|
||||
@ -136,7 +141,7 @@ describe('<md-checkbox>', () => {
|
||||
expect(checkbox.checked)
|
||||
.withContext('checkbox.checked after restore')
|
||||
.toBeTrue();
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'restore indeterminate',
|
||||
@ -146,9 +151,9 @@ describe('<md-checkbox>', () => {
|
||||
expect(checkbox.indeterminate)
|
||||
.withContext('checkbox.indeterminate should not be restored')
|
||||
.toBeFalse();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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', [
|
||||
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));
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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 */
|
||||
@ -130,8 +134,8 @@ export class Checkbox extends LitElement {
|
||||
// Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
|
||||
// Replace with this.internals.validity.customError when resolved.
|
||||
private hasCustomValidityError = false;
|
||||
private readonly internals =
|
||||
(this as HTMLElement /* needed for closure */).attachInternals();
|
||||
// Cast needed for closure
|
||||
private readonly internals = (this as HTMLElement).attachInternals();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@ -196,8 +200,11 @@ 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 =
|
||||
@ -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() {
|
||||
|
@ -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,8 +85,7 @@ describe('checkbox', () => {
|
||||
});
|
||||
|
||||
describe('checked', () => {
|
||||
it('get/set updates the checked property on the native checkbox element',
|
||||
async () => {
|
||||
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();
|
||||
@ -95,8 +95,7 @@ describe('checkbox', () => {
|
||||
expect(input.checked).toEqual(false);
|
||||
});
|
||||
|
||||
it('get/set updates the checked property after user updates checked state',
|
||||
async () => {
|
||||
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.
|
||||
@ -120,8 +119,7 @@ describe('checkbox', () => {
|
||||
});
|
||||
|
||||
describe('indeterminate', () => {
|
||||
it('get/set updates the indeterminate property on the native checkbox element',
|
||||
async () => {
|
||||
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();
|
||||
@ -138,8 +136,7 @@ describe('checkbox', () => {
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
it('get/set updates the disabled property on the native checkbox element',
|
||||
async () => {
|
||||
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();
|
||||
@ -153,14 +150,12 @@ describe('checkbox', () => {
|
||||
|
||||
describe('form submission', () => {
|
||||
async function setupFormTest(propsInit: Partial<Checkbox> = {}) {
|
||||
return await setupTest(html`
|
||||
<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>
|
||||
.value=${propsInit.value ?? ''}></md-test-checkbox>
|
||||
</form>`);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
@ -199,13 +200,17 @@ describe('checkbox', () => {
|
||||
<md-test-checkbox></md-test-checkbox>
|
||||
</label>
|
||||
`);
|
||||
const label = (test.harness.element.getRootNode() as HTMLElement)
|
||||
.querySelector<HTMLLabelElement>('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();
|
||||
|
@ -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', [
|
||||
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));
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -17,11 +17,13 @@ export class ChipHarness extends Harness<Chip> {
|
||||
protected override async getInteractiveElement() {
|
||||
await this.element.updateComplete;
|
||||
const {primaryId} = this.element as unknown as {primaryId: string};
|
||||
const primaryAction = 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;
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
@ -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
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,8 +58,11 @@ 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')
|
||||
@ -75,12 +75,18 @@ describe('Chip set', () => {
|
||||
.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
|
||||
@ -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,12 +157,11 @@ 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 () => {
|
||||
it('should navigate to first chip on forward when none focused', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
const third = new TestAssistChip();
|
||||
@ -166,12 +171,11 @@ describe('Chip set', () => {
|
||||
ltrKey: 'ArrowRight',
|
||||
rtlKey: 'ArrowLeft',
|
||||
current: null,
|
||||
next: first
|
||||
next: first,
|
||||
});
|
||||
});
|
||||
|
||||
it('should navigate to last chip on backward when none focused',
|
||||
async () => {
|
||||
it('should navigate to last chip on backward when none focused', async () => {
|
||||
const first = new TestAssistChip();
|
||||
const second = new TestAssistChip();
|
||||
const third = new TestAssistChip();
|
||||
@ -181,7 +185,7 @@ describe('Chip set', () => {
|
||||
ltrKey: 'ArrowLeft',
|
||||
rtlKey: 'ArrowRight',
|
||||
current: null,
|
||||
next: third
|
||||
next: third,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,8 +230,9 @@ 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();
|
||||
|
@ -24,7 +24,7 @@ export abstract class Chip extends LitElement {
|
||||
/** @nocollapse */
|
||||
static override shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -93,9 +93,9 @@ 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}
|
||||
<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())}
|
||||
`;
|
||||
|
@ -33,8 +33,7 @@ describe('Chip', () => {
|
||||
return {chip, harness: new ChipHarness(chip)};
|
||||
}
|
||||
|
||||
it('should dispatch `update-focus` for chip set when disabled changes',
|
||||
async () => {
|
||||
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);
|
||||
|
@ -27,7 +27,8 @@ 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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -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', () => {
|
||||
trailingAction.addEventListener(
|
||||
'focusout',
|
||||
() => {
|
||||
primaryAction.tabIndex = 0;
|
||||
}, {once: true});
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -132,8 +136,7 @@ describe('Multi-action chips', () => {
|
||||
expect(keydownHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should bubble event when navigating forward past trailing action',
|
||||
async () => {
|
||||
it('should bubble event when navigating forward past trailing action', async () => {
|
||||
const chip = await setupTest();
|
||||
const trailingHarness = new ChipHarness(chip);
|
||||
trailingHarness.action = 'trailing';
|
||||
@ -149,8 +152,7 @@ describe('Multi-action chips', () => {
|
||||
expect(keydownHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should bubble event when navigating backward before primary action',
|
||||
async () => {
|
||||
it('should bubble event when navigating backward before primary action', async () => {
|
||||
const chip = await setupTest();
|
||||
const primaryHarness = new ChipHarness(chip);
|
||||
const keydownHandler = jasmine.createSpy();
|
||||
@ -205,12 +207,11 @@ describe('Multi-action chips', () => {
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not remove chip if "remove" event is default prevented',
|
||||
async () => {
|
||||
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 => {
|
||||
chip.addEventListener('remove', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
@ -227,8 +228,7 @@ describe('Multi-action chips', () => {
|
||||
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.label}`);
|
||||
});
|
||||
|
||||
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided',
|
||||
async () => {
|
||||
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', async () => {
|
||||
const chip = await setupTest();
|
||||
chip.label = 'Label';
|
||||
chip.ariaLabel = 'Descriptive label';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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', [
|
||||
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()}),
|
||||
]);
|
||||
new Knob('supportingText', {
|
||||
defaultValue: 'Just a simple dialog.',
|
||||
ui: textInput(),
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
collection.addStories(...materialInitsToStoryInits(stories));
|
||||
|
||||
|
@ -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,
|
||||
];
|
||||
|
@ -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', () => {
|
||||
const closedPromise = new Promise<void>((resolve) => {
|
||||
harness.element.addEventListener(
|
||||
'closed',
|
||||
() => {
|
||||
resolve();
|
||||
}, {once: true});
|
||||
},
|
||||
{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,8 +137,7 @@ describe('<md-dialog>', () => {
|
||||
expect(isClosing).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('focuses element with autofocus when shown and previously focused element when closed',
|
||||
async () => {
|
||||
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);
|
||||
@ -166,7 +170,7 @@ describe('<md-dialog>', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
@ -196,8 +200,7 @@ describe('<md-dialog>', () => {
|
||||
.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not open on connected if opened, but closed before connected to DOM',
|
||||
async () => {
|
||||
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);
|
||||
@ -213,8 +216,7 @@ describe('<md-dialog>', () => {
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not open on connected if opened before connection but closed after',
|
||||
async () => {
|
||||
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);
|
||||
@ -226,7 +228,8 @@ describe('<md-dialog>', () => {
|
||||
await env.waitForStability();
|
||||
expect(openListener)
|
||||
.withContext(
|
||||
'should not open on connected since close was called before open could complete')
|
||||
'should not open on connected since close was called before open could complete',
|
||||
)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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'},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
@ -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;
|
||||
@ -236,17 +243,15 @@ 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"
|
||||
<slot
|
||||
name="headline"
|
||||
@slotchange=${this.handleHeadlineChange}></slot>
|
||||
</h2>
|
||||
<md-divider></md-divider>
|
||||
@ -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 => {
|
||||
this.intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
this.handleAnchorIntersection(entry);
|
||||
}
|
||||
}, {root: this.scroller!});
|
||||
},
|
||||
{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;
|
||||
});
|
||||
}
|
||||
|
@ -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', [
|
||||
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));
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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'},
|
||||
]
|
||||
})
|
||||
],
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
@ -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. */
|
||||
@ -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. */
|
||||
|
@ -156,7 +156,8 @@ describe('<md-branded-fab>', () => {
|
||||
const env = new Environment();
|
||||
|
||||
async function setupTest() {
|
||||
const element = env.render(html`<md-branded-fab></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>.');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
`;
|
||||
}
|
||||
@ -92,8 +88,9 @@ export abstract class SharedFab extends LitElement {
|
||||
return html`<span class="icon">
|
||||
<slot
|
||||
name="icon"
|
||||
aria-hidden=${
|
||||
ariaLabel || this.label ? 'true' : nothing as unknown as 'false'}>
|
||||
aria-hidden=${ariaLabel || this.label
|
||||
? 'true'
|
||||
: (nothing as unknown as 'false')}>
|
||||
<span></span>
|
||||
</slot>
|
||||
</span>`;
|
||||
|
@ -4,20 +4,32 @@
|
||||
* 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', [
|
||||
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('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}),
|
||||
@ -29,7 +41,8 @@ const collection =
|
||||
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));
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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,7 +67,8 @@ export class Field extends LitElement {
|
||||
*/
|
||||
@state() private refreshErrorAlert = false;
|
||||
@state() private disableTransitions = false;
|
||||
@query('.label.floating') private readonly floatingLabelEl!: 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;
|
||||
|
||||
@ -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,9 +189,9 @@ 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
|
||||
@ -193,8 +202,9 @@ export class Field extends LitElement {
|
||||
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 +
|
||||
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},
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
@ -292,7 +294,8 @@ describe('Field', () => {
|
||||
// Assertion.
|
||||
expect(instance.labelText)
|
||||
.withContext(
|
||||
'label text should be empty string if label is not provided')
|
||||
'label text should be empty string if label is not provided',
|
||||
)
|
||||
.toBe('');
|
||||
});
|
||||
|
||||
@ -315,19 +318,20 @@ describe('Field', () => {
|
||||
// Assertion.
|
||||
expect(instance.labelText)
|
||||
.withContext(
|
||||
'label text should equal label with asterisk when required')
|
||||
'label text should equal label with asterisk when required',
|
||||
)
|
||||
.toBe(`${labelValue}*`);
|
||||
});
|
||||
|
||||
it('should not render asterisk if required when there is no label',
|
||||
async () => {
|
||||
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')
|
||||
'label text should be empty string if label is not provided, even when required',
|
||||
)
|
||||
.toBe('');
|
||||
});
|
||||
});
|
||||
@ -335,18 +339,22 @@ describe('Field', () => {
|
||||
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()')
|
||||
@ -365,8 +373,10 @@ describe('Field', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -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.
|
||||
@ -45,8 +48,10 @@ export class FocusRing extends LitElement implements Attachable {
|
||||
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);
|
||||
|
@ -64,8 +64,7 @@ describe('focus ring', () => {
|
||||
expect(focusRing.control).withContext('focusRing.control').toBe(button);
|
||||
});
|
||||
|
||||
it('should update a referenced element when for attribute changes',
|
||||
async () => {
|
||||
it('should update a referenced element when for attribute changes', async () => {
|
||||
const {root, focusRing} = setupTest(html`
|
||||
<button id="first"></button>
|
||||
<button id="second"></button>
|
||||
|
@ -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,8 +57,8 @@ 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 -->
|
||||
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')!;
|
||||
|
||||
|
@ -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', [
|
||||
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));
|
||||
|
||||
|
@ -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. */
|
||||
|
@ -44,7 +44,8 @@ describe('icon button tests', () => {
|
||||
});
|
||||
|
||||
describe('md-icon-button', () => {
|
||||
it('setting `disabled` updates the disabled attribute on the native ' +
|
||||
it(
|
||||
'setting `disabled` updates the disabled attribute on the native ' +
|
||||
'button element',
|
||||
async () => {
|
||||
const {element} = await setUpTest('button');
|
||||
@ -57,9 +58,11 @@ describe('icon button tests', () => {
|
||||
element.disabled = false;
|
||||
await element.updateComplete;
|
||||
expect(button.hasAttribute('disabled')).toBeFalse();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('setting `ariaLabel` updates the aria-label attribute on the native ' +
|
||||
it(
|
||||
'setting `ariaLabel` updates the aria-label attribute on the native ' +
|
||||
'button element',
|
||||
async () => {
|
||||
const {element} = await setUpTest('button');
|
||||
@ -68,11 +71,13 @@ describe('icon button tests', () => {
|
||||
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' +
|
||||
it(
|
||||
'setting `ariaLabel` updates the aria-label attribute on the anchor' +
|
||||
'tag',
|
||||
async () => {
|
||||
const {element} = await setUpTest('link');
|
||||
@ -82,11 +87,13 @@ describe('icon button tests', () => {
|
||||
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 ' +
|
||||
it(
|
||||
'setting `disabled` updates the disabled attribute on the native ' +
|
||||
'button element',
|
||||
async () => {
|
||||
const {element} = await setUpTest('toggle');
|
||||
@ -99,9 +106,11 @@ describe('icon button tests', () => {
|
||||
element.disabled = false;
|
||||
await element.updateComplete;
|
||||
expect(button.hasAttribute('disabled')).toBeFalse();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('setting `ariaLabel` updates the aria-label attribute on the native ' +
|
||||
it(
|
||||
'setting `ariaLabel` updates the aria-label attribute on the native ' +
|
||||
'button element',
|
||||
async () => {
|
||||
const {element} = await setUpTest('toggle');
|
||||
@ -110,7 +119,8 @@ describe('icon button tests', () => {
|
||||
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,8 +145,7 @@ describe('icon button tests', () => {
|
||||
expect(changeEvent).toBeTrue();
|
||||
});
|
||||
|
||||
it('setting `selected` updates the aria-pressed attribute on the native button element',
|
||||
async () => {
|
||||
it('setting `selected` updates the aria-pressed attribute on the native button element', async () => {
|
||||
const {element} = await setUpTest('toggle');
|
||||
|
||||
element.selected = true;
|
||||
@ -167,8 +176,7 @@ describe('icon button tests', () => {
|
||||
});
|
||||
|
||||
it('if `flipsIconInRtl=true`, flips icon in an RTL context', async () => {
|
||||
const template = html`
|
||||
<div dir="rtl">
|
||||
const template = html` <div dir="rtl">
|
||||
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
||||
star
|
||||
</md-icon-button>
|
||||
@ -179,10 +187,8 @@ describe('icon button tests', () => {
|
||||
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">
|
||||
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>
|
||||
@ -190,14 +196,13 @@ describe('icon button tests', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -10,12 +10,16 @@ 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';
|
||||
@ -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.
|
||||
@ -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.
|
||||
@ -121,16 +127,17 @@ export class IconButton extends LitElement implements FormSubmitter {
|
||||
const ariaPressedValue = !this.toggle ? nothing : this.selected;
|
||||
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}));
|
||||
|
@ -87,19 +87,24 @@ export function isAriaAttribute(attribute: string): attribute is ARIAAttribute {
|
||||
* @return The aria attribute.
|
||||
*/
|
||||
export function ariaPropertyToAttribute<K extends ARIAProperty | 'role'>(
|
||||
property: K) {
|
||||
return property
|
||||
property: K,
|
||||
) {
|
||||
return (
|
||||
property
|
||||
.replace('aria', 'aria-')
|
||||
// IDREF attributes also include an "Element" or "Elements" suffix
|
||||
.replace(/Elements?/g, '')
|
||||
.toLowerCase() as ARIAPropertyToAttribute<K>;
|
||||
.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
|
||||
@ -125,10 +130,26 @@ export interface ARIAMixinStrict extends ARIAMixin {
|
||||
ariaColCount: `${number}` | null;
|
||||
ariaColIndex: `${number}` | null;
|
||||
ariaColSpan: `${number}` | null;
|
||||
ariaCurrent: 'page'|'step'|'location'|'date'|'time'|'true'|'false'|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;
|
||||
ariaHasPopup:
|
||||
| 'false'
|
||||
| 'true'
|
||||
| 'menu'
|
||||
| 'listbox'
|
||||
| 'tree'
|
||||
| 'grid'
|
||||
| 'dialog'
|
||||
| null;
|
||||
ariaHidden: 'true' | 'false' | null;
|
||||
ariaInvalid: 'true' | 'false' | null;
|
||||
ariaKeyShortcuts: string | null;
|
||||
@ -162,24 +183,116 @@ export interface ARIAMixinStrict extends ARIAMixin {
|
||||
* 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,8 +388,10 @@ 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
|
||||
@ -288,8 +407,10 @@ export function polyfillElementInternalsAria(
|
||||
const setValue = () => {
|
||||
internalAriaValue = value;
|
||||
if (!hasBeenConnected) {
|
||||
firstConnectedCallbacks.push(
|
||||
{property: ariaProperty, callback: setValue});
|
||||
firstConnectedCallbacks.push({
|
||||
property: ariaProperty,
|
||||
callback: setValue,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
@ -9,7 +9,13 @@
|
||||
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()', () => {
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@ -98,8 +105,7 @@ describe('aria', () => {
|
||||
|
||||
if (!('role' in Element.prototype)) {
|
||||
describe('polyfill', () => {
|
||||
it('should hydrate aria attributes when ARIAMixin is not supported',
|
||||
async () => {
|
||||
it('should hydrate aria attributes when ARIAMixin is not supported', async () => {
|
||||
const element = new TestElement();
|
||||
document.body.appendChild(element);
|
||||
element.role = 'button';
|
||||
@ -160,8 +166,7 @@ describe('aria', () => {
|
||||
.toEqual('Foo');
|
||||
});
|
||||
|
||||
it('should hydrate role attributes when set before connection',
|
||||
async () => {
|
||||
it('should hydrate role attributes when set before connection', async () => {
|
||||
const element = new TestElement();
|
||||
// TestElement() sets role in constructor
|
||||
document.body.appendChild(element);
|
||||
@ -173,8 +178,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should hydrate aria attributes when set before connection',
|
||||
async () => {
|
||||
it('should hydrate aria attributes when set before connection', async () => {
|
||||
const element = new TestElement();
|
||||
element.internals.ariaLabel = 'Foo';
|
||||
document.body.appendChild(element);
|
||||
@ -198,8 +202,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should not override aria attributes on host when set before connection',
|
||||
async () => {
|
||||
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';
|
||||
@ -215,8 +218,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should not override aria properties on host when set before connection',
|
||||
async () => {
|
||||
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';
|
||||
@ -235,8 +237,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should not override role attribute on host when set before connection',
|
||||
async () => {
|
||||
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';
|
||||
@ -252,8 +253,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should not override role property on host when set before connection',
|
||||
async () => {
|
||||
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';
|
||||
@ -272,8 +272,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should handle setting role multiple times before connection',
|
||||
async () => {
|
||||
it('should handle setting role multiple times before connection', async () => {
|
||||
const element = new TestElement();
|
||||
element.internals.role = 'button';
|
||||
element.internals.role = 'checkbox';
|
||||
@ -290,8 +289,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should handle setting role multiple times before connection when property is set on host',
|
||||
async () => {
|
||||
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';
|
||||
@ -309,8 +307,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should handle setting aria properties multiple times before connection',
|
||||
async () => {
|
||||
it('should handle setting aria properties multiple times before connection', async () => {
|
||||
const element = new TestElement();
|
||||
element.internals.ariaLabel = 'First';
|
||||
element.internals.ariaLabel = 'Second';
|
||||
@ -327,8 +324,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should handle setting aria properties multiple times before connection when property is set on host',
|
||||
async () => {
|
||||
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';
|
||||
@ -346,8 +342,7 @@ describe('aria', () => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
it('should handle setting role after first connection while disconnected',
|
||||
async () => {
|
||||
it('should handle setting role after first connection while disconnected', async () => {
|
||||
const element = new TestElement();
|
||||
element.internals.role = 'button';
|
||||
document.body.appendChild(element);
|
||||
|
@ -46,11 +46,11 @@ export function requestUpdateOnAriaChange(ctor: typeof ReactiveElement) {
|
||||
});
|
||||
}
|
||||
|
||||
ctor.addInitializer(element => {
|
||||
ctor.addInitializer((element) => {
|
||||
const controller = {
|
||||
hostConnected() {
|
||||
element.setAttribute('role', 'presentation');
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
element.addController(controller);
|
||||
|
@ -37,8 +37,8 @@ describe('aria', () => {
|
||||
|
||||
async function setupTest({ariaLabel}: {ariaLabel?: string} = {}) {
|
||||
const root = env.render(html`
|
||||
<test-aria-delegate aria-label=${
|
||||
ariaLabel || nothing}></test-aria-delegate>
|
||||
<test-aria-delegate
|
||||
aria-label=${ariaLabel || nothing}></test-aria-delegate>
|
||||
`);
|
||||
|
||||
const host = root.querySelector('test-aria-delegate');
|
||||
@ -82,8 +82,7 @@ describe('aria', () => {
|
||||
.toEqual(ariaLabel);
|
||||
});
|
||||
|
||||
it('should update delegated aria attributes when host attribute changes',
|
||||
async () => {
|
||||
it('should update delegated aria attributes when host attribute changes', async () => {
|
||||
const {host, child} = await setupTest({ariaLabel: 'First aria label'});
|
||||
|
||||
host.setAttribute('aria-label', 'Second aria label');
|
||||
@ -93,8 +92,7 @@ describe('aria', () => {
|
||||
.toEqual('Second aria label');
|
||||
});
|
||||
|
||||
it('should remove delegated aria attributes when host attribute is removed',
|
||||
async () => {
|
||||
it('should remove delegated aria attributes when host attribute is removed', async () => {
|
||||
const {host, child} = await setupTest({ariaLabel: 'First aria label'});
|
||||
|
||||
host.removeAttribute('aria-label');
|
||||
|
@ -81,12 +81,13 @@ if (!isServer) {
|
||||
* `Attachable` elements. If the `for` attribute changes, the controller will
|
||||
* re-attach to the new referenced element.
|
||||
*/
|
||||
FOR_ATTRIBUTE_OBSERVER = new MutationObserver(records => {
|
||||
FOR_ATTRIBUTE_OBSERVER = new MutationObserver((records) => {
|
||||
for (const record of records) {
|
||||
// When a control's `for` attribute changes, inform its
|
||||
// `AttachableController` to update to a new control.
|
||||
(record.target as AttachableControllerHost)[ATTACHABLE_CONTROLLER]
|
||||
?.hostConnected();
|
||||
(record.target as AttachableControllerHost)[
|
||||
ATTACHABLE_CONTROLLER
|
||||
]?.hostConnected();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -131,8 +132,9 @@ export class AttachableController implements ReactiveController, Attachable {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (this.host.getRootNode() as Document | ShadowRoot)
|
||||
.querySelector<HTMLElement>(`#${this.htmlFor}`);
|
||||
return (
|
||||
this.host.getRootNode() as Document | ShadowRoot
|
||||
).querySelector<HTMLElement>(`#${this.htmlFor}`);
|
||||
}
|
||||
|
||||
return this.currentControl || this.host.parentElement;
|
||||
@ -157,8 +159,11 @@ export class AttachableController implements ReactiveController, Attachable {
|
||||
*/
|
||||
constructor(
|
||||
private readonly host: AttachableControllerHost,
|
||||
private readonly onControlChange:
|
||||
(prev: HTMLElement|null, next: HTMLElement|null) => void) {
|
||||
private readonly onControlChange: (
|
||||
prev: HTMLElement | null,
|
||||
next: HTMLElement | null,
|
||||
) => void,
|
||||
) {
|
||||
host.addController(this);
|
||||
host[ATTACHABLE_CONTROLLER] = this;
|
||||
FOR_ATTRIBUTE_OBSERVER?.observe(host, {attributeFilter: ['for']});
|
||||
|
@ -6,14 +6,19 @@
|
||||
|
||||
// import 'jasmine'; (google3-only)
|
||||
|
||||
import {dispatchActivationClick, isActivationClick, redispatchEvent} from './events.js';
|
||||
import {
|
||||
dispatchActivationClick,
|
||||
isActivationClick,
|
||||
redispatchEvent,
|
||||
} from './events.js';
|
||||
|
||||
describe('events', () => {
|
||||
let instance: HTMLDivElement;
|
||||
|
||||
beforeEach(() => {
|
||||
instance = document.createElement('div');
|
||||
instance.attachShadow({mode: 'open'})
|
||||
instance
|
||||
.attachShadow({mode: 'open'})
|
||||
.append(document.createElement('slot'));
|
||||
// To have event.target set correctly, the EventTarget instance must be
|
||||
// attached to the DOM.
|
||||
@ -38,7 +43,8 @@ describe('events', () => {
|
||||
.not.toBe(event);
|
||||
expect(redispatchedEvent.target)
|
||||
.withContext(
|
||||
'target should be the instance that redispatched the event')
|
||||
'target should be the instance that redispatched the event',
|
||||
)
|
||||
.toBe(instance);
|
||||
expect(redispatchedEvent.type)
|
||||
.withContext('should be the same event type')
|
||||
@ -77,8 +83,9 @@ describe('events', () => {
|
||||
|
||||
it('should preventDefault() on the original event if canceled', () => {
|
||||
const event = new Event('foo', {cancelable: true});
|
||||
const fooHandler =
|
||||
jasmine.createSpy('fooHandler').and.callFake((event: Event) => {
|
||||
const fooHandler = jasmine
|
||||
.createSpy('fooHandler')
|
||||
.and.callFake((event: Event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
instance.addEventListener('foo', fooHandler);
|
||||
@ -119,7 +126,8 @@ describe('events', () => {
|
||||
listener.and.callThrough();
|
||||
instance.addEventListener('click', listener);
|
||||
instance.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, composed: true}));
|
||||
new MouseEvent('click', {bubbles: true, composed: true}),
|
||||
);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener.calls.mostRecent().returnValue).toBe(true);
|
||||
});
|
||||
@ -131,7 +139,8 @@ describe('events', () => {
|
||||
const innerEl = document.createElement('div');
|
||||
instance.shadowRoot!.append(innerEl);
|
||||
innerEl.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, composed: true}));
|
||||
new MouseEvent('click', {bubbles: true, composed: true}),
|
||||
);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener.calls.mostRecent().returnValue).toBe(false);
|
||||
});
|
||||
@ -144,7 +153,8 @@ describe('events', () => {
|
||||
instance.append(slottedEl);
|
||||
|
||||
slottedEl.dispatchEvent(
|
||||
new MouseEvent('click', {bubbles: true, composed: true}));
|
||||
new MouseEvent('click', {bubbles: true, composed: true}),
|
||||
);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener.calls.mostRecent().returnValue).toBe(false);
|
||||
});
|
||||
@ -162,8 +172,7 @@ describe('events', () => {
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('dispatches an event that cannot be heard outside dispatching scope',
|
||||
() => {
|
||||
it('dispatches an event that cannot be heard outside dispatching scope', () => {
|
||||
const innerEl = document.createElement('div');
|
||||
instance.shadowRoot!.append(innerEl);
|
||||
const listener = jasmine.createSpy('listener');
|
||||
|
@ -51,7 +51,8 @@ export interface FormSubmitter extends ReactiveElement, WithInternals {
|
||||
}
|
||||
|
||||
type FormSubmitterConstructor =
|
||||
(new () => FormSubmitter)|(abstract new () => FormSubmitter);
|
||||
| (new () => FormSubmitter)
|
||||
| (abstract new () => FormSubmitter);
|
||||
|
||||
/**
|
||||
* Sets up an element's constructor to enable form submission. The element
|
||||
@ -82,9 +83,9 @@ export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
|
||||
return;
|
||||
}
|
||||
|
||||
(ctor as unknown as typeof ReactiveElement).addInitializer(instance => {
|
||||
(ctor as unknown as typeof ReactiveElement).addInitializer((instance) => {
|
||||
const submitter = instance as FormSubmitter;
|
||||
submitter.addEventListener('click', async event => {
|
||||
submitter.addEventListener('click', async (event) => {
|
||||
const {type, [internals]: elementInternals} = submitter;
|
||||
const {form} = elementInternals;
|
||||
if (!form || type === 'button') {
|
||||
@ -92,7 +93,7 @@ export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
|
||||
}
|
||||
|
||||
// Wait a microtask for event bubbling to complete.
|
||||
await new Promise<void>(resolve => {
|
||||
await new Promise<void>((resolve) => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
@ -109,13 +110,17 @@ export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
|
||||
// elements. This patches the dispatched submit event to add the correct
|
||||
// `submitter`.
|
||||
// See https://github.com/WICG/webcomponents/issues/814
|
||||
form.addEventListener('submit', submitEvent => {
|
||||
form.addEventListener(
|
||||
'submit',
|
||||
(submitEvent) => {
|
||||
Object.defineProperty(submitEvent, 'submitter', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => submitter,
|
||||
});
|
||||
}, {capture: true, once: true});
|
||||
},
|
||||
{capture: true, once: true},
|
||||
);
|
||||
|
||||
elementInternals.setFormValue(submitter.value);
|
||||
form.requestSubmit();
|
||||
|
@ -36,13 +36,15 @@ class FormSubmitterButton extends LitElement {
|
||||
[internals] = this.attachInternals();
|
||||
}
|
||||
|
||||
|
||||
describe('setupFormSubmitter()', () => {
|
||||
const env = new Environment();
|
||||
|
||||
async function setupTest() {
|
||||
const root = env.render(
|
||||
html`<form><test-form-submitter-button></test-form-submitter-button></form>`);
|
||||
html`<form
|
||||
><test-form-submitter-button></test-form-submitter-button
|
||||
></form>`,
|
||||
);
|
||||
const submitter = root.querySelector('test-form-submitter-button');
|
||||
if (!submitter) {
|
||||
throw new Error(`Could not query rendered <test-form-submitter-button>`);
|
||||
@ -92,9 +94,13 @@ describe('setupFormSubmitter()', () => {
|
||||
|
||||
spyOn(form, 'requestSubmit');
|
||||
|
||||
harness.element.addEventListener('click', (event: Event) => {
|
||||
harness.element.addEventListener(
|
||||
'click',
|
||||
(event: Event) => {
|
||||
event.preventDefault();
|
||||
}, {once: true});
|
||||
},
|
||||
{once: true},
|
||||
);
|
||||
|
||||
await harness.clickWithMouse();
|
||||
|
||||
@ -103,8 +109,9 @@ describe('setupFormSubmitter()', () => {
|
||||
|
||||
it('should set the button as the SubmitEvent submitter', async () => {
|
||||
const {harness, form} = await setupTest();
|
||||
const submitListener =
|
||||
jasmine.createSpy('submitListener').and.callFake((event: Event) => {
|
||||
const submitListener = jasmine
|
||||
.createSpy('submitListener')
|
||||
.and.callFake((event: Event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
@ -121,7 +128,7 @@ describe('setupFormSubmitter()', () => {
|
||||
|
||||
it('should add name/value to form data when present', async () => {
|
||||
const {harness, form} = await setupTest();
|
||||
form.addEventListener('submit', event => {
|
||||
form.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
|
@ -13,6 +13,8 @@
|
||||
* this parameter can be used as a conditional guard. Defaults to `true`.
|
||||
*/
|
||||
export function isRtl(el: HTMLElement, shouldCheck = true) {
|
||||
return shouldCheck &&
|
||||
getComputedStyle(el).getPropertyValue('direction').trim() === 'rtl';
|
||||
return (
|
||||
shouldCheck &&
|
||||
getComputedStyle(el).getPropertyValue('direction').trim() === 'rtl'
|
||||
);
|
||||
}
|
@ -10,5 +10,5 @@ export const stringConverter = {
|
||||
},
|
||||
toAttribute(value: string): string | null {
|
||||
return value || null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -98,10 +98,12 @@ export function createAnimationSignal(): AnimationSignal {
|
||||
export function createThrottle() {
|
||||
const stack = new Set();
|
||||
return async (
|
||||
key = '', cb: (...args: unknown[]) => unknown,
|
||||
key = '',
|
||||
cb: (...args: unknown[]) => unknown,
|
||||
timeout = async () => {
|
||||
await new Promise(requestAnimationFrame);
|
||||
}) => {
|
||||
},
|
||||
) => {
|
||||
if (!stack.has(key)) {
|
||||
stack.add(key);
|
||||
await timeout();
|
||||
|
@ -5,7 +5,12 @@
|
||||
*/
|
||||
|
||||
// import 'jasmine'; (google3-only)
|
||||
import {AnimationSignal, createAnimationSignal, createThrottle, msFromTimeCSSValue} from './animation.js';
|
||||
import {
|
||||
AnimationSignal,
|
||||
createAnimationSignal,
|
||||
createThrottle,
|
||||
msFromTimeCSSValue,
|
||||
} from './animation.js';
|
||||
|
||||
describe('createAnimationSignal()', () => {
|
||||
let task: AnimationSignal;
|
||||
@ -55,14 +60,13 @@ describe('createAnimationSignal()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('createThrottle()', () => {
|
||||
it('throttles calls', async () => {
|
||||
const throttle = createThrottle();
|
||||
const key = 'foo';
|
||||
const fn = jasmine.createSpy();
|
||||
let timeoutResolver: Function | null = null;
|
||||
const timeout = new Promise(r => {
|
||||
const timeout = new Promise((r) => {
|
||||
timeoutResolver = r;
|
||||
});
|
||||
const timeoutFn = async () => {
|
||||
|
@ -47,8 +47,9 @@ const updateTabIndex = Symbol('updateTabIndex');
|
||||
* @param base The class to mix functionality into.
|
||||
* @return The provided class with `Focusable` mixed in.
|
||||
*/
|
||||
export function mixinFocusable<T extends MixinBase<LitElement>>(base: T):
|
||||
MixinReturn<T, Focusable> {
|
||||
export function mixinFocusable<T extends MixinBase<LitElement>>(
|
||||
base: T,
|
||||
): MixinReturn<T, Focusable> {
|
||||
abstract class FocusableElement extends base implements Focusable {
|
||||
@property({reflect: true}) declare tabIndex: number;
|
||||
|
||||
@ -76,7 +77,10 @@ export function mixinFocusable<T extends MixinBase<LitElement>>(base: T):
|
||||
}
|
||||
|
||||
override attributeChangedCallback(
|
||||
name: string, old: string|null, value: string|null) {
|
||||
name: string,
|
||||
old: string | null,
|
||||
value: string | null,
|
||||
) {
|
||||
super.attributeChangedCallback(name, old, value);
|
||||
if (name !== 'tabindex' || this[isUpdatingTabIndex]) {
|
||||
return;
|
||||
|
@ -17,8 +17,7 @@ describe('mixinFocusable()', () => {
|
||||
// tslint:disable-next-line:enforce-name-casing MixinClassCase
|
||||
const FocusableLitElement = mixinFocusable(LitElement);
|
||||
@customElement('test-focusable')
|
||||
class TestFocusable extends FocusableLitElement {
|
||||
}
|
||||
class TestFocusable extends FocusableLitElement {}
|
||||
|
||||
const env = new Environment();
|
||||
|
||||
@ -53,8 +52,7 @@ describe('mixinFocusable()', () => {
|
||||
expect(element.requestUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not override user-set tabindex="0" when isFocusable is false',
|
||||
async () => {
|
||||
it('should not override user-set tabindex="0" when isFocusable is false', async () => {
|
||||
const element = await setupTest();
|
||||
element[isFocusable] = false;
|
||||
element.tabIndex = 0;
|
||||
@ -62,16 +60,14 @@ describe('mixinFocusable()', () => {
|
||||
expect(element.tabIndex).withContext('tabIndex').toBe(0);
|
||||
});
|
||||
|
||||
it('should not override user-set tabindex="-1" when isFocusable is true',
|
||||
async () => {
|
||||
it('should not override user-set tabindex="-1" when isFocusable is true', async () => {
|
||||
const element = await setupTest();
|
||||
element.tabIndex = -1;
|
||||
expect(element[isFocusable]).withContext('isFocusable').toBeTrue();
|
||||
expect(element.tabIndex).withContext('tabIndex').toBe(-1);
|
||||
});
|
||||
|
||||
it('should restore default tabindex when user-set tabindex attribute is removed',
|
||||
async () => {
|
||||
it('should restore default tabindex when user-set tabindex attribute is removed', async () => {
|
||||
const element = await setupTest();
|
||||
element.tabIndex = -1;
|
||||
element.removeAttribute('tabindex');
|
||||
|
@ -26,10 +26,11 @@
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export type MixinBase<ExpectedBase = object> = abstract new (
|
||||
// Mixins must have a constructor with `...args: any[]`
|
||||
// tslint:disable-next-line:no-any
|
||||
export type MixinBase<ExpectedBase = object> = abstract new (...args: any[]) =>
|
||||
ExpectedBase;
|
||||
...args: any[]
|
||||
) => ExpectedBase;
|
||||
|
||||
/**
|
||||
* The return value of a mixin.
|
||||
@ -57,8 +58,6 @@ export type MixinBase<ExpectedBase = object> = abstract new (...args: any[]) =>
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
// Mixins must have a constructor with `...args: any[]`
|
||||
// tslint:disable-next-line:no-any
|
||||
export type MixinReturn<MixinBase, MixinClass = object> =
|
||||
// Mixins must have a constructor with `...args: any[]`
|
||||
// tslint:disable-next-line:no-any
|
||||
|
@ -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>>('Item', [
|
||||
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||
'Item',
|
||||
[
|
||||
new Knob('overline', {ui: textInput()}),
|
||||
new Knob('trailingSupportingText', {ui: textInput()}),
|
||||
new Knob('leadingIcon', {ui: boolInput()}),
|
||||
new Knob('trailingIcon', {ui: boolInput()}),
|
||||
]);
|
||||
],
|
||||
);
|
||||
|
||||
collection.addStories(...materialInitsToStoryInits(stories));
|
||||
|
||||
|
@ -64,10 +64,7 @@ const items: MaterialStoryInit<StoryKnobs> = {
|
||||
render(knobs) {
|
||||
return html`
|
||||
<div class="container">
|
||||
<md-item>
|
||||
Single line item
|
||||
${getKnobContent(knobs)}
|
||||
</md-item>
|
||||
<md-item> Single line item ${getKnobContent(knobs)} </md-item>
|
||||
|
||||
<md-item>
|
||||
Two line item
|
||||
@ -85,7 +82,7 @@ const items: MaterialStoryInit<StoryKnobs> = {
|
||||
</md-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const longText: MaterialStoryInit<StoryKnobs> = {
|
||||
@ -96,9 +93,7 @@ const longText: MaterialStoryInit<StoryKnobs> = {
|
||||
<div class="container">
|
||||
<md-item class="nowrap">
|
||||
Item with a truncated headline and supporting text.
|
||||
<div slot="supporting-text">
|
||||
Supporting text. ${LOREM_IPSUM}
|
||||
</div>
|
||||
<div slot="supporting-text"> Supporting text. ${LOREM_IPSUM} </div>
|
||||
${getKnobContent(knobs)}
|
||||
</md-item>
|
||||
|
||||
@ -112,37 +107,36 @@ const longText: MaterialStoryInit<StoryKnobs> = {
|
||||
|
||||
<md-item>
|
||||
Item that always shows long wrapping text.
|
||||
<div slot="supporting-text">
|
||||
Supporting text. ${LOREM_IPSUM}
|
||||
</div>
|
||||
<div slot="supporting-text"> Supporting text. ${LOREM_IPSUM} </div>
|
||||
${getKnobContent(knobs, /* threeLines */ true)}
|
||||
</md-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function getKnobContent(knobs: StoryKnobs, threeLines = false) {
|
||||
const overline = knobs.overline ?
|
||||
html`<div slot="overline">${knobs.overline}</div>` :
|
||||
nothing;
|
||||
const overline = knobs.overline
|
||||
? html`<div slot="overline">${knobs.overline}</div>`
|
||||
: nothing;
|
||||
|
||||
const classes = {
|
||||
'align-start': threeLines,
|
||||
};
|
||||
|
||||
const trailingText = knobs.trailingSupportingText ?
|
||||
html`<div class=${classMap(classes)} slot="trailing-supporting-text">${
|
||||
knobs.trailingSupportingText}</div>` :
|
||||
nothing;
|
||||
const trailingText = knobs.trailingSupportingText
|
||||
? html`<div class=${classMap(classes)} slot="trailing-supporting-text"
|
||||
>${knobs.trailingSupportingText}</div
|
||||
>`
|
||||
: nothing;
|
||||
|
||||
const leadingIcon = knobs.leadingIcon ?
|
||||
html`<md-icon class=${classMap(classes)} slot="start">event</md-icon>` :
|
||||
nothing;
|
||||
const leadingIcon = knobs.leadingIcon
|
||||
? html`<md-icon class=${classMap(classes)} slot="start">event</md-icon>`
|
||||
: nothing;
|
||||
|
||||
const trailingIcon = knobs.trailingIcon ?
|
||||
html`<md-icon class=${classMap(classes)} slot="end">star</md-icon>` :
|
||||
nothing;
|
||||
const trailingIcon = knobs.trailingIcon
|
||||
? html`<md-icon class=${classMap(classes)} slot="end">star</md-icon>`
|
||||
: nothing;
|
||||
|
||||
return [overline, trailingText, leadingIcon, trailingIcon];
|
||||
}
|
||||
|
@ -27,13 +27,13 @@ export class Item extends LitElement {
|
||||
<slot name="container"></slot>
|
||||
<slot class="non-text" name="start"></slot>
|
||||
<div class="text">
|
||||
<slot name="overline"
|
||||
<slot name="overline" @slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot
|
||||
class="default-slot"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot class="default-slot"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot name="headline"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot name="supporting-text"
|
||||
<slot name="headline" @slotchange=${this.handleTextSlotChange}></slot>
|
||||
<slot
|
||||
name="supporting-text"
|
||||
@slotchange=${this.handleTextSlotChange}></slot>
|
||||
</div>
|
||||
<slot class="non-text" name="trailing-supporting-text"></slot>
|
||||
|
@ -4,23 +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 {boolInput, Knob, textInput} from './index.js';
|
||||
|
||||
import {stories, StoryKnobs} from './stories.js';
|
||||
|
||||
const collection =
|
||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Navigation Bar', [
|
||||
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||
'Navigation Bar',
|
||||
[
|
||||
new Knob('hideInactiveLabels', {ui: boolInput(), defaultValue: false}),
|
||||
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
|
||||
new Knob('showBadge', {ui: boolInput(), defaultValue: false}),
|
||||
new Knob('badgeValue', {ui: textInput(), defaultValue: ''}),
|
||||
new Knob('active icon', {ui: textInput(), defaultValue: 'star'}),
|
||||
new Knob('inactive icon', {ui: textInput(), defaultValue: 'star_border'}),
|
||||
]);
|
||||
],
|
||||
);
|
||||
|
||||
collection.addStories(...materialInitsToStoryInits(stories));
|
||||
|
||||
|
@ -9,7 +9,7 @@ import '@material/web/labs/navigationbar/navigation-bar.js';
|
||||
import '@material/web/labs/navigationtab/navigation-tab.js';
|
||||
|
||||
import {MaterialStoryInit} from './material-collection.js';
|
||||
import {html, css} from 'lit';
|
||||
import {css, html} from 'lit';
|
||||
|
||||
/** Knob types for nav bar stories. */
|
||||
export interface StoryKnobs {
|
||||
@ -27,11 +27,11 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
||||
:host {
|
||||
/* Material Symbols does not include filled star. */
|
||||
--md-icon-font: 'Material Icons';
|
||||
}`,
|
||||
}
|
||||
`,
|
||||
render(knobs) {
|
||||
const {hideInactiveLabels, label, showBadge, badgeValue} = knobs;
|
||||
return html`
|
||||
<div style="width:400px">
|
||||
return html` <div style="width:400px">
|
||||
<md-navigation-bar
|
||||
activeIndex="1"
|
||||
.hideInactiveLabels=${hideInactiveLabels}>
|
||||
@ -68,7 +68,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
||||
</md-navigation-tab>
|
||||
</md-navigation-bar>
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/** Nav Bar stories. */
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user