mirror of
https://github.com/material-components/material-web.git
synced 2024-09-11 13:46:10 +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
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob, textInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Button', [
|
'Button',
|
||||||
|
[
|
||||||
new Knob('label', {ui: textInput(), defaultValue: ''}),
|
new Knob('label', {ui: textInput(), defaultValue: ''}),
|
||||||
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@material/web/icon/icon.js';
|
|
||||||
import '@material/web/button/elevated-button.js';
|
import '@material/web/button/elevated-button.js';
|
||||||
import '@material/web/button/filled-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/outlined-button.js';
|
||||||
import '@material/web/button/text-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 {MaterialStoryInit} from './material-collection.js';
|
||||||
import {css, html} from 'lit';
|
import {css, html} from 'lit';
|
||||||
@ -88,7 +88,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const links: MaterialStoryInit<StoryKnobs> = {
|
const links: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -101,40 +101,35 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-filled-button
|
<md-filled-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
${label || 'Filled'}
|
${label || 'Filled'}
|
||||||
</md-filled-button>
|
</md-filled-button>
|
||||||
|
|
||||||
<md-outlined-button
|
<md-outlined-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
${label || 'Outlined'}
|
${label || 'Outlined'}
|
||||||
</md-outlined-button>
|
</md-outlined-button>
|
||||||
|
|
||||||
<md-elevated-button
|
<md-elevated-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
${label || 'Elevated'}
|
${label || 'Elevated'}
|
||||||
</md-elevated-button>
|
</md-elevated-button>
|
||||||
|
|
||||||
<md-filled-tonal-button
|
<md-filled-tonal-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
${label || 'Tonal'}
|
${label || 'Tonal'}
|
||||||
</md-filled-tonal-button>
|
</md-filled-tonal-button>
|
||||||
|
|
||||||
<md-text-button
|
<md-text-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
${label || 'Text'}
|
${label || 'Text'}
|
||||||
</md-text-button>
|
</md-text-button>
|
||||||
</div>
|
</div>
|
||||||
@ -142,8 +137,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-filled-button
|
<md-filled-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
<md-icon slot="icon">open_in_new</md-icon>
|
<md-icon slot="icon">open_in_new</md-icon>
|
||||||
${label || 'Filled'}
|
${label || 'Filled'}
|
||||||
</md-filled-button>
|
</md-filled-button>
|
||||||
@ -151,8 +145,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-outlined-button
|
<md-outlined-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
<md-icon slot="icon">open_in_new</md-icon>
|
<md-icon slot="icon">open_in_new</md-icon>
|
||||||
${label || 'Outlined'}
|
${label || 'Outlined'}
|
||||||
</md-outlined-button>
|
</md-outlined-button>
|
||||||
@ -160,8 +153,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-elevated-button
|
<md-elevated-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
<md-icon slot="icon">open_in_new</md-icon>
|
<md-icon slot="icon">open_in_new</md-icon>
|
||||||
${label || 'Elevated'}
|
${label || 'Elevated'}
|
||||||
</md-elevated-button>
|
</md-elevated-button>
|
||||||
@ -169,8 +161,7 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-filled-tonal-button
|
<md-filled-tonal-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
<md-icon slot="icon">open_in_new</md-icon>
|
<md-icon slot="icon">open_in_new</md-icon>
|
||||||
${label || 'Tonal'}
|
${label || 'Tonal'}
|
||||||
</md-filled-tonal-button>
|
</md-filled-tonal-button>
|
||||||
@ -178,15 +169,14 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-text-button
|
<md-text-button
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
trailing-icon
|
trailing-icon>
|
||||||
>
|
|
||||||
<md-icon slot="icon">open_in_new</md-icon>
|
<md-icon slot="icon">open_in_new</md-icon>
|
||||||
${label || 'Text'}
|
${label || 'Text'}
|
||||||
</md-text-button>
|
</md-text-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Button stories. */
|
/** Button stories. */
|
||||||
|
@ -41,6 +41,9 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
@customElement('md-elevated-button')
|
@customElement('md-elevated-button')
|
||||||
export class MdElevatedButton extends ElevatedButton {
|
export class MdElevatedButton extends ElevatedButton {
|
||||||
static override styles =
|
static override styles = [
|
||||||
[sharedStyles, sharedElevationStyles, elevatedStyles];
|
sharedStyles,
|
||||||
|
sharedElevationStyles,
|
||||||
|
elevatedStyles,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,20 @@ import '../../ripple/ripple.js';
|
|||||||
import {html, isServer, LitElement, nothing} from 'lit';
|
import {html, isServer, LitElement, nothing} from 'lit';
|
||||||
import {property, query, queryAssignedElements} from 'lit/decorators.js';
|
import {property, query, queryAssignedElements} from 'lit/decorators.js';
|
||||||
import {classMap} from 'lit/directives/class-map.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 {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||||
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
||||||
import {internals} from '../../internal/controller/element-internals.js';
|
import {internals} from '../../internal/controller/element-internals.js';
|
||||||
import {dispatchActivationClick, isActivationClick} from '../../internal/controller/events.js';
|
import {
|
||||||
import {FormSubmitter, FormSubmitterType, setupFormSubmitter} from '../../internal/controller/form-submitter.js';
|
dispatchActivationClick,
|
||||||
|
isActivationClick,
|
||||||
|
} from '../../internal/controller/events.js';
|
||||||
|
import {
|
||||||
|
FormSubmitter,
|
||||||
|
FormSubmitterType,
|
||||||
|
setupFormSubmitter,
|
||||||
|
} from '../../internal/controller/form-submitter.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A button component.
|
* A button component.
|
||||||
@ -31,8 +38,10 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
|||||||
static readonly formAssociated = true;
|
static readonly formAssociated = true;
|
||||||
|
|
||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
static override shadowRootOptions:
|
static override shadowRootOptions: ShadowRootInit = {
|
||||||
ShadowRootInit = {mode: 'open', delegatesFocus: true};
|
mode: 'open',
|
||||||
|
delegatesFocus: true,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the button is disabled.
|
* Whether or not the button is disabled.
|
||||||
@ -87,8 +96,8 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
|||||||
private readonly assignedIcons!: HTMLElement[];
|
private readonly assignedIcons!: HTMLElement[];
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
[internals] =
|
[internals] = (this as HTMLElement) /* needed for closure */
|
||||||
(this as HTMLElement /* needed for closure */).attachInternals();
|
.attachInternals();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -138,12 +147,12 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
|||||||
private renderContent() {
|
private renderContent() {
|
||||||
// Link buttons may not be disabled
|
// Link buttons may not be disabled
|
||||||
const isDisabled = this.disabled && !this.href;
|
const isDisabled = this.disabled && !this.href;
|
||||||
const icon =
|
const icon = html`<slot
|
||||||
html`<slot name="icon" @slotchange="${this.handleSlotChange}"></slot>`;
|
name="icon"
|
||||||
|
@slotchange="${this.handleSlotChange}"></slot>`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.renderElevation?.()}
|
${this.renderElevation?.()} ${this.renderOutline?.()}
|
||||||
${this.renderOutline?.()}
|
|
||||||
<md-focus-ring part="focus-ring"></md-focus-ring>
|
<md-focus-ring part="focus-ring"></md-focus-ring>
|
||||||
<md-ripple class="button__ripple" ?disabled="${isDisabled}"></md-ripple>
|
<md-ripple class="button__ripple" ?disabled="${isDisabled}"></md-ripple>
|
||||||
<span class="touch"></span>
|
<span class="touch"></span>
|
||||||
@ -154,7 +163,7 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly handleActivationClick = (event: MouseEvent) => {
|
private readonly handleActivationClick = (event: MouseEvent) => {
|
||||||
if (!isActivationClick((event)) || !this.buttonElement) {
|
if (!isActivationClick(event) || !this.buttonElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.focus();
|
this.focus();
|
||||||
|
@ -17,12 +17,9 @@ import {customElement} from 'lit/decorators.js';
|
|||||||
@customElement('catalog-component-header')
|
@customElement('catalog-component-header')
|
||||||
export class CatalogComponentHeader extends LitElement {
|
export class CatalogComponentHeader extends LitElement {
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html` <div>
|
||||||
<div>
|
|
||||||
<slot class="title" name="title"></slot>
|
<slot class="title" name="title"></slot>
|
||||||
<slot
|
<slot class="image"></slot>
|
||||||
class="image"
|
|
||||||
></slot>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,12 +4,18 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@material/web/iconbutton/icon-button.js';
|
|
||||||
import '@material/web/icon/icon.js';
|
import '@material/web/icon/icon.js';
|
||||||
|
import '@material/web/iconbutton/icon-button.js';
|
||||||
|
|
||||||
import {MdIconButton} from '@material/web/iconbutton/icon-button.js';
|
import {MdIconButton} from '@material/web/iconbutton/icon-button.js';
|
||||||
import {css, html, LitElement} from 'lit';
|
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
|
* 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}
|
title=${this.buttonTitle}
|
||||||
.selected=${this.showCheckmark}
|
.selected=${this.showCheckmark}
|
||||||
aria-label=${this.label}
|
aria-label=${this.label}
|
||||||
aria-label-selected=${this.successLabel}
|
aria-label-selected=${this.successLabel}>
|
||||||
>
|
|
||||||
<md-icon>content_copy</md-icon>
|
<md-icon>content_copy</md-icon>
|
||||||
<md-icon slot="selected">checkmark</md-icon>
|
<md-icon slot="selected">checkmark</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
|
@ -68,15 +68,13 @@ export class HCTSlider extends LitElement {
|
|||||||
.min=${range[0]}
|
.min=${range[0]}
|
||||||
.max=${range[1]}
|
.max=${range[1]}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@input=${this.onInput}
|
@input=${this.onInput}></md-slider>
|
||||||
></md-slider>
|
|
||||||
<div
|
<div
|
||||||
id="gradient"
|
id="gradient"
|
||||||
class=${this.type}
|
class=${this.type}
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
background: this.buildGradient(),
|
background: this.buildGradient(),
|
||||||
})}
|
})}></div>
|
||||||
></div>
|
|
||||||
</section>`;
|
</section>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +98,7 @@ export class HCTSlider extends LitElement {
|
|||||||
|
|
||||||
if (this.type === 'hue') {
|
if (this.type === 'hue') {
|
||||||
for (let i = 0; i < numStops; i++) {
|
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
|
// 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
|
// black and white so it's not too dark or too bright and vary the hue
|
||||||
const hex = hexFromHct(hue, 100, 50);
|
const hex = hexFromHct(hue, 100, 50);
|
||||||
@ -111,7 +109,7 @@ export class HCTSlider extends LitElement {
|
|||||||
const hue = hct.hue;
|
const hue = hct.hue;
|
||||||
|
|
||||||
for (let i = 0; i < numStops; i++) {
|
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
|
// 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
|
// mid so we it's not too dark or too bright and vary the chroma
|
||||||
const hex = hexFromHct(hue, chroma, 50);
|
const hex = hexFromHct(hue, chroma, 50);
|
||||||
@ -119,7 +117,7 @@ export class HCTSlider extends LitElement {
|
|||||||
}
|
}
|
||||||
} else if (this.type === 'tone') {
|
} else if (this.type === 'tone') {
|
||||||
for (let i = 0; i < numStops; i++) {
|
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
|
// Set tone color to black (0 chroma means that hue doesn't matter) and
|
||||||
// vary the tone
|
// vary the tone
|
||||||
const hex = hexFromHct(0, 0, tone);
|
const hex = hexFromHct(0, 0, tone);
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import {animate, fadeIn, fadeOut} from '@lit-labs/motion';
|
import {animate, fadeIn, fadeOut} from '@lit-labs/motion';
|
||||||
import {EASING} from '@material/web/internal/motion/animation.js';
|
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 {customElement, property, state} from 'lit/decorators.js';
|
||||||
|
|
||||||
import {drawerOpenSignal} from '../signals/drawer-open-state.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
|
* widths, and position itself inline with the page at wider page widths. Most
|
||||||
* importantly, this sidebar is SSR compatible.
|
* importantly, this sidebar is SSR compatible.
|
||||||
*/
|
*/
|
||||||
@customElement('nav-drawer') export class NavDrawer extends SignalElement
|
@customElement('nav-drawer')
|
||||||
(LitElement) {
|
export class NavDrawer extends SignalElement(LitElement) {
|
||||||
/**
|
/**
|
||||||
* Whether or not the side drawer is collapsible or inline.
|
* 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 drawerContentOpacityDuration = showModal ? 300 : 150;
|
||||||
const scrimOpacityDuration = 150;
|
const scrimOpacityDuration = 150;
|
||||||
|
|
||||||
const drawerSlideAnimationEasing =
|
const drawerSlideAnimationEasing = showModal
|
||||||
showModal ? EASING.EMPHASIZED : EASING.EMPHASIZED_ACCELERATE;
|
? EASING.EMPHASIZED
|
||||||
|
: EASING.EMPHASIZED_ACCELERATE;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="root">
|
<div class="root">
|
||||||
@ -65,8 +66,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
|||||||
},
|
},
|
||||||
in: fadeIn,
|
in: fadeIn,
|
||||||
out: fadeOut,
|
out: fadeOut,
|
||||||
})}
|
})}></div>`
|
||||||
></div>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
<aside
|
<aside
|
||||||
?inert=${this.isCollapsible && !drawerOpenSignal.value}
|
?inert=${this.isCollapsible && !drawerOpenSignal.value}
|
||||||
@ -76,8 +76,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
|||||||
duration: drawerSlideAnimationDuration,
|
duration: drawerSlideAnimationDuration,
|
||||||
easing: drawerSlideAnimationEasing,
|
easing: drawerSlideAnimationEasing,
|
||||||
},
|
},
|
||||||
})}
|
})}>
|
||||||
>
|
|
||||||
<div class="scroll-wrapper">
|
<div class="scroll-wrapper">
|
||||||
<slot
|
<slot
|
||||||
${animate({
|
${animate({
|
||||||
@ -86,8 +85,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
|||||||
duration: drawerContentOpacityDuration,
|
duration: drawerContentOpacityDuration,
|
||||||
easing: 'linear',
|
easing: 'linear',
|
||||||
},
|
},
|
||||||
})}
|
})}></slot>
|
||||||
></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@ -102,8 +100,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
|||||||
private renderContent(showModal: boolean) {
|
private renderContent(showModal: boolean) {
|
||||||
return html` <div
|
return html` <div
|
||||||
class="pane content-pane"
|
class="pane content-pane"
|
||||||
?inert=${showModal || inertContentSignal.value}
|
?inert=${showModal || inertContentSignal.value}>
|
||||||
>
|
|
||||||
<div class="scroll-wrapper">
|
<div class="scroll-wrapper">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot name="app-content"></slot>
|
<slot name="app-content"></slot>
|
||||||
@ -119,8 +116,7 @@ import {SignalElement} from '../signals/signal-element.js';
|
|||||||
|
|
||||||
return html` <div
|
return html` <div
|
||||||
class="pane toc"
|
class="pane toc"
|
||||||
?inert=${showModal || inertContentSignal.value}
|
?inert=${showModal || inertContentSignal.value}>
|
||||||
>
|
|
||||||
<div class="scroll-wrapper">
|
<div class="scroll-wrapper">
|
||||||
<p>On this page:</p>
|
<p>On this page:</p>
|
||||||
<h2>${this.pageTitle}</h2>
|
<h2>${this.pageTitle}</h2>
|
||||||
@ -148,11 +144,16 @@ import {SignalElement} from '../signals/signal-element.js';
|
|||||||
|
|
||||||
updated(changed: PropertyValues<this>) {
|
updated(changed: PropertyValues<this>) {
|
||||||
super.updated(changed);
|
super.updated(changed);
|
||||||
if (this.lastDrawerOpen !== drawerOpenSignal.value &&
|
if (
|
||||||
drawerOpenSignal.value && this.isCollapsible) {
|
this.lastDrawerOpen !== drawerOpenSignal.value &&
|
||||||
(this.querySelector('md-list.nav md-list-item[tabindex="0"]') as
|
drawerOpenSignal.value &&
|
||||||
HTMLElement)
|
this.isCollapsible
|
||||||
?.focus();
|
) {
|
||||||
|
(
|
||||||
|
this.querySelector(
|
||||||
|
'md-list.nav md-list-item[tabindex="0"]',
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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/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 type {MdOutlinedSegmentedButton} from '@material/web/labs/segmentedbutton/outlined-segmented-button.js';
|
||||||
import {css, html, LitElement} from 'lit';
|
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 {ChangeColorEvent, ChangeDarkModeEvent} from '../types/color-events.js';
|
||||||
import {hctFromHex, hexFromHct} from '../utils/material-color-helpers.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 type {ColorMode} from '../utils/theme.js';
|
||||||
|
import {
|
||||||
|
getCurrentMode,
|
||||||
|
getCurrentSeedColor,
|
||||||
|
getCurrentThemeString,
|
||||||
|
} from '../utils/theme.js';
|
||||||
|
|
||||||
import type {HCTSlider} from './hct-slider.js';
|
import type {HCTSlider} from './hct-slider.js';
|
||||||
|
|
||||||
@ -68,17 +72,14 @@ export class ThemeChanger extends LitElement {
|
|||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<div id="head-wrapper">
|
<div id="head-wrapper">
|
||||||
<h2>
|
<h2> Theme Controls </h2>
|
||||||
Theme Controls
|
|
||||||
</h2>
|
|
||||||
<copy-code-button
|
<copy-code-button
|
||||||
button-title="Copy current theme to clipboard"
|
button-title="Copy current theme to clipboard"
|
||||||
label="Copy current theme"
|
label="Copy current theme"
|
||||||
.getCopyText=${getCurrentThemeString}>
|
.getCopyText=${getCurrentThemeString}>
|
||||||
</copy-code-button>
|
</copy-code-button>
|
||||||
</div>
|
</div>
|
||||||
${this.renderHexPicker()}
|
${this.renderHexPicker()} ${this.renderHctPicker()}
|
||||||
${this.renderHctPicker()}
|
|
||||||
${this.renderColorModePicker()}
|
${this.renderColorModePicker()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -96,8 +97,7 @@ export class ThemeChanger extends LitElement {
|
|||||||
id="color-input"
|
id="color-input"
|
||||||
@input=${this.onHexPickerInput}
|
@input=${this.onHexPickerInput}
|
||||||
type="color"
|
type="color"
|
||||||
.value=${live(this.hexColor)}
|
.value=${live(this.hexColor)} />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<md-focus-ring for="color-input"></md-focus-ring>
|
<md-focus-ring for="color-input"></md-focus-ring>
|
||||||
</span>
|
</span>
|
||||||
@ -115,23 +115,20 @@ export class ThemeChanger extends LitElement {
|
|||||||
type="hue"
|
type="hue"
|
||||||
label="Hue"
|
label="Hue"
|
||||||
max="360"
|
max="360"
|
||||||
@input=${this.onSliderInput}
|
@input=${this.onSliderInput}></hct-slider>
|
||||||
></hct-slider>
|
|
||||||
<hct-slider
|
<hct-slider
|
||||||
.value=${live(this.chroma)}
|
.value=${live(this.chroma)}
|
||||||
.color=${this.hexColor}
|
.color=${this.hexColor}
|
||||||
type="chroma"
|
type="chroma"
|
||||||
label="Chroma"
|
label="Chroma"
|
||||||
max="150"
|
max="150"
|
||||||
@input=${this.onSliderInput}
|
@input=${this.onSliderInput}></hct-slider>
|
||||||
></hct-slider>
|
|
||||||
<hct-slider
|
<hct-slider
|
||||||
.value=${live(this.tone)}
|
.value=${live(this.tone)}
|
||||||
type="tone"
|
type="tone"
|
||||||
label="Tone"
|
label="Tone"
|
||||||
max="100"
|
max="100"
|
||||||
@input=${this.onSliderInput}
|
@input=${this.onSliderInput}></hct-slider>
|
||||||
></hct-slider>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,8 +138,7 @@ export class ThemeChanger extends LitElement {
|
|||||||
private renderColorModePicker() {
|
private renderColorModePicker() {
|
||||||
return html`<md-outlined-segmented-button-set
|
return html`<md-outlined-segmented-button-set
|
||||||
@segmented-button-set-selection=${this.onColorModeSelection}
|
@segmented-button-set-selection=${this.onColorModeSelection}
|
||||||
aria-label="Color mode"
|
aria-label="Color mode">
|
||||||
>
|
|
||||||
${this.renderModeButton('dark', 'dark_mode')}
|
${this.renderModeButton('dark', 'dark_mode')}
|
||||||
${this.renderModeButton('auto', 'brightness_medium')}
|
${this.renderModeButton('auto', 'brightness_medium')}
|
||||||
${this.renderModeButton('light', 'light_mode')}
|
${this.renderModeButton('light', 'light_mode')}
|
||||||
@ -161,8 +157,7 @@ export class ThemeChanger extends LitElement {
|
|||||||
data-value=${mode}
|
data-value=${mode}
|
||||||
title=${mode}
|
title=${mode}
|
||||||
aria-label="${mode} color scheme"
|
aria-label="${mode} color scheme"
|
||||||
.selected=${this.selectedColorMode === mode}
|
.selected=${this.selectedColorMode === mode}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">${icon}</md-icon>
|
<md-icon slot="icon">${icon}</md-icon>
|
||||||
</md-outlined-segmented-button>`;
|
</md-outlined-segmented-button>`;
|
||||||
}
|
}
|
||||||
@ -208,9 +203,13 @@ export class ThemeChanger extends LitElement {
|
|||||||
this.updateHctFromHex(this.hexColor);
|
this.updateHctFromHex(this.hexColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onColorModeSelection(e: CustomEvent<{
|
private onColorModeSelection(
|
||||||
button: MdOutlinedSegmentedButton; selected: boolean; index: number;
|
e: CustomEvent<{
|
||||||
}>) {
|
button: MdOutlinedSegmentedButton;
|
||||||
|
selected: boolean;
|
||||||
|
index: number;
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
const {button} = e.detail;
|
const {button} = e.detail;
|
||||||
const value = button.dataset.value as ColorMode;
|
const value = button.dataset.value as ColorMode;
|
||||||
this.selectedColorMode = value;
|
this.selectedColorMode = value;
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import '@material/web/focus/md-focus-ring.js';
|
import '@material/web/focus/md-focus-ring.js';
|
||||||
import '@material/web/iconbutton/icon-button.js';
|
|
||||||
import '@material/web/icon/icon.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 type {MdIconButton} from '@material/web/iconbutton/icon-button.js';
|
||||||
import {css, html, LitElement} from 'lit';
|
import {css, html, LitElement} from 'lit';
|
||||||
@ -21,8 +21,8 @@ import {materialDesign} from '../svg/material-design-logo.js';
|
|||||||
/**
|
/**
|
||||||
* Top app bar of the catalog.
|
* Top app bar of the catalog.
|
||||||
*/
|
*/
|
||||||
@customElement('top-app-bar') export class TopAppBar extends SignalElement
|
@customElement('top-app-bar')
|
||||||
(LitElement) {
|
export class TopAppBar extends SignalElement(LitElement) {
|
||||||
/**
|
/**
|
||||||
* Whether or not the color picker menu is open.
|
* 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-selected="open navigation menu"
|
||||||
aria-label="close navigation menu"
|
aria-label="close navigation menu"
|
||||||
aria-expanded=${drawerOpenSignal.value ? 'false' : 'true'}
|
aria-expanded=${drawerOpenSignal.value ? 'false' : 'true'}
|
||||||
title="${
|
title="${!drawerOpenSignal.value
|
||||||
!drawerOpenSignal.value ? 'Open' : 'Close'} navigation menu"
|
? 'Open'
|
||||||
|
: 'Close'} navigation menu"
|
||||||
.selected=${live(!drawerOpenSignal.value)}
|
.selected=${live(!drawerOpenSignal.value)}
|
||||||
@input=${this.onMenuIconToggle}
|
@input=${this.onMenuIconToggle}>
|
||||||
>
|
|
||||||
<md-icon slot="selected">menu</md-icon>
|
<md-icon slot="selected">menu</md-icon>
|
||||||
<md-icon>menu_open</md-icon>
|
<md-icon>menu_open</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
@ -52,8 +52,7 @@ import {materialDesign} from '../svg/material-design-logo.js';
|
|||||||
href="/"
|
href="/"
|
||||||
class="home-button"
|
class="home-button"
|
||||||
title="Home"
|
title="Home"
|
||||||
aria-label="Home"
|
aria-label="Home">
|
||||||
>
|
|
||||||
${materialDesign}
|
${materialDesign}
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
</section>
|
</section>
|
||||||
@ -71,16 +70,14 @@ import {materialDesign} from '../svg/material-design-logo.js';
|
|||||||
<lit-island
|
<lit-island
|
||||||
on:interaction="pointerenter,focusin,pointerdown"
|
on:interaction="pointerenter,focusin,pointerdown"
|
||||||
import="/js/hydration-entrypoints/menu.js"
|
import="/js/hydration-entrypoints/menu.js"
|
||||||
id="menu-island"
|
id="menu-island">
|
||||||
>
|
|
||||||
<md-icon-button
|
<md-icon-button
|
||||||
id="theme-button"
|
id="theme-button"
|
||||||
@click="${this.onPaletteClick}"
|
@click="${this.onPaletteClick}"
|
||||||
title="Page theme controls"
|
title="Page theme controls"
|
||||||
aria-label="Page theme controls"
|
aria-label="Page theme controls"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-expanded=${this.menuOpen ? 'true' : 'false'}
|
aria-expanded=${this.menuOpen ? 'true' : 'false'}>
|
||||||
>
|
|
||||||
<md-icon>palette</md-icon>
|
<md-icon>palette</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
<md-menu
|
<md-menu
|
||||||
@ -93,8 +90,7 @@ import {materialDesign} from '../svg/material-design-logo.js';
|
|||||||
.open=${this.menuOpen}
|
.open=${this.menuOpen}
|
||||||
@opened=${this.onMenuOpened}
|
@opened=${this.onMenuOpened}
|
||||||
@closed=${this.onMenuClosed}
|
@closed=${this.onMenuClosed}
|
||||||
@keydown=${this.onKeydown}
|
@keydown=${this.onKeydown}>
|
||||||
>
|
|
||||||
<theme-changer></theme-changer>
|
<theme-changer></theme-changer>
|
||||||
</md-menu>
|
</md-menu>
|
||||||
</lit-island>
|
</lit-island>
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@material/web/fab/fab.js';
|
|
||||||
import '@material/web/fab/branded-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
|
* 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-icon-button.js';
|
||||||
import '@material/web/iconbutton/filled-tonal-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';
|
import '@material/web/iconbutton/outlined-icon-button.js';
|
||||||
|
@ -4,6 +4,6 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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/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
|
* 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/icon/icon.js';
|
||||||
import '@material/web/iconbutton/icon-button.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
|
* 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/nav-drawer.js';
|
||||||
import '../components/top-app-bar.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
|
* 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-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.
|
* @param previewEl An element reference to the playground preview element.
|
||||||
*/
|
*/
|
||||||
async function updateMessageTargetOnIframeLoad(
|
async function updateMessageTargetOnIframeLoad(
|
||||||
postdoc: PostDoc, previewEl: PlaygroundPreview) {
|
postdoc: PostDoc,
|
||||||
|
previewEl: PlaygroundPreview,
|
||||||
|
) {
|
||||||
await previewEl.updateComplete;
|
await previewEl.updateComplete;
|
||||||
const iframe = previewEl.iframe!;
|
const iframe = previewEl.iframe!;
|
||||||
|
|
||||||
@ -76,9 +78,9 @@ function demoDropdown() {
|
|||||||
|
|
||||||
// tslint:disable:no-unnecessary-type-assertion TSC externally seems to differ
|
// tslint:disable:no-unnecessary-type-assertion TSC externally seems to differ
|
||||||
// from internal here and needs these type assertions
|
// from internal here and needs these type assertions
|
||||||
const expandButton =
|
const expandButton = detailsEl?.querySelector(
|
||||||
detailsEl?.querySelector('summary md-outlined-icon-button') as
|
'summary md-outlined-icon-button',
|
||||||
MdOutlinedIconButton;
|
) as MdOutlinedIconButton;
|
||||||
// tslint:enable:no-unnecessary-type-assertion
|
// tslint:enable:no-unnecessary-type-assertion
|
||||||
|
|
||||||
// Synchronize details open state with toggle button
|
// Synchronize details open state with toggle button
|
||||||
|
@ -12,7 +12,16 @@
|
|||||||
* and global scroll listeners.
|
* 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
|
* 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
|
// Listen for system color change and applies the new theme if the current
|
||||||
// color mode is 'auto'.
|
// color mode is 'auto'.
|
||||||
window.matchMedia('(prefers-color-scheme: dark)')
|
window
|
||||||
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
.addEventListener('change', () => {
|
.addEventListener('change', () => {
|
||||||
if (getCurrentMode() !== 'auto') {
|
if (getCurrentMode() !== 'auto') {
|
||||||
return;
|
return;
|
||||||
|
@ -18,8 +18,9 @@ type ReactiveElementConstructor = new (...args: any[]) => ReactiveElement;
|
|||||||
*
|
*
|
||||||
* @param Base The class to mix-in and listen to Preact signal changes.
|
* @param Base The class to mix-in and listen to Preact signal changes.
|
||||||
*/
|
*/
|
||||||
export function SignalElement<T extends ReactiveElementConstructor>(Base: T):
|
export function SignalElement<T extends ReactiveElementConstructor>(
|
||||||
T {
|
Base: T,
|
||||||
|
): T {
|
||||||
return class SignalElement extends Base {
|
return class SignalElement extends Base {
|
||||||
private _disposeEffect?: () => void;
|
private _disposeEffect?: () => void;
|
||||||
|
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
import {Island} from '@11ty/is-land';
|
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.
|
// Removes the feature in which 11ty island removes DOM to render a fallback.
|
||||||
override forceFallback() {}
|
override forceFallback() {}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
// This file imports only files that will be SSRd e.g. if you can't SSR a
|
// This file imports only files that will be SSRd e.g. if you can't SSR a
|
||||||
// component, don't import it here.
|
// component, don't import it here.
|
||||||
|
import '@material/web/all.js';
|
||||||
import './components/catalog-component-header.js';
|
import './components/catalog-component-header.js';
|
||||||
import './components/catalog-component-header-title.js';
|
import './components/catalog-component-header-title.js';
|
||||||
import './components/top-app-bar.js';
|
|
||||||
import './components/nav-drawer.js';
|
import './components/nav-drawer.js';
|
||||||
import './components/theme-changer.js';
|
import './components/theme-changer.js';
|
||||||
import '@material/web/all.js';
|
import './components/top-app-bar.js';
|
||||||
// 🤫
|
// 🤫
|
||||||
import '@material/web/labs/item/item.js';
|
import '@material/web/labs/item/item.js';
|
@ -11,8 +11,7 @@ import {html} from 'lit';
|
|||||||
*
|
*
|
||||||
* Source: Internal google symbols search.
|
* Source: Internal google symbols search.
|
||||||
*/
|
*/
|
||||||
export const materialDesign = html`
|
export const materialDesign = html` <svg
|
||||||
<svg
|
|
||||||
viewBox="0 96 960 960"
|
viewBox="0 96 960 960"
|
||||||
fill="currentColor">
|
fill="currentColor">
|
||||||
<path
|
<path
|
||||||
|
@ -4,8 +4,9 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type WithStylesheet =
|
type WithStylesheet = typeof globalThis & {
|
||||||
typeof globalThis&{[stylesheetName: string]: CSSStyleSheet | undefined};
|
[stylesheetName: string]: CSSStyleSheet | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a stringified CSS theme to a document or shadowroot by creating or
|
* 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.
|
* used to generate the localstorage name.
|
||||||
*/
|
*/
|
||||||
export function applyThemeString(
|
export function applyThemeString(
|
||||||
doc: DocumentOrShadowRoot, themeString: string, ssName = 'material-theme') {
|
doc: DocumentOrShadowRoot,
|
||||||
|
themeString: string,
|
||||||
|
ssName = 'material-theme',
|
||||||
|
) {
|
||||||
// Get constructable stylesheet
|
// Get constructable stylesheet
|
||||||
let sheet = (globalThis as WithStylesheet)[ssName];
|
let sheet = (globalThis as WithStylesheet)[ssName];
|
||||||
// Create a new sheet if it doesn't exist already and save it globally.
|
// 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.
|
// Set the color of the URL bar because we are cool like that.
|
||||||
const surfaceContainer =
|
const surfaceContainer = themeString.match(
|
||||||
themeString.match(/--md-sys-color-surface-container:(.+?);/)?.[1];
|
/--md-sys-color-surface-container:(.+?);/,
|
||||||
|
)?.[1];
|
||||||
if (surfaceContainer) {
|
if (surfaceContainer) {
|
||||||
document.querySelector('meta[name="theme-color"]')
|
document
|
||||||
|
.querySelector('meta[name="theme-color"]')
|
||||||
?.setAttribute('content', surfaceContainer);
|
?.setAttribute('content', surfaceContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,13 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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';
|
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.
|
* used to generate the localstorage name.
|
||||||
*/
|
*/
|
||||||
export function applyMaterialTheme(
|
export function applyMaterialTheme(
|
||||||
doc: DocumentOrShadowRoot, theme: Theme, ssName = 'material-theme') {
|
doc: DocumentOrShadowRoot,
|
||||||
|
theme: Theme,
|
||||||
|
ssName = 'material-theme',
|
||||||
|
) {
|
||||||
let styleString = ':root,:host{';
|
let styleString = ':root,:host{';
|
||||||
for (const [key, value] of Object.entries(theme)) {
|
for (const [key, value] of Object.entries(theme)) {
|
||||||
styleString += `--md-sys-color-${key}:${value};`;
|
styleString += `--md-sys-color-${key}:${value};`;
|
||||||
|
@ -4,8 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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';
|
export type ColorMode = 'light' | 'dark' | 'auto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,8 +106,10 @@ export function saveSeedColor(color: string) {
|
|||||||
* @return The last applied color mode while in "auto".
|
* @return The last applied color mode while in "auto".
|
||||||
*/
|
*/
|
||||||
export function getLastSavedAutoColorMode() {
|
export function getLastSavedAutoColorMode() {
|
||||||
return localStorage.getItem('last-auto-color-mode') as | 'light' | 'dark' |
|
return localStorage.getItem('last-auto-color-mode') as
|
||||||
null;
|
| 'light'
|
||||||
|
| 'dark'
|
||||||
|
| null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -8,10 +8,10 @@
|
|||||||
import '@material/web/button/outlined-button.js';
|
import '@material/web/button/outlined-button.js';
|
||||||
import '@material/web/checkbox/checkbox.js';
|
import '@material/web/checkbox/checkbox.js';
|
||||||
import '@material/web/radio/radio.js';
|
import '@material/web/radio/radio.js';
|
||||||
|
import '@material/web/ripple/ripple.js';
|
||||||
import '@material/web/select/filled-select.js';
|
import '@material/web/select/filled-select.js';
|
||||||
import '@material/web/select/select-option.js';
|
import '@material/web/select/select-option.js';
|
||||||
import '@material/web/textfield/filled-text-field.js';
|
import '@material/web/textfield/filled-text-field.js';
|
||||||
import '@material/web/ripple/ripple.js';
|
|
||||||
|
|
||||||
import {css, html, LitElement} from 'lit';
|
import {css, html, LitElement} from 'lit';
|
||||||
import {customElement, property} from 'lit/decorators.js';
|
import {customElement, property} from 'lit/decorators.js';
|
||||||
@ -36,8 +36,7 @@ export function boolInput(): KnobUi<boolean> {
|
|||||||
touch-target="none"
|
touch-target="none"
|
||||||
style="margin-inline-end: 16px;"
|
style="margin-inline-end: 16px;"
|
||||||
.checked=${!!knob.latestValue}
|
.checked=${!!knob.latestValue}
|
||||||
@change="${valueChanged}"
|
@change="${valueChanged}">
|
||||||
>
|
|
||||||
</md-checkbox>
|
</md-checkbox>
|
||||||
${knob.name}
|
${knob.name}
|
||||||
</label>
|
</label>
|
||||||
@ -70,11 +69,17 @@ export class KnobColorSelector extends LitElement {
|
|||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
width: var(--_color-picker-size);
|
width: var(--_color-picker-size);
|
||||||
height: 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;
|
padding: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: var(--_color-picker-border-width);
|
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;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,8 +132,7 @@ export class KnobColorSelector extends LitElement {
|
|||||||
<md-outlined-button
|
<md-outlined-button
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.hasAlpha = !this.hasAlpha;
|
this.hasAlpha = !this.hasAlpha;
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
${this.hasAlpha ? 'rgba' : 'rgb'}
|
${this.hasAlpha ? 'rgba' : 'rgb'}
|
||||||
</md-outlined-button>
|
</md-outlined-button>
|
||||||
</span>`;
|
</span>`;
|
||||||
@ -139,8 +143,7 @@ export class KnobColorSelector extends LitElement {
|
|||||||
style=${styleMap(sharedTextFieldStyles)}
|
style=${styleMap(sharedTextFieldStyles)}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@change=${this.propagateEvt}
|
@change=${this.propagateEvt}
|
||||||
@input=${this.onInput}
|
@input=${this.onInput}></md-filled-text-field>`;
|
||||||
></md-filled-text-field>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderColorInput() {
|
private renderColorInput() {
|
||||||
@ -155,8 +158,7 @@ export class KnobColorSelector extends LitElement {
|
|||||||
id="color-picker"
|
id="color-picker"
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
@change=${this.propagateEvt}
|
@change=${this.propagateEvt}
|
||||||
@input=${this.onInput}
|
@input=${this.onInput} />
|
||||||
/>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,14 +178,16 @@ export class KnobColorSelector extends LitElement {
|
|||||||
|
|
||||||
override click() {
|
override click() {
|
||||||
const input = this.renderRoot!.querySelector(
|
const input = this.renderRoot!.querySelector(
|
||||||
'input,md-filled-text-field') as HTMLElement;
|
'input,md-filled-text-field',
|
||||||
|
) as HTMLElement;
|
||||||
input.click();
|
input.click();
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
override focus() {
|
override focus() {
|
||||||
const input = this.renderRoot!.querySelector(
|
const input = this.renderRoot!.querySelector(
|
||||||
'input,md-filled-text-field') as HTMLElement;
|
'input,md-filled-text-field',
|
||||||
|
) as HTMLElement;
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,8 +226,7 @@ export function colorPicker(opts?: ColorPickerOpts): KnobUi<string> {
|
|||||||
<knob-color-selector
|
<knob-color-selector
|
||||||
.value="${knob.latestValue ?? ''}"
|
.value="${knob.latestValue ?? ''}"
|
||||||
.hasAlpha="${config.hasAlpha}"
|
.hasAlpha="${config.hasAlpha}"
|
||||||
@input=${valueChanged}
|
@input=${valueChanged}></knob-color-selector>
|
||||||
></knob-color-selector>
|
|
||||||
${knob.name}
|
${knob.name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -269,8 +272,7 @@ export function textInput<T>(options?: TextInputOptions<T>): KnobUi<T> {
|
|||||||
<md-filled-text-field
|
<md-filled-text-field
|
||||||
style=${styleMap(sharedTextFieldStyles)}
|
style=${styleMap(sharedTextFieldStyles)}
|
||||||
.value="${(knob.latestValue ?? '') as unknown as string}"
|
.value="${(knob.latestValue ?? '') as unknown as string}"
|
||||||
@input="${valueChanged}"
|
@input="${valueChanged}"></md-filled-text-field>
|
||||||
></md-filled-text-field>
|
|
||||||
${knob.name}
|
${knob.name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -311,8 +313,7 @@ export function numberInput(opts?: NumberInputOpts): KnobUi<number> {
|
|||||||
type="number"
|
type="number"
|
||||||
step="${config.step}"
|
step="${config.step}"
|
||||||
.value="${knob.latestValue ? knob.latestValue.toString() : '0'}"
|
.value="${knob.latestValue ? knob.latestValue.toString() : '0'}"
|
||||||
@input="${valueChanged}"
|
@input="${valueChanged}"></md-filled-text-field>
|
||||||
></md-filled-text-field>
|
|
||||||
${knob.name}
|
${knob.name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -343,7 +344,7 @@ export function button(): KnobUi<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RadioSelectorConfig<T extends string> {
|
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;
|
readonly name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,8 +365,7 @@ export function radioSelector<T extends string>({
|
|||||||
name="${name}"
|
name="${name}"
|
||||||
value="${value}"
|
value="${value}"
|
||||||
@change="${valueChanged}"
|
@change="${valueChanged}"
|
||||||
?checked="${knob.latestValue === option.value}"
|
?checked="${knob.latestValue === option.value}"></md-radio>
|
||||||
></md-radio>
|
|
||||||
${option.label}
|
${option.label}
|
||||||
</label>`;
|
</label>`;
|
||||||
});
|
});
|
||||||
@ -375,7 +375,7 @@ export function radioSelector<T extends string>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SelectDropdownConfig<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. */
|
/** A select dropdown Knob UI. */
|
||||||
@ -391,8 +391,7 @@ export function selectDropdown<T extends string>({
|
|||||||
return html`<md-select-option
|
return html`<md-select-option
|
||||||
?selected="${knob.latestValue === option.value}"
|
?selected="${knob.latestValue === option.value}"
|
||||||
.value="${option.value}"
|
.value="${option.value}"
|
||||||
.headline=${option.label}
|
.headline=${option.label}></md-select-option>`;
|
||||||
></md-select-option>`;
|
|
||||||
});
|
});
|
||||||
return html`
|
return html`
|
||||||
<label>
|
<label>
|
||||||
@ -414,7 +413,10 @@ export function selectDropdown<T extends string>({
|
|||||||
* to the updated value.
|
* to the updated value.
|
||||||
*/
|
*/
|
||||||
export function cssCustomProperty(
|
export function cssCustomProperty(
|
||||||
knob: Knob<string>, val: string, containerOfRenderedStory: HTMLElement) {
|
knob: Knob<string>,
|
||||||
|
val: string,
|
||||||
|
containerOfRenderedStory: HTMLElement,
|
||||||
|
) {
|
||||||
const value = knob.isUnset ? knob.defaultValue : val;
|
const value = knob.isUnset ? knob.defaultValue : val;
|
||||||
if (value) {
|
if (value) {
|
||||||
containerOfRenderedStory.style.setProperty(knob.name, value);
|
containerOfRenderedStory.style.setProperty(knob.name, value);
|
||||||
|
@ -93,8 +93,9 @@ export class StoriesRenderer extends LitElement {
|
|||||||
let label: string | TemplateResult = '';
|
let label: string | TemplateResult = '';
|
||||||
|
|
||||||
if (!this.hideLabels) {
|
if (!this.hideLabels) {
|
||||||
const description =
|
const description = story.description
|
||||||
story.description ? html`<small>${story.description}</small>` : '';
|
? html`<small>${story.description}</small>`
|
||||||
|
: '';
|
||||||
label = html`
|
label = html`
|
||||||
<h3 class="m-headline5">${story.name}</h3>
|
<h3 class="m-headline5">${story.name}</h3>
|
||||||
${description}
|
${description}
|
||||||
@ -126,8 +127,7 @@ export class StoriesRenderer extends LitElement {
|
|||||||
<story-knob-panel
|
<story-knob-panel
|
||||||
.open=${this.knobsOpen}
|
.open=${this.knobsOpen}
|
||||||
.type=${this.knobsPanelType}
|
.type=${this.knobsPanelType}
|
||||||
@open-changed=${onOpenChanged}
|
@open-changed=${onOpenChanged}>
|
||||||
>
|
|
||||||
${knobs.renderUI()}
|
${knobs.renderUI()}
|
||||||
</story-knob-panel>
|
</story-knob-panel>
|
||||||
`;
|
`;
|
||||||
@ -184,7 +184,8 @@ export class StoriesRenderer extends LitElement {
|
|||||||
if (!allowedStories.has(story)) {
|
if (!allowedStories.has(story)) {
|
||||||
console.error(
|
console.error(
|
||||||
`A stories renderer can only render stories ` +
|
`A stories renderer can only render stories ` +
|
||||||
`from its collection.`);
|
`from its collection.`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
storiesToRender.push(story);
|
storiesToRender.push(story);
|
||||||
}
|
}
|
||||||
@ -199,7 +200,9 @@ export class StoriesRenderer extends LitElement {
|
|||||||
private unobserveKnobs() {
|
private unobserveKnobs() {
|
||||||
if (this.observedKnobs !== undefined) {
|
if (this.observedKnobs !== undefined) {
|
||||||
this.observedKnobs.removeEventListener(
|
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 */
|
/* 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/elevation/elevation.js';
|
||||||
|
import '@material/web/icon/icon.js';
|
||||||
|
import '@material/web/iconbutton/icon-button.js';
|
||||||
|
|
||||||
import {css, html, LitElement, PropertyValues} from 'lit';
|
import {css, html, LitElement, PropertyValues} from 'lit';
|
||||||
import {customElement, property, query} from 'lit/decorators.js';
|
import {customElement, property, query} from 'lit/decorators.js';
|
||||||
@ -84,8 +84,7 @@ export class StoryKnobPanel extends LitElement {
|
|||||||
class="dragBar"
|
class="dragBar"
|
||||||
@pointerdown=${this.onDragStart}
|
@pointerdown=${this.onDragStart}
|
||||||
@pointermove=${this.onDrag}
|
@pointermove=${this.onDrag}
|
||||||
@pointerup=${this.onDragEnd}
|
@pointerup=${this.onDragEnd}>
|
||||||
>
|
|
||||||
<md-icon>drag_handle</md-icon>
|
<md-icon>drag_handle</md-icon>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -99,8 +98,7 @@ export class StoryKnobPanel extends LitElement {
|
|||||||
<md-icon-button
|
<md-icon-button
|
||||||
class="dragIconButton"
|
class="dragIconButton"
|
||||||
aria-label=${iconLabel}
|
aria-label=${iconLabel}
|
||||||
@click=${this.onDragIconClick}
|
@click=${this.onDragIconClick}>
|
||||||
>
|
|
||||||
<md-icon>${iconSvg}</md-icon>
|
<md-icon>${iconSvg}</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
`;
|
`;
|
||||||
@ -127,11 +125,13 @@ export class StoryKnobPanel extends LitElement {
|
|||||||
super.updated(changed);
|
super.updated(changed);
|
||||||
|
|
||||||
if (changed.has('open')) {
|
if (changed.has('open')) {
|
||||||
this.dispatchEvent(new CustomEvent('open-changed', {
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('open-changed', {
|
||||||
detail: {
|
detail: {
|
||||||
open: this.open,
|
open: this.open,
|
||||||
},
|
},
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,10 +181,11 @@ export class StoryKnobPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rightBound = DEFAULT_DIMENSIONS.RIGHT_OFFSET;
|
const rightBound = DEFAULT_DIMENSIONS.RIGHT_OFFSET;
|
||||||
const leftBound = DEFAULT_DIMENSIONS.RIGHT_OFFSET + this.containerWidth -
|
const leftBound =
|
||||||
window.innerWidth;
|
DEFAULT_DIMENSIONS.RIGHT_OFFSET + this.containerWidth - window.innerWidth;
|
||||||
const topBound = -DEFAULT_DIMENSIONS.TOP_OFFSET;
|
const topBound = -DEFAULT_DIMENSIONS.TOP_OFFSET;
|
||||||
const bottomBound = window.innerHeight -
|
const bottomBound =
|
||||||
|
window.innerHeight -
|
||||||
(DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET);
|
(DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET);
|
||||||
|
|
||||||
// do not allow drag outside right bound
|
// do not allow drag outside right bound
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
/* Slimmed down version of Lit stories story-renderer without IE renderer */
|
/* 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 {customElement, property} from 'lit/decorators.js';
|
||||||
|
|
||||||
import {Story} from '../story.js';
|
import {Story} from '../story.js';
|
||||||
|
@ -53,8 +53,9 @@ export class Knob<T, Name extends string = string> extends EventTarget {
|
|||||||
private readonly onReset = () => {
|
private readonly onReset = () => {
|
||||||
this.reset();
|
this.reset();
|
||||||
};
|
};
|
||||||
private readonly renderedStoryContainers =
|
private readonly renderedStoryContainers = new Set<
|
||||||
new Set<HTMLElement|DocumentFragment>();
|
HTMLElement | DocumentFragment
|
||||||
|
>();
|
||||||
|
|
||||||
constructor(readonly name: Name, init: KnobInit<T>) {
|
constructor(readonly name: Name, init: KnobInit<T>) {
|
||||||
super();
|
super();
|
||||||
@ -85,14 +86,17 @@ export class Knob<T, Name extends string = string> extends EventTarget {
|
|||||||
if (!this.wiring) {
|
if (!this.wiring) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const alreadyWired =
|
const alreadyWired = this.renderedStoryContainers.has(
|
||||||
this.renderedStoryContainers.has(containerOfRenderedStory);
|
containerOfRenderedStory,
|
||||||
|
);
|
||||||
if (!alreadyWired) {
|
if (!alreadyWired) {
|
||||||
this.renderedStoryContainers.add(containerOfRenderedStory);
|
this.renderedStoryContainers.add(containerOfRenderedStory);
|
||||||
// Ensure default values are wired correctly.
|
// Ensure default values are wired correctly.
|
||||||
if (this.dirty ||
|
if (
|
||||||
|
this.dirty ||
|
||||||
(this.latestValue !== undefined &&
|
(this.latestValue !== undefined &&
|
||||||
this.latestValue === this.defaultValue)) {
|
this.latestValue === this.defaultValue)
|
||||||
|
) {
|
||||||
this.wiring?.(this, this.latestValue!, containerOfRenderedStory);
|
this.wiring?.(this, this.latestValue!, containerOfRenderedStory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,15 +143,17 @@ type KnobKeys<Knobs extends PolymorphicArrayOfKnobs> = Knobs[number]['name'];
|
|||||||
*
|
*
|
||||||
* This type operator would return `number`.
|
* This type operator would return `number`.
|
||||||
*/
|
*/
|
||||||
type TypeOfKnobWithName<Knobs extends PolymorphicArrayOfKnobs,
|
type TypeOfKnobWithName<
|
||||||
SearchName extends string> =
|
Knobs extends PolymorphicArrayOfKnobs,
|
||||||
Extract<Knobs[number], {name: SearchName}> extends Knob<infer U>?
|
SearchName extends string,
|
||||||
U|undefined :
|
> = Extract<Knobs[number], {name: SearchName}> extends Knob<infer U>
|
||||||
never;
|
? U | undefined
|
||||||
|
: never;
|
||||||
|
|
||||||
/** A helper class for getting the latest value for a knob by name. */
|
/** A helper class for getting the latest value for a knob by name. */
|
||||||
export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> extends
|
export class KnobValues<
|
||||||
EventTarget {
|
Knobs extends PolymorphicArrayOfKnobs,
|
||||||
|
> extends EventTarget {
|
||||||
private readonly byName: ReadonlyMap<string, Knob<unknown>>;
|
private readonly byName: ReadonlyMap<string, Knob<unknown>>;
|
||||||
|
|
||||||
constructor(knobsArray: PolymorphicArrayOfKnobs) {
|
constructor(knobsArray: PolymorphicArrayOfKnobs) {
|
||||||
@ -156,25 +162,32 @@ export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> extends
|
|||||||
for (const knob of knobsArray) {
|
for (const knob of knobsArray) {
|
||||||
if (byName.has(knob.name)) {
|
if (byName.has(knob.name)) {
|
||||||
throw new Error(
|
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);
|
byName.set(knob.name, knob);
|
||||||
knob.addEventListener('changed', (e) => {
|
knob.addEventListener('changed', (e) => {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('changed', {detail: {knobName: knob.name}}));
|
new CustomEvent('changed', {detail: {knobName: knob.name}}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.byName = byName;
|
this.byName = byName;
|
||||||
}
|
}
|
||||||
|
|
||||||
get<SearchName extends KnobKeys<Knobs>>(knobName: SearchName):
|
get<SearchName extends KnobKeys<Knobs>>(
|
||||||
TypeOfKnobWithName<Knobs, SearchName> {
|
knobName: SearchName,
|
||||||
return this.byName.get(knobName)?.latestValue as
|
): TypeOfKnobWithName<Knobs, SearchName> {
|
||||||
TypeOfKnobWithName<Knobs, SearchName>;
|
return this.byName.get(knobName)?.latestValue as TypeOfKnobWithName<
|
||||||
|
Knobs,
|
||||||
|
SearchName
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
set<SearchName extends KnobKeys<Knobs>>(
|
set<SearchName extends KnobKeys<Knobs>>(
|
||||||
knobName: SearchName, newValue: TypeOfKnobWithName<Knobs, SearchName>) {
|
knobName: SearchName,
|
||||||
|
newValue: TypeOfKnobWithName<Knobs, SearchName>,
|
||||||
|
) {
|
||||||
const knob = this.byName.get(knobName);
|
const knob = this.byName.get(knobName);
|
||||||
if (knob === undefined) {
|
if (knob === undefined) {
|
||||||
throw new Error(`No knob with name ${knobName}`);
|
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
|
* 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).
|
* to the value it had before the wiring set it the first time).
|
||||||
*/
|
*/
|
||||||
render(knob: Knob<T>, onChange: (val: T) => void, onReset: () => void):
|
render(
|
||||||
TemplateResult;
|
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.
|
* wired up by just applying styles to the containerOfRenderedStory.
|
||||||
*/
|
*/
|
||||||
export interface KnobWiring<T> {
|
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.
|
// tslint:disable-next-line:no-any No way to represent this type clearly.
|
||||||
export function materialInitsToStoryInits<T extends {[name: string]: any}>(
|
export function materialInitsToStoryInits<T extends {[name: string]: any}>(
|
||||||
inits: Array<MaterialStoryInit<T>>):
|
inits: Array<MaterialStoryInit<T>>,
|
||||||
Array<LitStoryInit<KnobValues<KnobTypesToKnobs<T>>>> {
|
): Array<LitStoryInit<KnobValues<KnobTypesToKnobs<T>>>> {
|
||||||
return inits.map((init) => {
|
return inits.map((init) => {
|
||||||
return {
|
return {
|
||||||
name: init.name,
|
name: init.name,
|
||||||
|
@ -24,14 +24,15 @@ export interface BaseStoryInit {
|
|||||||
type GenericKnobValues = KnobValues<PolymorphicArrayOfKnobs>;
|
type GenericKnobValues = KnobValues<PolymorphicArrayOfKnobs>;
|
||||||
|
|
||||||
/** A story with an arbitrary render function. */
|
/** A story with an arbitrary render function. */
|
||||||
export interface StoryInit<
|
export interface StoryInit<KV extends GenericKnobValues = GenericKnobValues>
|
||||||
KV extends GenericKnobValues = GenericKnobValues> extends BaseStoryInit {
|
extends BaseStoryInit {
|
||||||
render(container: HTMLElement | DocumentFragment, knobs: KV): Promise<void>;
|
render(container: HTMLElement | DocumentFragment, knobs: KV): Promise<void>;
|
||||||
styles?: CSSStyleSheet[];
|
styles?: CSSStyleSheet[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
|
class StoryImpl<
|
||||||
PolymorphicArrayOfKnobs> {
|
Knobs extends PolymorphicArrayOfKnobs = PolymorphicArrayOfKnobs,
|
||||||
|
> {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly description: string | undefined;
|
readonly description: string | undefined;
|
||||||
@ -54,13 +55,16 @@ class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
|
|||||||
this.description = init.description;
|
this.description = init.description;
|
||||||
this.id = init.id || this.name.replace(/ /g, '_').replace(/,/g, '');
|
this.id = init.id || this.name.replace(/ /g, '_').replace(/,/g, '');
|
||||||
if (this.id.includes('/')) {
|
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 ` +
|
`The name can, but if so you have to give an ` +
|
||||||
`explicit id that doesn't.`;
|
`explicit id that doesn't.`;
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
const wrapperDivMap =
|
const wrapperDivMap = new WeakMap<
|
||||||
new WeakMap<HTMLElement|DocumentFragment, HTMLDivElement>();
|
HTMLElement | DocumentFragment,
|
||||||
|
HTMLDivElement
|
||||||
|
>();
|
||||||
|
|
||||||
this.initStyles = init.styles;
|
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
|
* The type is exposed here, but the implementation isn't, because a Story
|
||||||
* must be constructed via a Collection.
|
* must be constructed via a Collection.
|
||||||
*/
|
*/
|
||||||
export type Story<Knobs extends PolymorphicArrayOfKnobs =
|
export type Story<
|
||||||
PolymorphicArrayOfKnobs> = StoryImpl<Knobs>;
|
Knobs extends PolymorphicArrayOfKnobs = PolymorphicArrayOfKnobs,
|
||||||
|
> = StoryImpl<Knobs>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tree of related stories and sub-collections.
|
* 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
|
* either a toplevel collection for a named component, or it is a member of
|
||||||
* exactly one collection.
|
* exactly one collection.
|
||||||
*/
|
*/
|
||||||
export class Collection<T extends PolymorphicArrayOfKnobs =
|
export class Collection<
|
||||||
ReadonlyArray<Knob<unknown>>> {
|
T extends PolymorphicArrayOfKnobs = ReadonlyArray<Knob<unknown>>,
|
||||||
|
> {
|
||||||
private readonly children = new Map<string, Story | Collection>();
|
private readonly children = new Map<string, Story | Collection>();
|
||||||
readonly customStyles: CSSStyleSheet[] = [];
|
readonly customStyles: CSSStyleSheet[] = [];
|
||||||
private static readonly collectionsByName = new Map<string, Collection>();
|
private static readonly collectionsByName = new Map<string, Collection>();
|
||||||
@ -148,8 +154,9 @@ export class Collection<T extends PolymorphicArrayOfKnobs =
|
|||||||
for (const init of inits) {
|
for (const init of inits) {
|
||||||
const story = new StoryImpl(init, this);
|
const story = new StoryImpl(init, this);
|
||||||
if (this.children.has(story.id)) {
|
if (this.children.has(story.id)) {
|
||||||
const message = `A story or subcollection already exists with the id ${
|
const message = `A story or subcollection already exists with the id ${JSON.stringify(
|
||||||
JSON.stringify(story.id)}`;
|
story.id,
|
||||||
|
)}`;
|
||||||
// Don't throw an error, as that will disrupt live_reload / hot reload,
|
// Don't throw an error, as that will disrupt live_reload / hot reload,
|
||||||
// by halting this module's initialization.
|
// by halting this module's initialization.
|
||||||
console.error(message);
|
console.error(message);
|
||||||
@ -168,8 +175,8 @@ export class Collection<T extends PolymorphicArrayOfKnobs =
|
|||||||
* Describes a single configuration of a specific web UI component.
|
* Describes a single configuration of a specific web UI component.
|
||||||
*/
|
*/
|
||||||
export interface LitStoryInit<
|
export interface LitStoryInit<
|
||||||
KV extends KnobValues<PolymorphicArrayOfKnobs> =
|
KV extends KnobValues<PolymorphicArrayOfKnobs> = KnobValues<PolymorphicArrayOfKnobs>,
|
||||||
KnobValues<PolymorphicArrayOfKnobs>> extends BaseStoryInit {
|
> extends BaseStoryInit {
|
||||||
renderLit(knobs: KV): TemplateResult | Promise<TemplateResult>;
|
renderLit(knobs: KV): TemplateResult | Promise<TemplateResult>;
|
||||||
litStyles?: CSSResult | CSSResult[];
|
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.
|
* A collection with convenience methods for rendering lit-html templates.
|
||||||
*/
|
*/
|
||||||
export class LitCollection<T extends PolymorphicArrayOfKnobs =
|
export class LitCollection<
|
||||||
ReadonlyArray<Knob<unknown>>> extends
|
T extends PolymorphicArrayOfKnobs = ReadonlyArray<Knob<unknown>>,
|
||||||
Collection<T> {
|
> extends Collection<T> {
|
||||||
override addStories(
|
override addStories(
|
||||||
...inits: Array<StoryInit<KnobValues<T>>|LitStoryInit<KnobValues<T>>>) {
|
...inits: Array<StoryInit<KnobValues<T>> | LitStoryInit<KnobValues<T>>>
|
||||||
|
) {
|
||||||
const simpleInits: StoryInit[] = [];
|
const simpleInits: StoryInit[] = [];
|
||||||
for (const init of inits) {
|
for (const init of inits) {
|
||||||
if (isLitStoryInit(init)) {
|
if (isLitStoryInit(init)) {
|
||||||
let styles: CSSStyleSheet[] = [];
|
let styles: CSSStyleSheet[] = [];
|
||||||
if (init.litStyles) {
|
if (init.litStyles) {
|
||||||
styles = init.litStyles instanceof Array ?
|
styles =
|
||||||
init.litStyles.map((s) => s.styleSheet!) :
|
init.litStyles instanceof Array
|
||||||
[init.litStyles.styleSheet!];
|
? init.litStyles.map((s) => s.styleSheet!)
|
||||||
|
: [init.litStyles.styleSheet!];
|
||||||
}
|
}
|
||||||
simpleInits.push({
|
simpleInits.push({
|
||||||
...init,
|
...init,
|
||||||
|
@ -44,7 +44,6 @@ const postdoc = new PostDoc({
|
|||||||
onMessage,
|
onMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
await postdoc.handshake;
|
await postdoc.handshake;
|
||||||
|
|
||||||
// Request the initial theme.
|
// Request the initial theme.
|
||||||
|
@ -20,7 +20,7 @@ describe('<md-checkbox>', () => {
|
|||||||
|
|
||||||
describe('forms', () => {
|
describe('forms', () => {
|
||||||
createFormTests({
|
createFormTests({
|
||||||
queryControl: root => root.querySelector('md-checkbox'),
|
queryControl: (root) => root.querySelector('md-checkbox'),
|
||||||
valueTests: [
|
valueTests: [
|
||||||
{
|
{
|
||||||
name: 'unnamed',
|
name: 'unnamed',
|
||||||
@ -29,7 +29,7 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(formData)
|
expect(formData)
|
||||||
.withContext('should not add anything to form without a name')
|
.withContext('should not add anything to form without a name')
|
||||||
.toHaveSize(0);
|
.toHaveSize(0);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'unchecked',
|
name: 'unchecked',
|
||||||
@ -38,7 +38,7 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(formData)
|
expect(formData)
|
||||||
.withContext('should not add anything to form when unchecked')
|
.withContext('should not add anything to form when unchecked')
|
||||||
.toHaveSize(0);
|
.toHaveSize(0);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'checked default value',
|
name: 'checked default value',
|
||||||
@ -46,26 +46,31 @@ describe('<md-checkbox>', () => {
|
|||||||
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
|
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
|
||||||
assertValue(formData) {
|
assertValue(formData) {
|
||||||
expect(formData.get('checkbox')).toBe('on');
|
expect(formData.get('checkbox')).toBe('on');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'checked custom value',
|
name: 'checked custom value',
|
||||||
render: () =>
|
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) {
|
assertValue(formData) {
|
||||||
expect(formData.get('checkbox')).toBe('Custom value');
|
expect(formData.get('checkbox')).toBe('Custom value');
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'indeterminate',
|
name: 'indeterminate',
|
||||||
render: () =>
|
render: () =>
|
||||||
html`<md-checkbox name="checkbox" checked indeterminate></md-checkbox>`,
|
html`<md-checkbox
|
||||||
|
name="checkbox"
|
||||||
|
checked
|
||||||
|
indeterminate></md-checkbox>`,
|
||||||
assertValue(formData) {
|
assertValue(formData) {
|
||||||
expect(formData)
|
expect(formData)
|
||||||
.withContext(
|
.withContext('should not add anything to form when indeterminate')
|
||||||
'should not add anything to form when indeterminate')
|
|
||||||
.toHaveSize(0);
|
.toHaveSize(0);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'disabled',
|
name: 'disabled',
|
||||||
@ -75,8 +80,8 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(formData)
|
expect(formData)
|
||||||
.withContext('should not add anything to form when disabled')
|
.withContext('should not add anything to form when disabled')
|
||||||
.toHaveSize(0);
|
.toHaveSize(0);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
resetTests: [
|
resetTests: [
|
||||||
{
|
{
|
||||||
@ -89,7 +94,7 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(checkbox.checked)
|
expect(checkbox.checked)
|
||||||
.withContext('checkbox.checked after reset')
|
.withContext('checkbox.checked after reset')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reset to checked',
|
name: 'reset to checked',
|
||||||
@ -102,7 +107,7 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(checkbox.checked)
|
expect(checkbox.checked)
|
||||||
.withContext('checkbox.checked after reset')
|
.withContext('checkbox.checked after reset')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'reset to indeterminate',
|
name: 'reset to indeterminate',
|
||||||
@ -115,8 +120,8 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(checkbox.indeterminate)
|
expect(checkbox.indeterminate)
|
||||||
.withContext('checkbox.indeterminate should not be reset')
|
.withContext('checkbox.indeterminate should not be reset')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
restoreTests: [
|
restoreTests: [
|
||||||
{
|
{
|
||||||
@ -126,7 +131,7 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(checkbox.checked)
|
expect(checkbox.checked)
|
||||||
.withContext('checkbox.checked after restore')
|
.withContext('checkbox.checked after restore')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'restore checked',
|
name: 'restore checked',
|
||||||
@ -136,7 +141,7 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(checkbox.checked)
|
expect(checkbox.checked)
|
||||||
.withContext('checkbox.checked after restore')
|
.withContext('checkbox.checked after restore')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'restore indeterminate',
|
name: 'restore indeterminate',
|
||||||
@ -146,9 +151,9 @@ describe('<md-checkbox>', () => {
|
|||||||
expect(checkbox.indeterminate)
|
expect(checkbox.indeterminate)
|
||||||
.withContext('checkbox.indeterminate should not be restored')
|
.withContext('checkbox.indeterminate should not be restored')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -4,20 +4,27 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Checkbox', [
|
'Checkbox',
|
||||||
|
[
|
||||||
new Knob('checked', {defaultValue: false, ui: boolInput()}),
|
new Knob('checked', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('indeterminate', {defaultValue: false, ui: boolInput()}),
|
new Knob('indeterminate', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
|
|
||||||
import '@material/web/checkbox/checkbox.js';
|
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';
|
import {css, html} from 'lit';
|
||||||
|
|
||||||
/** Knob types for checkbox stories. */
|
/** Knob types for checkbox stories. */
|
||||||
@ -25,8 +28,7 @@ const checkbox: MaterialStoryInit<StoryKnobs> = {
|
|||||||
?checked=${checked}
|
?checked=${checked}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?indeterminate=${indeterminate}
|
?indeterminate=${indeterminate}
|
||||||
touch-target="wrapper"
|
touch-target="wrapper"></md-checkbox>
|
||||||
></md-checkbox>
|
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -53,8 +55,7 @@ const withLabels: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-checkbox
|
<md-checkbox
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
aria-label="Cats"
|
aria-label="Cats"
|
||||||
touch-target="wrapper"
|
touch-target="wrapper"></md-checkbox>
|
||||||
></md-checkbox>
|
|
||||||
Cats
|
Cats
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@ -62,8 +63,7 @@ const withLabels: MaterialStoryInit<StoryKnobs> = {
|
|||||||
checked
|
checked
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
aria-label="dogs"
|
aria-label="dogs"
|
||||||
touch-target="wrapper"
|
touch-target="wrapper"></md-checkbox>
|
||||||
></md-checkbox>
|
|
||||||
Dogs
|
Dogs
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
@ -71,8 +71,7 @@ const withLabels: MaterialStoryInit<StoryKnobs> = {
|
|||||||
indeterminate
|
indeterminate
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
aria-label="Birds"
|
aria-label="Birds"
|
||||||
touch-target="wrapper"
|
touch-target="wrapper"></md-checkbox>
|
||||||
></md-checkbox>
|
|
||||||
Birds
|
Birds
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,11 @@ import {classMap} from 'lit/directives/class-map.js';
|
|||||||
|
|
||||||
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
import {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||||
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.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.
|
* A checkbox component.
|
||||||
@ -26,7 +30,7 @@ export class Checkbox extends LitElement {
|
|||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
static override shadowRootOptions = {
|
static override shadowRootOptions = {
|
||||||
...LitElement.shadowRootOptions,
|
...LitElement.shadowRootOptions,
|
||||||
delegatesFocus: true
|
delegatesFocus: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
@ -130,8 +134,8 @@ export class Checkbox extends LitElement {
|
|||||||
// Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
|
// Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
|
||||||
// Replace with this.internals.validity.customError when resolved.
|
// Replace with this.internals.validity.customError when resolved.
|
||||||
private hasCustomValidityError = false;
|
private hasCustomValidityError = false;
|
||||||
private readonly internals =
|
// Cast needed for closure
|
||||||
(this as HTMLElement /* needed for closure */).attachInternals();
|
private readonly internals = (this as HTMLElement).attachInternals();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -196,8 +200,11 @@ export class Checkbox extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override update(changed: PropertyValues<Checkbox>) {
|
protected override update(changed: PropertyValues<Checkbox>) {
|
||||||
if (changed.has('checked') || changed.has('disabled') ||
|
if (
|
||||||
changed.has('indeterminate')) {
|
changed.has('checked') ||
|
||||||
|
changed.has('disabled') ||
|
||||||
|
changed.has('indeterminate')
|
||||||
|
) {
|
||||||
this.prevChecked = changed.get('checked') ?? this.checked;
|
this.prevChecked = changed.get('checked') ?? this.checked;
|
||||||
this.prevDisabled = changed.get('disabled') ?? this.disabled;
|
this.prevDisabled = changed.get('disabled') ?? this.disabled;
|
||||||
this.prevIndeterminate =
|
this.prevIndeterminate =
|
||||||
@ -235,7 +242,8 @@ export class Checkbox extends LitElement {
|
|||||||
// form.reportValidity() to work in Chrome.
|
// form.reportValidity() to work in Chrome.
|
||||||
return html`
|
return html`
|
||||||
<div class="container ${containerClasses}">
|
<div class="container ${containerClasses}">
|
||||||
<input type="checkbox"
|
<input
|
||||||
|
type="checkbox"
|
||||||
id="input"
|
id="input"
|
||||||
aria-checked=${isIndeterminate ? 'mixed' : nothing}
|
aria-checked=${isIndeterminate ? 'mixed' : nothing}
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}
|
||||||
@ -244,8 +252,7 @@ export class Checkbox extends LitElement {
|
|||||||
?required=${this.required}
|
?required=${this.required}
|
||||||
.indeterminate=${this.indeterminate}
|
.indeterminate=${this.indeterminate}
|
||||||
.checked=${this.checked}
|
.checked=${this.checked}
|
||||||
@change=${this.handleChange}
|
@change=${this.handleChange} />
|
||||||
>
|
|
||||||
|
|
||||||
<div class="outline"></div>
|
<div class="outline"></div>
|
||||||
<div class="background"></div>
|
<div class="background"></div>
|
||||||
@ -284,7 +291,10 @@ export class Checkbox extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.internals.setValidity(
|
this.internals.setValidity(
|
||||||
input.validity, input.validationMessage, this.getInput());
|
input.validity,
|
||||||
|
input.validationMessage,
|
||||||
|
this.getInput(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInput() {
|
private getInput() {
|
||||||
|
@ -23,7 +23,8 @@ describe('checkbox', () => {
|
|||||||
const env = new Environment();
|
const env = new Environment();
|
||||||
|
|
||||||
async function setupTest(
|
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');
|
const element = env.render(template).querySelector('md-test-checkbox');
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Could not query rendered <md-test-checkbox>.');
|
throw new Error('Could not query rendered <md-test-checkbox>.');
|
||||||
@ -84,8 +85,7 @@ describe('checkbox', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('checked', () => {
|
describe('checked', () => {
|
||||||
it('get/set updates the checked property on the native checkbox element',
|
it('get/set updates the checked property on the native checkbox element', async () => {
|
||||||
async () => {
|
|
||||||
const {harness, input} = await setupTest();
|
const {harness, input} = await setupTest();
|
||||||
harness.element.checked = true;
|
harness.element.checked = true;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
@ -95,8 +95,7 @@ describe('checkbox', () => {
|
|||||||
expect(input.checked).toEqual(false);
|
expect(input.checked).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('get/set updates the checked property after user updates checked state',
|
it('get/set updates the checked property after user updates checked state', async () => {
|
||||||
async () => {
|
|
||||||
const {harness, input} = await setupTest();
|
const {harness, input} = await setupTest();
|
||||||
|
|
||||||
// Simulate user interaction setting checked to true.
|
// Simulate user interaction setting checked to true.
|
||||||
@ -120,8 +119,7 @@ describe('checkbox', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('indeterminate', () => {
|
describe('indeterminate', () => {
|
||||||
it('get/set updates the indeterminate property on the native checkbox element',
|
it('get/set updates the indeterminate property on the native checkbox element', async () => {
|
||||||
async () => {
|
|
||||||
const {harness, input} = await setupTest();
|
const {harness, input} = await setupTest();
|
||||||
harness.element.indeterminate = true;
|
harness.element.indeterminate = true;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
@ -138,8 +136,7 @@ describe('checkbox', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('disabled', () => {
|
describe('disabled', () => {
|
||||||
it('get/set updates the disabled property on the native checkbox element',
|
it('get/set updates the disabled property on the native checkbox element', async () => {
|
||||||
async () => {
|
|
||||||
const {harness, input} = await setupTest();
|
const {harness, input} = await setupTest();
|
||||||
harness.element.disabled = true;
|
harness.element.disabled = true;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
@ -153,14 +150,12 @@ describe('checkbox', () => {
|
|||||||
|
|
||||||
describe('form submission', () => {
|
describe('form submission', () => {
|
||||||
async function setupFormTest(propsInit: Partial<Checkbox> = {}) {
|
async function setupFormTest(propsInit: Partial<Checkbox> = {}) {
|
||||||
return await setupTest(html`
|
return await setupTest(html` <form>
|
||||||
<form>
|
|
||||||
<md-test-checkbox
|
<md-test-checkbox
|
||||||
.checked=${propsInit.checked === true}
|
.checked=${propsInit.checked === true}
|
||||||
.disabled=${propsInit.disabled === true}
|
.disabled=${propsInit.disabled === true}
|
||||||
.name=${propsInit.name ?? ''}
|
.name=${propsInit.name ?? ''}
|
||||||
.value=${propsInit.value ?? ''}
|
.value=${propsInit.value ?? ''}></md-test-checkbox>
|
||||||
></md-test-checkbox>
|
|
||||||
</form>`);
|
</form>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,8 +166,11 @@ describe('checkbox', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not submit if disabled', async () => {
|
it('does not submit if disabled', async () => {
|
||||||
const {harness} =
|
const {harness} = await setupFormTest({
|
||||||
await setupFormTest({name: 'foo', checked: true, disabled: true});
|
name: 'foo',
|
||||||
|
checked: true,
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
const formData = await harness.submitForm();
|
const formData = await harness.submitForm();
|
||||||
expect(formData.get('foo')).toBeNull();
|
expect(formData.get('foo')).toBeNull();
|
||||||
});
|
});
|
||||||
@ -185,8 +183,11 @@ describe('checkbox', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('submits under correct conditions', async () => {
|
it('submits under correct conditions', async () => {
|
||||||
const {harness} =
|
const {harness} = await setupFormTest({
|
||||||
await setupFormTest({name: 'foo', checked: true, value: 'bar'});
|
name: 'foo',
|
||||||
|
checked: true,
|
||||||
|
value: 'bar',
|
||||||
|
});
|
||||||
const formData = await harness.submitForm();
|
const formData = await harness.submitForm();
|
||||||
expect(formData.get('foo')).toEqual('bar');
|
expect(formData.get('foo')).toEqual('bar');
|
||||||
});
|
});
|
||||||
@ -199,13 +200,17 @@ describe('checkbox', () => {
|
|||||||
<md-test-checkbox></md-test-checkbox>
|
<md-test-checkbox></md-test-checkbox>
|
||||||
</label>
|
</label>
|
||||||
`);
|
`);
|
||||||
const label = (test.harness.element.getRootNode() as HTMLElement)
|
const label = (
|
||||||
.querySelector<HTMLLabelElement>('label')!;
|
test.harness.element.getRootNode() as HTMLElement
|
||||||
|
).querySelector<HTMLLabelElement>('label')!;
|
||||||
return {...test, label};
|
return {...test, label};
|
||||||
}
|
}
|
||||||
|
|
||||||
it('toggles when label is clicked', async () => {
|
it('toggles when label is clicked', async () => {
|
||||||
const {harness: {element}, label} = await setupLabelTest();
|
const {
|
||||||
|
harness: {element},
|
||||||
|
label,
|
||||||
|
} = await setupLabelTest();
|
||||||
label.click();
|
label.click();
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(element.checked).toBeTrue();
|
expect(element.checked).toBeTrue();
|
||||||
|
@ -4,21 +4,28 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob, textInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Chips', [
|
'Chips',
|
||||||
|
[
|
||||||
new Knob('label', {defaultValue: '', ui: textInput()}),
|
new Knob('label', {defaultValue: '', ui: textInput()}),
|
||||||
new Knob('elevated', {defaultValue: false, ui: boolInput()}),
|
new Knob('elevated', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('scrolling', {defaultValue: false, ui: boolInput()}),
|
new Knob('scrolling', {defaultValue: false, ui: boolInput()}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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/assist-chip.js';
|
||||||
|
import '@material/web/chips/chip-set.js';
|
||||||
import '@material/web/chips/filter-chip.js';
|
import '@material/web/chips/filter-chip.js';
|
||||||
import '@material/web/chips/input-chip.js';
|
import '@material/web/chips/input-chip.js';
|
||||||
import '@material/web/chips/suggestion-chip.js';
|
import '@material/web/chips/suggestion-chip.js';
|
||||||
|
import '@material/web/icon/icon.js';
|
||||||
|
|
||||||
import {MaterialStoryInit} from './material-collection.js';
|
import {MaterialStoryInit} from './material-collection.js';
|
||||||
import {css, html, svg} from 'lit';
|
import {css, html, svg} from 'lit';
|
||||||
@ -53,13 +53,11 @@ const assist: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-assist-chip
|
<md-assist-chip
|
||||||
label=${label || 'Assist chip'}
|
label=${label || 'Assist chip'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}></md-assist-chip>
|
||||||
></md-assist-chip>
|
|
||||||
<md-assist-chip
|
<md-assist-chip
|
||||||
label=${label || 'Assist chip with icon'}
|
label=${label || 'Assist chip with icon'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">local_laundry_service</md-icon>
|
<md-icon slot="icon">local_laundry_service</md-icon>
|
||||||
</md-assist-chip>
|
</md-assist-chip>
|
||||||
<md-assist-chip
|
<md-assist-chip
|
||||||
@ -67,16 +65,16 @@ const assist: MaterialStoryInit<StoryKnobs> = {
|
|||||||
?elevated=${elevated}
|
?elevated=${elevated}
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>${GOOGLE_LOGO}</md-assist-chip>
|
>${GOOGLE_LOGO}</md-assist-chip
|
||||||
|
>
|
||||||
<md-assist-chip
|
<md-assist-chip
|
||||||
label=${label || 'Disabled assist chip (focusable)'}
|
label=${label || 'Disabled assist chip (focusable)'}
|
||||||
disabled
|
disabled
|
||||||
always-focusable
|
always-focusable
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}></md-assist-chip>
|
||||||
></md-assist-chip>
|
|
||||||
</md-chip-set>
|
</md-chip-set>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const filters: MaterialStoryInit<StoryKnobs> = {
|
const filters: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -85,36 +83,31 @@ const filters: MaterialStoryInit<StoryKnobs> = {
|
|||||||
render({label, elevated, disabled, scrolling}) {
|
render({label, elevated, disabled, scrolling}) {
|
||||||
const classes = {'scrolling': scrolling};
|
const classes = {'scrolling': scrolling};
|
||||||
return html`
|
return html`
|
||||||
<md-chip-set class=${classMap(classes)}
|
<md-chip-set class=${classMap(classes)} aria-label="Filter chips">
|
||||||
aria-label="Filter chips">
|
|
||||||
<md-filter-chip
|
<md-filter-chip
|
||||||
label=${label || 'Filter chip'}
|
label=${label || 'Filter chip'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}></md-filter-chip>
|
||||||
></md-filter-chip>
|
|
||||||
<md-filter-chip
|
<md-filter-chip
|
||||||
label=${label || 'Filter chip with icon'}
|
label=${label || 'Filter chip with icon'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">local_laundry_service</md-icon>
|
<md-icon slot="icon">local_laundry_service</md-icon>
|
||||||
</md-filter-chip>
|
</md-filter-chip>
|
||||||
<md-filter-chip
|
<md-filter-chip
|
||||||
label=${label || 'Removable filter chip'}
|
label=${label || 'Removable filter chip'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}
|
||||||
removable
|
removable></md-filter-chip>
|
||||||
></md-filter-chip>
|
|
||||||
<md-filter-chip
|
<md-filter-chip
|
||||||
label=${label || 'Disabled filter chip (focusable)'}
|
label=${label || 'Disabled filter chip (focusable)'}
|
||||||
disabled
|
disabled
|
||||||
always-focusable
|
always-focusable
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}
|
||||||
removable
|
removable></md-filter-chip>
|
||||||
></md-filter-chip>
|
|
||||||
</md-chip-set>
|
</md-chip-set>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputs: MaterialStoryInit<StoryKnobs> = {
|
const inputs: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -126,39 +119,37 @@ const inputs: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-chip-set class=${classMap(classes)} aria-label="Input chips">
|
<md-chip-set class=${classMap(classes)} aria-label="Input chips">
|
||||||
<md-input-chip
|
<md-input-chip
|
||||||
label=${label || 'Input chip'}
|
label=${label || 'Input chip'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}></md-input-chip>
|
||||||
></md-input-chip>
|
|
||||||
<md-input-chip
|
<md-input-chip
|
||||||
label=${label || 'Input chip with icon'}
|
label=${label || 'Input chip with icon'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">local_laundry_service</md-icon>
|
<md-icon slot="icon">local_laundry_service</md-icon>
|
||||||
</md-input-chip>
|
</md-input-chip>
|
||||||
<md-input-chip
|
<md-input-chip
|
||||||
label=${label || 'Input chip with avatar'}
|
label=${label || 'Input chip with avatar'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
avatar
|
avatar>
|
||||||
>
|
<img
|
||||||
<img slot="icon" src="https://lh3.googleusercontent.com/a/default-user=s48">
|
slot="icon"
|
||||||
|
src="https://lh3.googleusercontent.com/a/default-user=s48" />
|
||||||
</md-input-chip>
|
</md-input-chip>
|
||||||
<md-input-chip
|
<md-input-chip
|
||||||
label=${label || 'Input link chip'}
|
label=${label || 'Input link chip'}
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>${GOOGLE_LOGO}</md-input-chip>
|
>${GOOGLE_LOGO}</md-input-chip
|
||||||
|
>
|
||||||
<md-input-chip
|
<md-input-chip
|
||||||
label=${label || 'Remove-only input chip'}
|
label=${label || 'Remove-only input chip'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
remove-only
|
remove-only></md-input-chip>
|
||||||
></md-input-chip>
|
|
||||||
<md-input-chip
|
<md-input-chip
|
||||||
label=${label || 'Disabled input chip (focusable)'}
|
label=${label || 'Disabled input chip (focusable)'}
|
||||||
disabled
|
disabled
|
||||||
always-focusable
|
always-focusable></md-input-chip>
|
||||||
></md-input-chip>
|
|
||||||
</md-chip-set>
|
</md-chip-set>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const suggestions: MaterialStoryInit<StoryKnobs> = {
|
const suggestions: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -171,13 +162,11 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-suggestion-chip
|
<md-suggestion-chip
|
||||||
label=${label || 'Suggestion chip'}
|
label=${label || 'Suggestion chip'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}></md-suggestion-chip>
|
||||||
></md-suggestion-chip>
|
|
||||||
<md-suggestion-chip
|
<md-suggestion-chip
|
||||||
label=${label || 'Suggestion chip with icon'}
|
label=${label || 'Suggestion chip with icon'}
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">local_laundry_service</md-icon>
|
<md-icon slot="icon">local_laundry_service</md-icon>
|
||||||
</md-suggestion-chip>
|
</md-suggestion-chip>
|
||||||
<md-suggestion-chip
|
<md-suggestion-chip
|
||||||
@ -185,16 +174,16 @@ const suggestions: MaterialStoryInit<StoryKnobs> = {
|
|||||||
?elevated=${elevated}
|
?elevated=${elevated}
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>${GOOGLE_LOGO}</md-suggestion-chip>
|
>${GOOGLE_LOGO}</md-suggestion-chip
|
||||||
|
>
|
||||||
<md-suggestion-chip
|
<md-suggestion-chip
|
||||||
label=${label || 'Disabled suggestion chip (focusable)'}
|
label=${label || 'Disabled suggestion chip (focusable)'}
|
||||||
disabled
|
disabled
|
||||||
always-focusable
|
always-focusable
|
||||||
?elevated=${elevated}
|
?elevated=${elevated}></md-suggestion-chip>
|
||||||
></md-suggestion-chip>
|
|
||||||
</md-chip-set>
|
</md-chip-set>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Chips stories. */
|
/** Chips stories. */
|
||||||
|
@ -28,6 +28,10 @@ declare global {
|
|||||||
@customElement('md-filter-chip')
|
@customElement('md-filter-chip')
|
||||||
export class MdFilterChip extends FilterChip {
|
export class MdFilterChip extends FilterChip {
|
||||||
static override styles = [
|
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() {
|
protected override async getInteractiveElement() {
|
||||||
await this.element.updateComplete;
|
await this.element.updateComplete;
|
||||||
const {primaryId} = this.element as unknown as {primaryId: string};
|
const {primaryId} = this.element as unknown as {primaryId: string};
|
||||||
const primaryAction = primaryId &&
|
const primaryAction =
|
||||||
|
primaryId &&
|
||||||
this.element.renderRoot.querySelector<HTMLElement>(`#${primaryId}`);
|
this.element.renderRoot.querySelector<HTMLElement>(`#${primaryId}`);
|
||||||
// Retrieve MultiActionChip's trailingAction
|
// Retrieve MultiActionChip's trailingAction
|
||||||
const {trailingAction} =
|
const {trailingAction} = this.element as {
|
||||||
this.element as {trailingAction?: HTMLElement | null};
|
trailingAction?: HTMLElement | null;
|
||||||
|
};
|
||||||
|
|
||||||
// Default to trailing action if there isn't a primary action and the user
|
// Default to trailing action if there isn't a primary action and the user
|
||||||
// didn't explicitly set `harness.action = 'trailing'` (remove-only input
|
// 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 (this.action === 'trailing' || !primaryAction) {
|
||||||
if (!trailingAction) {
|
if (!trailingAction) {
|
||||||
throw new Error(
|
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;
|
return trailingAction;
|
||||||
@ -37,7 +40,8 @@ export class ChipHarness extends Harness<Chip> {
|
|||||||
|
|
||||||
if (!primaryAction) {
|
if (!primaryAction) {
|
||||||
throw new Error(
|
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;
|
return primaryAction;
|
||||||
|
@ -26,6 +26,10 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
@customElement('md-input-chip')
|
@customElement('md-input-chip')
|
||||||
export class MdInputChip extends InputChip {
|
export class MdInputChip extends InputChip {
|
||||||
static override styles =
|
static override styles = [
|
||||||
[sharedStyles, trailingIconStyles, selectableStyles, styles];
|
sharedStyles,
|
||||||
|
trailingIconStyles,
|
||||||
|
selectableStyles,
|
||||||
|
styles,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -44,22 +44,26 @@ export class AssistChip extends Chip {
|
|||||||
const {ariaLabel} = this as ARIAMixinStrict;
|
const {ariaLabel} = this as ARIAMixinStrict;
|
||||||
if (this.href) {
|
if (this.href) {
|
||||||
return html`
|
return html`
|
||||||
<a class="primary action"
|
<a
|
||||||
|
class="primary action"
|
||||||
id="link"
|
id="link"
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}
|
||||||
href=${this.href}
|
href=${this.href}
|
||||||
target=${this.target || nothing}
|
target=${this.target || nothing}
|
||||||
>${content}</a>
|
>${content}</a
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<button class="primary action"
|
<button
|
||||||
|
class="primary action"
|
||||||
id="button"
|
id="button"
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}
|
||||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||||
type="button"
|
type="button"
|
||||||
>${content}</button>
|
>${content}</button
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,10 @@
|
|||||||
import {html, isServer, LitElement} from 'lit';
|
import {html, isServer, LitElement} from 'lit';
|
||||||
import {queryAssignedElements} from 'lit/decorators.js';
|
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';
|
import {Chip} from './chip.js';
|
||||||
|
|
||||||
@ -21,12 +24,16 @@ export class ChipSet extends LitElement {
|
|||||||
|
|
||||||
get chips() {
|
get chips() {
|
||||||
return this.childElements.filter(
|
return this.childElements.filter(
|
||||||
(child): child is Chip => child instanceof Chip);
|
(child): child is Chip => child instanceof Chip,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@queryAssignedElements() private readonly childElements!: HTMLElement[];
|
@queryAssignedElements() private readonly childElements!: HTMLElement[];
|
||||||
private readonly internals = polyfillElementInternalsAria(
|
private readonly internals = polyfillElementInternalsAria(
|
||||||
this, (this as HTMLElement /* needed for closure */).attachInternals());
|
this,
|
||||||
|
// Cast needed for closure
|
||||||
|
(this as HTMLElement).attachInternals(),
|
||||||
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -71,7 +78,7 @@ export class ChipSet extends LitElement {
|
|||||||
// Check if moving forwards or backwards
|
// Check if moving forwards or backwards
|
||||||
const isRtl = getComputedStyle(this).direction === 'rtl';
|
const isRtl = getComputedStyle(this).direction === 'rtl';
|
||||||
const forwards = isRtl ? isLeft : isRight;
|
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 (!focusedChip) {
|
||||||
// If there is not already a chip focused, select the first or last chip
|
// If there is not already a chip focused, select the first or last chip
|
||||||
// based on the direction we're traveling.
|
// based on the direction we're traveling.
|
||||||
|
@ -19,14 +19,11 @@ import {ChipSet} from './chip-set.js';
|
|||||||
import {InputChip} from './input-chip.js';
|
import {InputChip} from './input-chip.js';
|
||||||
|
|
||||||
@customElement('test-chip-set')
|
@customElement('test-chip-set')
|
||||||
class TestChipSet extends ChipSet {
|
class TestChipSet extends ChipSet {}
|
||||||
}
|
|
||||||
@customElement('test-chip-set-assist-chip')
|
@customElement('test-chip-set-assist-chip')
|
||||||
class TestAssistChip extends AssistChip {
|
class TestAssistChip extends AssistChip {}
|
||||||
}
|
|
||||||
@customElement('test-chip-set-input-chip')
|
@customElement('test-chip-set-input-chip')
|
||||||
class TestInputChip extends InputChip {
|
class TestInputChip extends InputChip {}
|
||||||
}
|
|
||||||
|
|
||||||
describe('Chip set', () => {
|
describe('Chip set', () => {
|
||||||
const env = new Environment();
|
const env = new Environment();
|
||||||
@ -61,8 +58,11 @@ describe('Chip set', () => {
|
|||||||
|
|
||||||
describe('navigation', () => {
|
describe('navigation', () => {
|
||||||
it('should add tabindex="-1" to all chips except the first', async () => {
|
it('should add tabindex="-1" to all chips except the first', async () => {
|
||||||
const chipSet = await setupTest(
|
const chipSet = await setupTest([
|
||||||
[new TestAssistChip(), new TestAssistChip(), new TestAssistChip()]);
|
new TestAssistChip(),
|
||||||
|
new TestAssistChip(),
|
||||||
|
new TestAssistChip(),
|
||||||
|
]);
|
||||||
|
|
||||||
expect(chipSet.chips[0].getAttribute('tabindex'))
|
expect(chipSet.chips[0].getAttribute('tabindex'))
|
||||||
.withContext('first tabindex')
|
.withContext('first tabindex')
|
||||||
@ -75,12 +75,18 @@ describe('Chip set', () => {
|
|||||||
.toBe('-1');
|
.toBe('-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testNavigation({chipSet, ltrKey, rtlKey, current, next}: {
|
async function testNavigation({
|
||||||
chipSet: ChipSet,
|
chipSet,
|
||||||
ltrKey: string,
|
ltrKey,
|
||||||
rtlKey: string,
|
rtlKey,
|
||||||
current: Chip|null,
|
current,
|
||||||
next: Chip,
|
next,
|
||||||
|
}: {
|
||||||
|
chipSet: ChipSet;
|
||||||
|
ltrKey: string;
|
||||||
|
rtlKey: string;
|
||||||
|
current: Chip | null;
|
||||||
|
next: Chip;
|
||||||
}) {
|
}) {
|
||||||
const harness = current ? new ChipHarness(current) : new Harness(chipSet);
|
const harness = current ? new ChipHarness(current) : new Harness(chipSet);
|
||||||
// Don't use harness focusing since we need to test real focus states
|
// Don't use harness focusing since we need to test real focus states
|
||||||
@ -109,7 +115,7 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'ArrowRight',
|
ltrKey: 'ArrowRight',
|
||||||
rtlKey: 'ArrowLeft',
|
rtlKey: 'ArrowLeft',
|
||||||
current: first,
|
current: first,
|
||||||
next: second
|
next: second,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -123,7 +129,7 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'ArrowLeft',
|
ltrKey: 'ArrowLeft',
|
||||||
rtlKey: 'ArrowRight',
|
rtlKey: 'ArrowRight',
|
||||||
current: second,
|
current: second,
|
||||||
next: first
|
next: first,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,7 +143,7 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'Home',
|
ltrKey: 'Home',
|
||||||
rtlKey: 'Home',
|
rtlKey: 'Home',
|
||||||
current: second,
|
current: second,
|
||||||
next: first
|
next: first,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -151,12 +157,11 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'End',
|
ltrKey: 'End',
|
||||||
rtlKey: 'End',
|
rtlKey: 'End',
|
||||||
current: second,
|
current: second,
|
||||||
next: third
|
next: third,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to first chip on forward when none focused',
|
it('should navigate to first chip on forward when none focused', async () => {
|
||||||
async () => {
|
|
||||||
const first = new TestAssistChip();
|
const first = new TestAssistChip();
|
||||||
const second = new TestAssistChip();
|
const second = new TestAssistChip();
|
||||||
const third = new TestAssistChip();
|
const third = new TestAssistChip();
|
||||||
@ -166,12 +171,11 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'ArrowRight',
|
ltrKey: 'ArrowRight',
|
||||||
rtlKey: 'ArrowLeft',
|
rtlKey: 'ArrowLeft',
|
||||||
current: null,
|
current: null,
|
||||||
next: first
|
next: first,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should navigate to last chip on backward when none focused',
|
it('should navigate to last chip on backward when none focused', async () => {
|
||||||
async () => {
|
|
||||||
const first = new TestAssistChip();
|
const first = new TestAssistChip();
|
||||||
const second = new TestAssistChip();
|
const second = new TestAssistChip();
|
||||||
const third = new TestAssistChip();
|
const third = new TestAssistChip();
|
||||||
@ -181,7 +185,7 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'ArrowLeft',
|
ltrKey: 'ArrowLeft',
|
||||||
rtlKey: 'ArrowRight',
|
rtlKey: 'ArrowRight',
|
||||||
current: null,
|
current: null,
|
||||||
next: third
|
next: third,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -196,7 +200,7 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'ArrowRight',
|
ltrKey: 'ArrowRight',
|
||||||
rtlKey: 'ArrowLeft',
|
rtlKey: 'ArrowLeft',
|
||||||
current: first,
|
current: first,
|
||||||
next: third
|
next: third,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -212,7 +216,7 @@ describe('Chip set', () => {
|
|||||||
ltrKey: 'ArrowRight',
|
ltrKey: 'ArrowRight',
|
||||||
rtlKey: 'ArrowLeft',
|
rtlKey: 'ArrowLeft',
|
||||||
current: first,
|
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
|
// Don't use harness focusing since we need to test real focus states
|
||||||
second.focus();
|
second.focus();
|
||||||
await harness.keypress('ArrowLeft');
|
await harness.keypress('ArrowLeft');
|
||||||
const {trailingAction} =
|
const {trailingAction} = first as unknown as {
|
||||||
first as unknown as {trailingAction: HTMLElement};
|
trailingAction: HTMLElement;
|
||||||
|
};
|
||||||
expect(trailingAction.matches(':focus-within'))
|
expect(trailingAction.matches(':focus-within'))
|
||||||
.withContext('trailing action of first chip is focused')
|
.withContext('trailing action of first chip is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
|
@ -24,7 +24,7 @@ export abstract class Chip extends LitElement {
|
|||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
static override shadowRootOptions = {
|
static override shadowRootOptions = {
|
||||||
...LitElement.shadowRootOptions,
|
...LitElement.shadowRootOptions,
|
||||||
delegatesFocus: true
|
delegatesFocus: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,9 +93,9 @@ export abstract class Chip extends LitElement {
|
|||||||
protected renderContainerContent() {
|
protected renderContainerContent() {
|
||||||
return html`
|
return html`
|
||||||
${this.renderOutline()}
|
${this.renderOutline()}
|
||||||
<md-focus-ring part="focus-ring"
|
<md-focus-ring part="focus-ring" for=${this.primaryId}></md-focus-ring>
|
||||||
for=${this.primaryId}></md-focus-ring>
|
<md-ripple
|
||||||
<md-ripple for=${this.primaryId}
|
for=${this.primaryId}
|
||||||
?disabled=${this.rippleDisabled}></md-ripple>
|
?disabled=${this.rippleDisabled}></md-ripple>
|
||||||
${this.renderPrimaryAction(this.renderPrimaryContent())}
|
${this.renderPrimaryAction(this.renderPrimaryContent())}
|
||||||
`;
|
`;
|
||||||
|
@ -33,8 +33,7 @@ describe('Chip', () => {
|
|||||||
return {chip, harness: new ChipHarness(chip)};
|
return {chip, harness: new ChipHarness(chip)};
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should dispatch `update-focus` for chip set when disabled changes',
|
it('should dispatch `update-focus` for chip set when disabled changes', async () => {
|
||||||
async () => {
|
|
||||||
const {chip} = await setupTest();
|
const {chip} = await setupTest();
|
||||||
const updateFocusListener = jasmine.createSpy('updateFocusListener');
|
const updateFocusListener = jasmine.createSpy('updateFocusListener');
|
||||||
chip.addEventListener('update-focus', updateFocusListener);
|
chip.addEventListener('update-focus', updateFocusListener);
|
||||||
|
@ -27,7 +27,8 @@ export class FilterChip extends MultiActionChip {
|
|||||||
return 'button';
|
return 'button';
|
||||||
}
|
}
|
||||||
|
|
||||||
@query('.primary.action') protected readonly primaryAction!: HTMLElement|null;
|
@query('.primary.action')
|
||||||
|
protected readonly primaryAction!: HTMLElement | null;
|
||||||
@query('.trailing.action')
|
@query('.trailing.action')
|
||||||
protected readonly trailingAction!: HTMLElement | null;
|
protected readonly trailingAction!: HTMLElement | null;
|
||||||
|
|
||||||
@ -43,13 +44,15 @@ export class FilterChip extends MultiActionChip {
|
|||||||
protected override renderPrimaryAction(content: unknown) {
|
protected override renderPrimaryAction(content: unknown) {
|
||||||
const {ariaLabel} = this as ARIAMixinStrict;
|
const {ariaLabel} = this as ARIAMixinStrict;
|
||||||
return html`
|
return html`
|
||||||
<button class="primary action"
|
<button
|
||||||
|
class="primary action"
|
||||||
id="button"
|
id="button"
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}
|
||||||
aria-pressed=${this.selected}
|
aria-pressed=${this.selected}
|
||||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||||
@click=${this.handleClick}
|
@click=${this.handleClick}
|
||||||
>${content}</button>
|
>${content}</button
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +63,8 @@ export class FilterChip extends MultiActionChip {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<svg class="checkmark" viewBox="0 0 18 18" aria-hidden="true">
|
<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>
|
</svg>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -70,7 +74,7 @@ export class FilterChip extends MultiActionChip {
|
|||||||
return renderRemoveButton({
|
return renderRemoveButton({
|
||||||
focusListener,
|
focusListener,
|
||||||
ariaLabel: this.ariaLabelRemove,
|
ariaLabel: this.ariaLabelRemove,
|
||||||
disabled: this.disabled
|
disabled: this.disabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,12 +68,14 @@ export class InputChip extends MultiActionChip {
|
|||||||
const {ariaLabel} = this as ARIAMixinStrict;
|
const {ariaLabel} = this as ARIAMixinStrict;
|
||||||
if (this.href) {
|
if (this.href) {
|
||||||
return html`
|
return html`
|
||||||
<a class="primary action"
|
<a
|
||||||
|
class="primary action"
|
||||||
id="link"
|
id="link"
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}
|
||||||
href=${this.href}
|
href=${this.href}
|
||||||
target=${this.target || nothing}
|
target=${this.target || nothing}
|
||||||
>${content}</a>
|
>${content}</a
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,12 +88,14 @@ export class InputChip extends MultiActionChip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<button class="primary action"
|
<button
|
||||||
|
class="primary action"
|
||||||
id="button"
|
id="button"
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}
|
||||||
?disabled=${this.disabled && !this.alwaysFocusable}
|
?disabled=${this.disabled && !this.alwaysFocusable}
|
||||||
type="button"
|
type="button"
|
||||||
>${content}</button>
|
>${content}</button
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,8 +67,9 @@ export abstract class MultiActionChip extends Chip {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract renderTrailingAction(focusListener: EventListener):
|
protected abstract renderTrailingAction(
|
||||||
unknown;
|
focusListener: EventListener,
|
||||||
|
): unknown;
|
||||||
|
|
||||||
private handleKeyDown(event: KeyboardEvent) {
|
private handleKeyDown(event: KeyboardEvent) {
|
||||||
const isLeft = event.key === 'ArrowLeft';
|
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
|
// shift+tab from the trailing action to move to the previous chip rather
|
||||||
// than the primary action in the same chip.
|
// than the primary action in the same chip.
|
||||||
primaryAction.tabIndex = -1;
|
primaryAction.tabIndex = -1;
|
||||||
trailingAction.addEventListener('focusout', () => {
|
trailingAction.addEventListener(
|
||||||
|
'focusout',
|
||||||
|
() => {
|
||||||
primaryAction.tabIndex = 0;
|
primaryAction.tabIndex = 0;
|
||||||
}, {once: true});
|
},
|
||||||
|
{once: true},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,11 @@ import {renderRemoveButton} from './trailing-icons.js';
|
|||||||
|
|
||||||
@customElement('test-multi-action-chip')
|
@customElement('test-multi-action-chip')
|
||||||
class TestMultiActionChip extends MultiActionChip {
|
class TestMultiActionChip extends MultiActionChip {
|
||||||
static override styles = css`:host { position: relative; }`;
|
static override styles = css`
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
@query('#primary') primaryAction!: HTMLElement;
|
@query('#primary') primaryAction!: HTMLElement;
|
||||||
@query('.trailing.action') trailingAction!: HTMLElement;
|
@query('.trailing.action') trailingAction!: HTMLElement;
|
||||||
@ -37,7 +41,7 @@ class TestMultiActionChip extends MultiActionChip {
|
|||||||
return renderRemoveButton({
|
return renderRemoveButton({
|
||||||
focusListener,
|
focusListener,
|
||||||
ariaLabel: this.ariaLabelRemove,
|
ariaLabel: this.ariaLabelRemove,
|
||||||
disabled: this.disabled
|
disabled: this.disabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,8 +136,7 @@ describe('Multi-action chips', () => {
|
|||||||
expect(keydownHandler).not.toHaveBeenCalled();
|
expect(keydownHandler).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should bubble event when navigating forward past trailing action',
|
it('should bubble event when navigating forward past trailing action', async () => {
|
||||||
async () => {
|
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
const trailingHarness = new ChipHarness(chip);
|
const trailingHarness = new ChipHarness(chip);
|
||||||
trailingHarness.action = 'trailing';
|
trailingHarness.action = 'trailing';
|
||||||
@ -149,8 +152,7 @@ describe('Multi-action chips', () => {
|
|||||||
expect(keydownHandler).toHaveBeenCalledTimes(1);
|
expect(keydownHandler).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should bubble event when navigating backward before primary action',
|
it('should bubble event when navigating backward before primary action', async () => {
|
||||||
async () => {
|
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
const primaryHarness = new ChipHarness(chip);
|
const primaryHarness = new ChipHarness(chip);
|
||||||
const keydownHandler = jasmine.createSpy();
|
const keydownHandler = jasmine.createSpy();
|
||||||
@ -205,12 +207,11 @@ describe('Multi-action chips', () => {
|
|||||||
expect(handler).toHaveBeenCalledTimes(1);
|
expect(handler).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not remove chip if "remove" event is default prevented',
|
it('should not remove chip if "remove" event is default prevented', async () => {
|
||||||
async () => {
|
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
const harness = new ChipHarness(chip);
|
const harness = new ChipHarness(chip);
|
||||||
harness.action = 'trailing';
|
harness.action = 'trailing';
|
||||||
chip.addEventListener('remove', event => {
|
chip.addEventListener('remove', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -227,8 +228,7 @@ describe('Multi-action chips', () => {
|
|||||||
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.label}`);
|
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.label}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided',
|
it('should provide a default "ariaLabelRemove" when "ariaLabel" is provided', async () => {
|
||||||
async () => {
|
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
chip.label = 'Label';
|
chip.label = 'Label';
|
||||||
chip.ariaLabel = 'Descriptive label';
|
chip.ariaLabel = 'Descriptive label';
|
||||||
|
@ -19,20 +19,24 @@ interface RemoveButtonProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @protected */
|
/** @protected */
|
||||||
export function renderRemoveButton(
|
export function renderRemoveButton({
|
||||||
{ariaLabel, disabled, focusListener, tabbable = false}:
|
ariaLabel,
|
||||||
RemoveButtonProperties) {
|
disabled,
|
||||||
|
focusListener,
|
||||||
|
tabbable = false,
|
||||||
|
}: RemoveButtonProperties) {
|
||||||
return html`
|
return html`
|
||||||
<button class="trailing action"
|
<button
|
||||||
|
class="trailing action"
|
||||||
aria-label=${ariaLabel}
|
aria-label=${ariaLabel}
|
||||||
tabindex=${!tabbable ? -1 : nothing}
|
tabindex=${!tabbable ? -1 : nothing}
|
||||||
@click=${handleRemoveClick}
|
@click=${handleRemoveClick}
|
||||||
@focus=${focusListener}
|
@focus=${focusListener}>
|
||||||
>
|
|
||||||
<md-focus-ring part="trailing-focus-ring"></md-focus-ring>
|
<md-focus-ring part="trailing-focus-ring"></md-focus-ring>
|
||||||
<md-ripple ?disabled=${disabled}></md-ripple>
|
<md-ripple ?disabled=${disabled}></md-ripple>
|
||||||
<svg class="trailing icon" viewBox="0 96 960 960" aria-hidden="true">
|
<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>
|
</svg>
|
||||||
<span class="touch"></span>
|
<span class="touch"></span>
|
||||||
</button>
|
</button>
|
||||||
@ -45,8 +49,9 @@ function handleRemoveClick(this: Chip, event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const preventDefault =
|
const preventDefault = !this.dispatchEvent(
|
||||||
!this.dispatchEvent(new Event('remove', {cancelable: true}));
|
new Event('remove', {cancelable: true}),
|
||||||
|
);
|
||||||
if (preventDefault) {
|
if (preventDefault) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,30 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {Knob, textInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Dialog', [
|
'Dialog',
|
||||||
|
[
|
||||||
new Knob('icon', {defaultValue: '', ui: textInput()}),
|
new Knob('icon', {defaultValue: '', ui: textInput()}),
|
||||||
new Knob('headline', {defaultValue: 'Dialog', ui: textInput()}),
|
new Knob('headline', {defaultValue: 'Dialog', ui: textInput()}),
|
||||||
new Knob(
|
new Knob('supportingText', {
|
||||||
'supportingText',
|
defaultValue: 'Just a simple dialog.',
|
||||||
{defaultValue: 'Just a simple dialog.', ui: textInput()}),
|
ui: textInput(),
|
||||||
]);
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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-button.js';
|
||||||
|
import '@material/web/button/filled-tonal-button.js';
|
||||||
import '@material/web/button/text-button.js';
|
import '@material/web/button/text-button.js';
|
||||||
import '@material/web/dialog/dialog.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 {MdDialog} from '@material/web/dialog/dialog.js';
|
||||||
import {MaterialStoryInit} from './material-collection.js';
|
import {MaterialStoryInit} from './material-collection.js';
|
||||||
@ -32,8 +32,9 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
name: 'Dialog',
|
name: 'Dialog',
|
||||||
render({icon, headline, supportingText}) {
|
render({icon, headline, supportingText}) {
|
||||||
return html`
|
return html`
|
||||||
<md-filled-button @click=${showDialog}
|
<md-filled-button @click=${showDialog} aria-label="Open a dialog"
|
||||||
aria-label="Open a dialog">Open</md-filled-button>
|
>Open</md-filled-button
|
||||||
|
>
|
||||||
|
|
||||||
<md-dialog aria-label=${headline ? nothing : 'A simple dialog'}>
|
<md-dialog aria-label=${headline ? nothing : 'A simple dialog'}>
|
||||||
${icon ? html`<md-icon slot="icon">${icon}</md-icon>` : nothing}
|
${icon ? html`<md-icon slot="icon">${icon}</md-icon>` : nothing}
|
||||||
@ -47,15 +48,16 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</div>
|
</div>
|
||||||
</md-dialog>
|
</md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const alert: MaterialStoryInit<StoryKnobs> = {
|
const alert: MaterialStoryInit<StoryKnobs> = {
|
||||||
name: 'Alert',
|
name: 'Alert',
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<md-filled-button @click=${showDialog}
|
<md-filled-button @click=${showDialog} aria-label="Open an alert dialog"
|
||||||
aria-label="Open an alert dialog">Alert</md-filled-button>
|
>Alert</md-filled-button
|
||||||
|
>
|
||||||
|
|
||||||
<md-dialog type="alert">
|
<md-dialog type="alert">
|
||||||
<div slot="headline">Alert dialog</div>
|
<div slot="headline">Alert dialog</div>
|
||||||
@ -68,15 +70,18 @@ const alert: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</div>
|
</div>
|
||||||
</md-dialog>
|
</md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirm: MaterialStoryInit<StoryKnobs> = {
|
const confirm: MaterialStoryInit<StoryKnobs> = {
|
||||||
name: 'Confirm',
|
name: 'Confirm',
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<md-filled-button @click=${showDialog}
|
<md-filled-button
|
||||||
aria-label="Open a confirmation dialog">Confirm</md-filled-button>
|
@click=${showDialog}
|
||||||
|
aria-label="Open a confirmation dialog"
|
||||||
|
>Confirm</md-filled-button
|
||||||
|
>
|
||||||
|
|
||||||
<md-dialog style="max-width: 320px;">
|
<md-dialog style="max-width: 320px;">
|
||||||
<div slot="headline">Permanently delete?</div>
|
<div slot="headline">Permanently delete?</div>
|
||||||
@ -87,12 +92,13 @@ const confirm: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</form>
|
</form>
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
<md-text-button form="form" value="delete">Delete</md-text-button>
|
<md-text-button form="form" value="delete">Delete</md-text-button>
|
||||||
<md-filled-tonal-button form="form" value="cancel"
|
<md-filled-tonal-button form="form" value="cancel" autofocus
|
||||||
autofocus>Cancel</md-filled-tonal-button>
|
>Cancel</md-filled-tonal-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</md-dialog>
|
</md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const choose: MaterialStoryInit<StoryKnobs> = {
|
const choose: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -105,22 +111,36 @@ const choose: MaterialStoryInit<StoryKnobs> = {
|
|||||||
`,
|
`,
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<md-filled-button @click=${showDialog}
|
<md-filled-button @click=${showDialog} aria-label="Open a choice dialog"
|
||||||
aria-label="Open a choice dialog">Choice</md-filled-button>
|
>Choice</md-filled-button
|
||||||
|
>
|
||||||
|
|
||||||
<md-dialog>
|
<md-dialog>
|
||||||
<div slot="headline">Choose your favorite pet</div>
|
<div slot="headline">Choose your favorite pet</div>
|
||||||
<form id="form" slot="content" method="dialog">
|
<form id="form" slot="content" method="dialog">
|
||||||
<label>
|
<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>
|
<span aria-hidden="true">Cats</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span aria-hidden="true">Dogs</span>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<span aria-hidden="true">Birds</span>
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
@ -130,7 +150,7 @@ const choose: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</div>
|
</div>
|
||||||
</md-dialog>
|
</md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const contacts: MaterialStoryInit<StoryKnobs> = {
|
const contacts: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -140,7 +160,7 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
|
|||||||
min-width: calc(100vw - 212px);
|
min-width: calc(100vw - 212px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.contacts [slot="header"] {
|
.contacts [slot='header'] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -150,7 +170,8 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-content, .contact-row {
|
.contact-content,
|
||||||
|
.contact-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
@ -165,8 +186,9 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
|
|||||||
`,
|
`,
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<md-filled-button @click=${showDialog}
|
<md-filled-button @click=${showDialog} aria-label="Open a form dialog"
|
||||||
aria-label="Open a form dialog">Form</md-filled-button>
|
>Form</md-filled-button
|
||||||
|
>
|
||||||
|
|
||||||
<md-dialog class="contacts">
|
<md-dialog class="contacts">
|
||||||
<span slot="headline">
|
<span slot="headline">
|
||||||
@ -177,7 +199,9 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</span>
|
</span>
|
||||||
<form id="form" slot="content" method="dialog" class="contact-content">
|
<form id="form" slot="content" method="dialog" class="contact-content">
|
||||||
<div class="contact-row">
|
<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>
|
<md-filled-text-field label="Last Name"></md-filled-text-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-row">
|
<div class="contact-row">
|
||||||
@ -188,24 +212,23 @@ const contacts: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-filled-text-field label="Phone"></md-filled-text-field>
|
<md-filled-text-field label="Phone"></md-filled-text-field>
|
||||||
</form>
|
</form>
|
||||||
<div slot="actions">
|
<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>
|
<div style="flex: 1"></div>
|
||||||
<md-text-button form="form" value="cancel">Cancel</md-text-button>
|
<md-text-button form="form" value="cancel">Cancel</md-text-button>
|
||||||
<md-text-button form="form" value="save">Save</md-text-button>
|
<md-text-button form="form" value="save">Save</md-text-button>
|
||||||
</div>
|
</div>
|
||||||
</md-dialog>
|
</md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const floatingSheet: MaterialStoryInit<StoryKnobs> = {
|
const floatingSheet: MaterialStoryInit<StoryKnobs> = {
|
||||||
name: 'Floating sheet',
|
name: 'Floating sheet',
|
||||||
render() {
|
render() {
|
||||||
return html`
|
return html`
|
||||||
<md-filled-button
|
<md-filled-button @click=${showDialog} aria-label="Open a floating sheet">
|
||||||
@click=${showDialog}
|
|
||||||
aria-label="Open a floating sheet"
|
|
||||||
>
|
|
||||||
Floating sheet
|
Floating sheet
|
||||||
</md-filled-button>
|
</md-filled-button>
|
||||||
|
|
||||||
@ -217,16 +240,21 @@ const floatingSheet: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
</span>
|
</span>
|
||||||
<form id="form" slot="content" method="dialog">
|
<form id="form" slot="content" method="dialog">
|
||||||
This is a floating sheet with title.
|
This is a floating sheet with title. Floating sheets offer no action
|
||||||
Floating sheets offer no action buttons at the bottom,
|
buttons at the bottom, but there's a close icon button at the top
|
||||||
but there's a close icon button at the top right.
|
right. They accept any HTML content.
|
||||||
They accept any HTML content.
|
|
||||||
</form>
|
</form>
|
||||||
</md-dialog>
|
</md-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Dialog stories. */
|
/** Dialog stories. */
|
||||||
export const stories =
|
export const stories = [
|
||||||
[standard, alert, confirm, choose, contacts, floatingSheet];
|
standard,
|
||||||
|
alert,
|
||||||
|
confirm,
|
||||||
|
choose,
|
||||||
|
contacts,
|
||||||
|
floatingSheet,
|
||||||
|
];
|
||||||
|
@ -20,7 +20,7 @@ describe('<md-dialog>', () => {
|
|||||||
<md-dialog>
|
<md-dialog>
|
||||||
<form id="form" method="dialog" slot="content">
|
<form id="form" method="dialog" slot="content">
|
||||||
Content
|
Content
|
||||||
<input autofocus>
|
<input autofocus />
|
||||||
</form>
|
</form>
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
<button form="form" value="button">Close</button>
|
<button form="form" value="button">Close</button>
|
||||||
@ -107,14 +107,19 @@ describe('<md-dialog>', () => {
|
|||||||
it('closes when element with action is clicked', async () => {
|
it('closes when element with action is clicked', async () => {
|
||||||
const {harness} = await setupTest();
|
const {harness} = await setupTest();
|
||||||
await harness.element.show();
|
await harness.element.show();
|
||||||
const closedPromise = new Promise<void>(resolve => {
|
const closedPromise = new Promise<void>((resolve) => {
|
||||||
harness.element.addEventListener('closed', () => {
|
harness.element.addEventListener(
|
||||||
|
'closed',
|
||||||
|
() => {
|
||||||
resolve();
|
resolve();
|
||||||
}, {once: true});
|
},
|
||||||
|
{once: true},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
harness.element.querySelector<HTMLButtonElement>(
|
harness.element
|
||||||
'[value="button"]')!.click();
|
.querySelector<HTMLButtonElement>('[value="button"]')!
|
||||||
|
.click();
|
||||||
await closedPromise;
|
await closedPromise;
|
||||||
expect(harness.element.open).toBeFalse();
|
expect(harness.element.open).toBeFalse();
|
||||||
expect(harness.element.returnValue).toBe('button');
|
expect(harness.element.returnValue).toBe('button');
|
||||||
@ -132,8 +137,7 @@ describe('<md-dialog>', () => {
|
|||||||
expect(isClosing).toHaveBeenCalled();
|
expect(isClosing).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('focuses element with autofocus when shown and previously focused element when closed',
|
it('focuses element with autofocus when shown and previously focused element when closed', async () => {
|
||||||
async () => {
|
|
||||||
const {harness, focusElement} = await setupTest();
|
const {harness, focusElement} = await setupTest();
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
document.body.append(button);
|
document.body.append(button);
|
||||||
@ -166,7 +170,7 @@ describe('<md-dialog>', () => {
|
|||||||
it('should not change returnValue if close event is canceled', async () => {
|
it('should not change returnValue if close event is canceled', async () => {
|
||||||
const {harness} = await setupTest();
|
const {harness} = await setupTest();
|
||||||
|
|
||||||
harness.element.addEventListener('close', event => {
|
harness.element.addEventListener('close', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -196,8 +200,7 @@ describe('<md-dialog>', () => {
|
|||||||
.toHaveBeenCalled();
|
.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not open on connected if opened, but closed before connected to DOM',
|
it('should not open on connected if opened, but closed before connected to DOM', async () => {
|
||||||
async () => {
|
|
||||||
const openListener = jasmine.createSpy('openListener');
|
const openListener = jasmine.createSpy('openListener');
|
||||||
const dialog = document.createElement('md-dialog');
|
const dialog = document.createElement('md-dialog');
|
||||||
disableDialogAnimations(dialog);
|
disableDialogAnimations(dialog);
|
||||||
@ -213,8 +216,7 @@ describe('<md-dialog>', () => {
|
|||||||
.not.toHaveBeenCalled();
|
.not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not open on connected if opened before connection but closed after',
|
it('should not open on connected if opened before connection but closed after', async () => {
|
||||||
async () => {
|
|
||||||
const openListener = jasmine.createSpy('openListener');
|
const openListener = jasmine.createSpy('openListener');
|
||||||
const dialog = document.createElement('md-dialog');
|
const dialog = document.createElement('md-dialog');
|
||||||
disableDialogAnimations(dialog);
|
disableDialogAnimations(dialog);
|
||||||
@ -226,7 +228,8 @@ describe('<md-dialog>', () => {
|
|||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(openListener)
|
expect(openListener)
|
||||||
.withContext(
|
.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();
|
.not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -14,7 +14,8 @@ import {Dialog} from './internal/dialog.js';
|
|||||||
export class DialogHarness extends Harness<Dialog> {
|
export class DialogHarness extends Harness<Dialog> {
|
||||||
override async getInteractiveElement() {
|
override async getInteractiveElement() {
|
||||||
await this.element.updateComplete;
|
await this.element.updateComplete;
|
||||||
return this.element.querySelector<HTMLElement>('[autocomplete]') ??
|
return (
|
||||||
this.element;
|
this.element.querySelector<HTMLElement>('[autocomplete]') ?? this.element
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,20 +54,21 @@ export const DIALOG_DEFAULT_OPEN_ANIMATION: DialogAnimation = {
|
|||||||
[
|
[
|
||||||
// Dialog slide down
|
// Dialog slide down
|
||||||
[{'transform': 'translateY(-50px)'}, {'transform': 'translateY(0)'}],
|
[{'transform': 'translateY(-50px)'}, {'transform': 'translateY(0)'}],
|
||||||
{duration: 500, easing: EASING.EMPHASIZED}
|
{duration: 500, easing: EASING.EMPHASIZED},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
scrim: [
|
scrim: [
|
||||||
[
|
[
|
||||||
// Scrim fade in
|
// Scrim fade in
|
||||||
[{'opacity': 0}, {'opacity': 0.32}], {duration: 500, easing: 'linear'}
|
[{'opacity': 0}, {'opacity': 0.32}],
|
||||||
|
{duration: 500, easing: 'linear'},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
container: [
|
container: [
|
||||||
[
|
[
|
||||||
// Container fade in
|
// Container fade in
|
||||||
[{'opacity': 0}, {'opacity': 1}],
|
[{'opacity': 0}, {'opacity': 1}],
|
||||||
{duration: 50, easing: 'linear', pseudoElement: '::before'}
|
{duration: 50, easing: 'linear', pseudoElement: '::before'},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
// Container grow
|
// Container grow
|
||||||
@ -83,21 +84,21 @@ export const DIALOG_DEFAULT_OPEN_ANIMATION: DialogAnimation = {
|
|||||||
[
|
[
|
||||||
// Headline fade in
|
// Headline fade in
|
||||||
[{'opacity': 0}, {'opacity': 0, offset: 0.2}, {'opacity': 1}],
|
[{'opacity': 0}, {'opacity': 0, offset: 0.2}, {'opacity': 1}],
|
||||||
{duration: 250, easing: 'linear', fill: 'forwards'}
|
{duration: 250, easing: 'linear', fill: 'forwards'},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
content: [
|
content: [
|
||||||
[
|
[
|
||||||
// Content fade in
|
// Content fade in
|
||||||
[{'opacity': 0}, {'opacity': 0, offset: 0.2}, {'opacity': 1}],
|
[{'opacity': 0}, {'opacity': 0, offset: 0.2}, {'opacity': 1}],
|
||||||
{duration: 250, easing: 'linear', fill: 'forwards'}
|
{duration: 250, easing: 'linear', fill: 'forwards'},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
[
|
[
|
||||||
// Actions fade in
|
// Actions fade in
|
||||||
[{'opacity': 0}, {'opacity': 0, offset: 0.5}, {'opacity': 1}],
|
[{'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
|
// Dialog slide up
|
||||||
[{'transform': 'translateY(0)'}, {'transform': 'translateY(-50px)'}],
|
[{'transform': 'translateY(0)'}, {'transform': 'translateY(-50px)'}],
|
||||||
{duration: 150, easing: EASING.EMPHASIZED_ACCELERATE}
|
{duration: 150, easing: EASING.EMPHASIZED_ACCELERATE},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
scrim: [
|
scrim: [
|
||||||
[
|
[
|
||||||
// Scrim fade out
|
// Scrim fade out
|
||||||
[{'opacity': 0.32}, {'opacity': 0}], {duration: 150, easing: 'linear'}
|
[{'opacity': 0.32}, {'opacity': 0}],
|
||||||
|
{duration: 150, easing: 'linear'},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
container: [
|
container: [
|
||||||
@ -133,27 +135,27 @@ export const DIALOG_DEFAULT_CLOSE_ANIMATION: DialogAnimation = {
|
|||||||
// Container fade out
|
// Container fade out
|
||||||
[{'opacity': '1'}, {'opacity': '0'}],
|
[{'opacity': '1'}, {'opacity': '0'}],
|
||||||
{delay: 100, duration: 50, easing: 'linear', pseudoElement: '::before'},
|
{delay: 100, duration: 50, easing: 'linear', pseudoElement: '::before'},
|
||||||
]
|
],
|
||||||
],
|
],
|
||||||
headline: [
|
headline: [
|
||||||
[
|
[
|
||||||
// Headline fade out
|
// Headline fade out
|
||||||
[{'opacity': 1}, {'opacity': 0}],
|
[{'opacity': 1}, {'opacity': 0}],
|
||||||
{duration: 100, easing: 'linear', fill: 'forwards'}
|
{duration: 100, easing: 'linear', fill: 'forwards'},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
content: [
|
content: [
|
||||||
[
|
[
|
||||||
// Content fade out
|
// Content fade out
|
||||||
[{'opacity': 1}, {'opacity': 0}],
|
[{'opacity': 1}, {'opacity': 0}],
|
||||||
{duration: 100, easing: 'linear', fill: 'forwards'}
|
{duration: 100, easing: 'linear', fill: 'forwards'},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
[
|
[
|
||||||
// Actions fade out
|
// Actions fade out
|
||||||
[{'opacity': 1}, {'opacity': 0}],
|
[{'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 {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
||||||
import {redispatchEvent} from '../../internal/controller/events.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.
|
* A dialog component.
|
||||||
@ -34,7 +39,7 @@ export class Dialog extends LitElement {
|
|||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
static override shadowRootOptions = {
|
static override shadowRootOptions = {
|
||||||
...LitElement.shadowRootOptions,
|
...LitElement.shadowRootOptions,
|
||||||
delegatesFocus: true
|
delegatesFocus: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -139,8 +144,9 @@ export class Dialog extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preventOpen =
|
const preventOpen = !this.dispatchEvent(
|
||||||
!this.dispatchEvent(new Event('open', {cancelable: true}));
|
new Event('open', {cancelable: true}),
|
||||||
|
);
|
||||||
if (preventOpen) {
|
if (preventOpen) {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
return;
|
return;
|
||||||
@ -191,8 +197,9 @@ export class Dialog extends LitElement {
|
|||||||
|
|
||||||
const prevReturnValue = this.returnValue;
|
const prevReturnValue = this.returnValue;
|
||||||
this.returnValue = returnValue;
|
this.returnValue = returnValue;
|
||||||
const preventClose =
|
const preventClose = !this.dispatchEvent(
|
||||||
!this.dispatchEvent(new Event('close', {cancelable: true}));
|
new Event('close', {cancelable: true}),
|
||||||
|
);
|
||||||
if (preventClose) {
|
if (preventClose) {
|
||||||
this.returnValue = prevReturnValue;
|
this.returnValue = prevReturnValue;
|
||||||
return;
|
return;
|
||||||
@ -236,17 +243,15 @@ export class Dialog extends LitElement {
|
|||||||
role=${this.type === 'alert' ? 'alertdialog' : nothing}
|
role=${this.type === 'alert' ? 'alertdialog' : nothing}
|
||||||
@cancel=${this.handleCancel}
|
@cancel=${this.handleCancel}
|
||||||
@click=${this.handleDialogClick}
|
@click=${this.handleDialogClick}
|
||||||
.returnValue=${this.returnValue || nothing}
|
.returnValue=${this.returnValue || nothing}>
|
||||||
>
|
<div class="container" @click=${this.handleContentClick}>
|
||||||
<div class="container"
|
|
||||||
@click=${this.handleContentClick}
|
|
||||||
>
|
|
||||||
<div class="headline">
|
<div class="headline">
|
||||||
<div class="icon" aria-hidden="true">
|
<div class="icon" aria-hidden="true">
|
||||||
<slot name="icon" @slotchange=${this.handleIconChange}></slot>
|
<slot name="icon" @slotchange=${this.handleIconChange}></slot>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="headline" aria-hidden=${!this.hasHeadline || nothing}>
|
<h2 id="headline" aria-hidden=${!this.hasHeadline || nothing}>
|
||||||
<slot name="headline"
|
<slot
|
||||||
|
name="headline"
|
||||||
@slotchange=${this.handleHeadlineChange}></slot>
|
@slotchange=${this.handleHeadlineChange}></slot>
|
||||||
</h2>
|
</h2>
|
||||||
<md-divider></md-divider>
|
<md-divider></md-divider>
|
||||||
@ -260,8 +265,7 @@ export class Dialog extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<md-divider></md-divider>
|
<md-divider></md-divider>
|
||||||
<slot name="actions"
|
<slot name="actions" @slotchange=${this.handleActionsChange}></slot>
|
||||||
@slotchange=${this.handleActionsChange}></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
@ -269,11 +273,14 @@ export class Dialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override firstUpdated() {
|
protected override firstUpdated() {
|
||||||
this.intersectionObserver = new IntersectionObserver(entries => {
|
this.intersectionObserver = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
this.handleAnchorIntersection(entry);
|
this.handleAnchorIntersection(entry);
|
||||||
}
|
}
|
||||||
}, {root: this.scroller!});
|
},
|
||||||
|
{root: this.scroller!},
|
||||||
|
);
|
||||||
|
|
||||||
this.intersectionObserver.observe(this.topAnchor!);
|
this.intersectionObserver.observe(this.topAnchor!);
|
||||||
this.intersectionObserver.observe(this.bottomAnchor!);
|
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,
|
// Click originated on the backdrop. Native `<dialog>`s will not cancel,
|
||||||
// but Material dialogs do.
|
// but Material dialogs do.
|
||||||
const preventDefault =
|
const preventDefault = !this.dispatchEvent(
|
||||||
!this.dispatchEvent(new Event('cancel', {cancelable: true}));
|
new Event('cancel', {cancelable: true}),
|
||||||
|
);
|
||||||
if (preventDefault) {
|
if (preventDefault) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -343,13 +351,16 @@ export class Dialog extends LitElement {
|
|||||||
scrim: scrimAnimate,
|
scrim: scrimAnimate,
|
||||||
headline: headlineAnimate,
|
headline: headlineAnimate,
|
||||||
content: contentAnimate,
|
content: contentAnimate,
|
||||||
actions: actionsAnimate
|
actions: actionsAnimate,
|
||||||
} = animation;
|
} = animation;
|
||||||
|
|
||||||
const elementAndAnimation: Array<[Element, DialogAnimationArgs[]]> = [
|
const elementAndAnimation: Array<[Element, DialogAnimationArgs[]]> = [
|
||||||
[dialog, dialogAnimate ?? []], [scrim, scrimAnimate ?? []],
|
[dialog, dialogAnimate ?? []],
|
||||||
[container, containerAnimate ?? []], [headline, headlineAnimate ?? []],
|
[scrim, scrimAnimate ?? []],
|
||||||
[content, contentAnimate ?? []], [actions, actionsAnimate ?? []]
|
[container, containerAnimate ?? []],
|
||||||
|
[headline, headlineAnimate ?? []],
|
||||||
|
[content, contentAnimate ?? []],
|
||||||
|
[actions, actionsAnimate ?? []],
|
||||||
];
|
];
|
||||||
|
|
||||||
const animations: Animation[] = [];
|
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) {
|
private handleHeadlineChange(event: Event) {
|
||||||
@ -389,7 +400,7 @@ export class Dialog extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getIsConnectedPromise() {
|
private getIsConnectedPromise() {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>((resolve) => {
|
||||||
this.isConnectedPromiseResolve = resolve;
|
this.isConnectedPromiseResolve = resolve;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,20 +4,27 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Divider', [
|
'Divider',
|
||||||
|
[
|
||||||
new Knob('inset', {defaultValue: true, ui: boolInput()}),
|
new Knob('inset', {defaultValue: true, ui: boolInput()}),
|
||||||
new Knob('inset (start)', {defaultValue: false, ui: boolInput()}),
|
new Knob('inset (start)', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('inset (end)', {defaultValue: false, ui: boolInput()}),
|
new Knob('inset (end)', {defaultValue: false, ui: boolInput()}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -35,25 +35,24 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
`,
|
`,
|
||||||
render(knobs) {
|
render(knobs) {
|
||||||
return html`
|
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>
|
<li>List item one</li>
|
||||||
<md-divider
|
<md-divider
|
||||||
?inset=${knobs.inset}
|
?inset=${knobs.inset}
|
||||||
?inset-start=${knobs['inset (start)']}
|
?inset-start=${knobs['inset (start)']}
|
||||||
?inset-end=${knobs['inset (end)']}
|
?inset-end=${knobs['inset (end)']}></md-divider>
|
||||||
></md-divider>
|
|
||||||
<li>List item two</li>
|
<li>List item two</li>
|
||||||
<md-divider role="separator"></md-divider>
|
<md-divider role="separator"></md-divider>
|
||||||
<li>List item three</li>
|
<li>List item three</li>
|
||||||
<md-divider
|
<md-divider
|
||||||
?inset=${knobs.inset}
|
?inset=${knobs.inset}
|
||||||
?inset-start=${knobs['inset (start)']}
|
?inset-start=${knobs['inset (start)']}
|
||||||
?inset-end=${knobs['inset (end)']}
|
?inset-end=${knobs['inset (end)']}></md-divider>
|
||||||
></md-divider>
|
|
||||||
<li>List item four</li>
|
<li>List item four</li>
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Divider stories. */
|
/** Divider stories. */
|
||||||
|
@ -4,18 +4,23 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {Knob, numberInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Elevation', [
|
'Elevation',
|
||||||
new Knob('level', {defaultValue: 1, ui: numberInput()}),
|
[new Knob('level', {defaultValue: 1, ui: numberInput()})],
|
||||||
]);
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -4,12 +4,22 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.js';
|
import './material-collection.js';
|
||||||
|
import './index.js';
|
||||||
|
|
||||||
import {FabSize} from '@material/web/fab/fab.js';
|
import {FabSize} from '@material/web/fab/fab.js';
|
||||||
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js';
|
import {
|
||||||
import {boolInput, Knob, selectDropdown, textInput} from './index.js';
|
KnobTypesToKnobs,
|
||||||
|
MaterialCollection,
|
||||||
|
materialInitsToStoryInits,
|
||||||
|
setUpDemo,
|
||||||
|
} from './material-collection.js';
|
||||||
|
import {
|
||||||
|
boolInput,
|
||||||
|
Knob,
|
||||||
|
selectDropdown,
|
||||||
|
textInput,
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
@ -24,8 +34,8 @@ const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('FAB', [
|
|||||||
{label: 'medium', value: 'medium'},
|
{label: 'medium', value: 'medium'},
|
||||||
{label: 'small', value: 'small'},
|
{label: 'small', value: 'small'},
|
||||||
{label: 'large', value: 'large'},
|
{label: 'large', value: 'large'},
|
||||||
]
|
],
|
||||||
})
|
}),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -4,12 +4,15 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import '@material/web/fab/branded-fab.js';
|
||||||
import '@material/web/fab/fab.js';
|
import '@material/web/fab/fab.js';
|
||||||
import '@material/web/icon/icon.js';
|
import '@material/web/icon/icon.js';
|
||||||
import '@material/web/fab/branded-fab.js';
|
|
||||||
|
|
||||||
import {FabSize} from '@material/web/fab/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';
|
import {css, html, nothing} from 'lit';
|
||||||
|
|
||||||
/** Knob types for fab stories. */
|
/** Knob types for fab stories. */
|
||||||
@ -46,8 +49,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
variant="surface"
|
variant="surface"
|
||||||
.lowered=${lowered}
|
.lowered=${lowered}
|
||||||
.label=${label}
|
.label=${label}
|
||||||
.size=${size!}
|
.size=${size!}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">${icon}</md-icon>
|
<md-icon slot="icon">${icon}</md-icon>
|
||||||
</md-fab>
|
</md-fab>
|
||||||
</label>
|
</label>
|
||||||
@ -59,8 +61,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
.lowered=${lowered}
|
.lowered=${lowered}
|
||||||
.label=${label}
|
.label=${label}
|
||||||
.size=${size!}
|
.size=${size!}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">${icon}</md-icon>
|
<md-icon slot="icon">${icon}</md-icon>
|
||||||
</md-fab>
|
</md-fab>
|
||||||
</label>
|
</label>
|
||||||
@ -72,8 +73,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
.lowered=${lowered}
|
.lowered=${lowered}
|
||||||
.label=${label}
|
.label=${label}
|
||||||
.size=${size!}
|
.size=${size!}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">${icon}</md-icon>
|
<md-icon slot="icon">${icon}</md-icon>
|
||||||
</md-fab>
|
</md-fab>
|
||||||
</label>
|
</label>
|
||||||
@ -85,8 +85,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
.lowered=${lowered}
|
.lowered=${lowered}
|
||||||
.label=${label}
|
.label=${label}
|
||||||
.size=${size!}
|
.size=${size!}>
|
||||||
>
|
|
||||||
<md-icon slot="icon">${icon}</md-icon>
|
<md-icon slot="icon">${icon}</md-icon>
|
||||||
</md-fab>
|
</md-fab>
|
||||||
</label>
|
</label>
|
||||||
@ -97,8 +96,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
aria-label=${label ? nothing : 'An example branded FAB'}
|
aria-label=${label ? nothing : 'An example branded FAB'}
|
||||||
.lowered=${lowered}
|
.lowered=${lowered}
|
||||||
.label=${label}
|
.label=${label}
|
||||||
.size=${size!}
|
.size=${size!}>
|
||||||
>
|
|
||||||
<svg slot="icon" width="36" height="36" viewBox="0 0 36 36">
|
<svg slot="icon" width="36" height="36" viewBox="0 0 36 36">
|
||||||
<path fill="#34A853" d="M16 16v14h4V20z"></path>
|
<path fill="#34A853" d="M16 16v14h4V20z"></path>
|
||||||
<path fill="#4285F4" d="M30 16H20l-4 4h14z"></path>
|
<path fill="#4285F4" d="M30 16H20l-4 4h14z"></path>
|
||||||
@ -110,7 +108,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Checkbox stories. */
|
/** Checkbox stories. */
|
||||||
|
@ -156,7 +156,8 @@ describe('<md-branded-fab>', () => {
|
|||||||
const env = new Environment();
|
const env = new Environment();
|
||||||
|
|
||||||
async function setupTest() {
|
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');
|
.querySelector('md-branded-fab');
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error('Could not query rendered <md-branded-fab>.');
|
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> {
|
export class FabHarness extends Harness<Fab> {
|
||||||
override async getInteractiveElement() {
|
override async getInteractiveElement() {
|
||||||
await this.element.updateComplete;
|
await this.element.updateComplete;
|
||||||
return this.element.renderRoot.querySelector('.fab') as
|
return this.element.renderRoot.querySelector('.fab') as HTMLButtonElement;
|
||||||
HTMLButtonElement;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,6 @@ export abstract class SharedFab extends LitElement {
|
|||||||
*/
|
*/
|
||||||
@property() label = '';
|
@property() label = '';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lowers the FAB's elevation.
|
* Lowers the FAB's elevation.
|
||||||
*/
|
*/
|
||||||
@ -57,14 +56,11 @@ export abstract class SharedFab extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
class="fab ${classMap(this.getRenderClasses())}"
|
class="fab ${classMap(this.getRenderClasses())}"
|
||||||
aria-label=${ariaLabel || nothing}
|
aria-label=${ariaLabel || nothing}>
|
||||||
>
|
|
||||||
<md-elevation></md-elevation>
|
<md-elevation></md-elevation>
|
||||||
<md-focus-ring part="focus-ring"></md-focus-ring>
|
<md-focus-ring part="focus-ring"></md-focus-ring>
|
||||||
<md-ripple class="ripple"></md-ripple>
|
<md-ripple class="ripple"></md-ripple>
|
||||||
${this.renderTouchTarget()}
|
${this.renderTouchTarget()} ${this.renderIcon()} ${this.renderLabel()}
|
||||||
${this.renderIcon()}
|
|
||||||
${this.renderLabel()}
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -92,8 +88,9 @@ export abstract class SharedFab extends LitElement {
|
|||||||
return html`<span class="icon">
|
return html`<span class="icon">
|
||||||
<slot
|
<slot
|
||||||
name="icon"
|
name="icon"
|
||||||
aria-hidden=${
|
aria-hidden=${ariaLabel || this.label
|
||||||
ariaLabel || this.label ? 'true' : nothing as unknown as 'false'}>
|
? 'true'
|
||||||
|
: (nothing as unknown as 'false')}>
|
||||||
<span></span>
|
<span></span>
|
||||||
</slot>
|
</slot>
|
||||||
</span>`;
|
</span>`;
|
||||||
|
@ -4,20 +4,32 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.js';
|
import './material-collection.js';
|
||||||
|
import './index.js';
|
||||||
|
|
||||||
import {KnobTypesToKnobs, MaterialCollection, materialInitsToStoryInits, setUpDemo} from './material-collection.js';
|
import {
|
||||||
import {boolInput, Knob, numberInput, textInput} from './index.js';
|
KnobTypesToKnobs,
|
||||||
|
MaterialCollection,
|
||||||
|
materialInitsToStoryInits,
|
||||||
|
setUpDemo,
|
||||||
|
} from './material-collection.js';
|
||||||
|
import {
|
||||||
|
boolInput,
|
||||||
|
Knob,
|
||||||
|
numberInput,
|
||||||
|
textInput,
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Field', [
|
'Field',
|
||||||
|
[
|
||||||
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
|
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
|
||||||
new Knob(
|
new Knob('Supporting text', {
|
||||||
'Supporting text',
|
ui: textInput(),
|
||||||
{ui: textInput(), defaultValue: 'Supporting text'}),
|
defaultValue: 'Supporting text',
|
||||||
|
}),
|
||||||
new Knob('Error text', {ui: textInput(), defaultValue: 'Error text'}),
|
new Knob('Error text', {ui: textInput(), defaultValue: 'Error text'}),
|
||||||
new Knob('count', {ui: numberInput(), defaultValue: 0}),
|
new Knob('count', {ui: numberInput(), defaultValue: 0}),
|
||||||
new Knob('max', {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('Leading icon', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('Trailing icon', {ui: boolInput(), defaultValue: false}),
|
new Knob('Trailing icon', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('resizable', {ui: boolInput(), defaultValue: false}),
|
new Knob('resizable', {ui: boolInput(), defaultValue: false}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@material/web/icon/icon.js';
|
|
||||||
import '@material/web/field/filled-field.js';
|
import '@material/web/field/filled-field.js';
|
||||||
import '@material/web/field/outlined-field.js';
|
import '@material/web/field/outlined-field.js';
|
||||||
|
import '@material/web/icon/icon.js';
|
||||||
|
|
||||||
import {MaterialStoryInit} from './material-collection.js';
|
import {MaterialStoryInit} from './material-collection.js';
|
||||||
import {css, html, nothing} from 'lit';
|
import {css, html, nothing} from 'lit';
|
||||||
@ -52,16 +52,16 @@ const filled: MaterialStoryInit<StoryKnobs> = {
|
|||||||
required,
|
required,
|
||||||
resizable,
|
resizable,
|
||||||
count,
|
count,
|
||||||
max
|
max,
|
||||||
} = knobs;
|
} = knobs;
|
||||||
const supportingText = knobs['Supporting text'];
|
const supportingText = knobs['Supporting text'];
|
||||||
const errorText = knobs['Error text'];
|
const errorText = knobs['Error text'];
|
||||||
const hasStart = knobs['Leading icon'];
|
const hasStart = knobs['Leading icon'];
|
||||||
const hasEnd = knobs['Trailing icon'];
|
const hasEnd = knobs['Trailing icon'];
|
||||||
|
|
||||||
const content = resizable ?
|
const content = resizable
|
||||||
html`<textarea rows="1" ?disabled=${disabled}></textarea>` :
|
? html`<textarea rows="1" ?disabled=${disabled}></textarea>`
|
||||||
html`<input ?disabled=${disabled}>`;
|
: html`<input ?disabled=${disabled} />`;
|
||||||
|
|
||||||
const styles = {resize: resizable ? 'both' : null};
|
const styles = {resize: resizable ? 'both' : null};
|
||||||
return html`
|
return html`
|
||||||
@ -78,14 +78,12 @@ const filled: MaterialStoryInit<StoryKnobs> = {
|
|||||||
supporting-text=${supportingText}
|
supporting-text=${supportingText}
|
||||||
error-text=${errorText}
|
error-text=${errorText}
|
||||||
count=${count}
|
count=${count}
|
||||||
max=${max}
|
max=${max}>
|
||||||
>
|
${hasStart ? START_CONTENT : nothing} ${content}
|
||||||
${hasStart ? START_CONTENT : nothing}
|
|
||||||
${content}
|
|
||||||
${hasEnd ? END_CONTENT : nothing}
|
${hasEnd ? END_CONTENT : nothing}
|
||||||
</md-filled-field>
|
</md-filled-field>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const outlined: MaterialStoryInit<StoryKnobs> = {
|
const outlined: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -101,16 +99,18 @@ const outlined: MaterialStoryInit<StoryKnobs> = {
|
|||||||
required,
|
required,
|
||||||
resizable,
|
resizable,
|
||||||
count,
|
count,
|
||||||
max
|
max,
|
||||||
} = knobs;
|
} = knobs;
|
||||||
const supportingText = knobs['Supporting text'];
|
const supportingText = knobs['Supporting text'];
|
||||||
const errorText = knobs['Error text'];
|
const errorText = knobs['Error text'];
|
||||||
const hasStart = knobs['Leading icon'];
|
const hasStart = knobs['Leading icon'];
|
||||||
const hasEnd = knobs['Trailing icon'];
|
const hasEnd = knobs['Trailing icon'];
|
||||||
const content = resizable ?
|
const content = resizable
|
||||||
html`<textarea rows="1" ?disabled=${
|
? html`<textarea
|
||||||
disabled} aria-describedby="description"></textarea>` :
|
rows="1"
|
||||||
html`<input ?disabled=${disabled} aria-describedby="description">`;
|
?disabled=${disabled}
|
||||||
|
aria-describedby="description"></textarea>`
|
||||||
|
: html`<input ?disabled=${disabled} aria-describedby="description" />`;
|
||||||
|
|
||||||
const styles = {resize: resizable ? 'both' : null};
|
const styles = {resize: resizable ? 'both' : null};
|
||||||
return html`
|
return html`
|
||||||
@ -127,15 +127,13 @@ const outlined: MaterialStoryInit<StoryKnobs> = {
|
|||||||
supporting-text=${supportingText}
|
supporting-text=${supportingText}
|
||||||
error-text=${errorText}
|
error-text=${errorText}
|
||||||
count=${count}
|
count=${count}
|
||||||
max=${max}
|
max=${max}>
|
||||||
>
|
|
||||||
<div id="description" slot="aria-describedby" hidden></div>
|
<div id="description" slot="aria-describedby" hidden></div>
|
||||||
${hasStart ? START_CONTENT : nothing}
|
${hasStart ? START_CONTENT : nothing} ${content}
|
||||||
${content}
|
|
||||||
${hasEnd ? END_CONTENT : nothing}
|
${hasEnd ? END_CONTENT : nothing}
|
||||||
</md-outlined-field>
|
</md-outlined-field>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Field stories. */
|
/** Field stories. */
|
||||||
|
@ -4,7 +4,14 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 {property, query, queryAssignedElements, state} from 'lit/decorators.js';
|
||||||
import {classMap} from 'lit/directives/class-map.js';
|
import {classMap} from 'lit/directives/class-map.js';
|
||||||
|
|
||||||
@ -60,7 +67,8 @@ export class Field extends LitElement {
|
|||||||
*/
|
*/
|
||||||
@state() private refreshErrorAlert = false;
|
@state() private refreshErrorAlert = false;
|
||||||
@state() private disableTransitions = 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('.label.resting') private readonly restingLabelEl!: HTMLElement | null;
|
||||||
@query('.container') private readonly containerEl!: HTMLElement | null;
|
@query('.container') private readonly containerEl!: HTMLElement | null;
|
||||||
|
|
||||||
@ -92,7 +100,7 @@ export class Field extends LitElement {
|
|||||||
// Animate if focused or populated change.
|
// Animate if focused or populated change.
|
||||||
this.animateLabelIfNeeded({
|
this.animateLabelIfNeeded({
|
||||||
wasFocused: props.get('focused'),
|
wasFocused: props.get('focused'),
|
||||||
wasPopulated: props.get('populated')
|
wasPopulated: props.get('populated'),
|
||||||
});
|
});
|
||||||
|
|
||||||
super.update(props);
|
super.update(props);
|
||||||
@ -118,17 +126,14 @@ export class Field extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
<div class="field ${classMap(classes)}">
|
<div class="field ${classMap(classes)}">
|
||||||
<div class="container-overflow">
|
<div class="container-overflow">
|
||||||
${this.renderBackground?.()}
|
${this.renderBackground?.()} ${this.renderIndicator?.()} ${outline}
|
||||||
${this.renderIndicator?.()}
|
|
||||||
${outline}
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="start">
|
<div class="start">
|
||||||
<slot name="start"></slot>
|
<slot name="start"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="middle">
|
<div class="middle">
|
||||||
<div class="label-wrapper">
|
<div class="label-wrapper">
|
||||||
${restingLabel}
|
${restingLabel} ${outline ? nothing : floatingLabel}
|
||||||
${outline ? nothing : floatingLabel}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@ -145,8 +150,12 @@ export class Field extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected override updated(changed: PropertyValues<Field>) {
|
protected override updated(changed: PropertyValues<Field>) {
|
||||||
if (changed.has('supportingText') || changed.has('errorText') ||
|
if (
|
||||||
changed.has('count') || changed.has('max')) {
|
changed.has('supportingText') ||
|
||||||
|
changed.has('errorText') ||
|
||||||
|
changed.has('count') ||
|
||||||
|
changed.has('max')
|
||||||
|
) {
|
||||||
this.updateSlottedAriaDescribedBy();
|
this.updateSlottedAriaDescribedBy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,9 +189,9 @@ export class Field extends LitElement {
|
|||||||
const start = html`<span>${supportingOrErrorText}</span>`;
|
const start = html`<span>${supportingOrErrorText}</span>`;
|
||||||
// Conditionally render counter so we don't render the extra `gap`.
|
// Conditionally render counter so we don't render the extra `gap`.
|
||||||
// TODO(b/244473435): add aria-label and announcements
|
// TODO(b/244473435): add aria-label and announcements
|
||||||
const end = counterText ?
|
const end = counterText
|
||||||
html`<span class="counter">${counterText}</span>` :
|
? html`<span class="counter">${counterText}</span>`
|
||||||
nothing;
|
: nothing;
|
||||||
|
|
||||||
// Announce if there is an error and error text visible.
|
// Announce if there is an error and error text visible.
|
||||||
// If refreshErrorAlert is true, do not announce. This will remove the
|
// 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;
|
const role = shouldErrorAnnounce ? 'alert' : nothing;
|
||||||
return html`
|
return html`
|
||||||
<div class="supporting-text" role=${role}>${start}${end}</div>
|
<div class="supporting-text" role=${role}>${start}${end}</div>
|
||||||
<slot name="aria-describedby" @slotchange=${
|
<slot
|
||||||
this.updateSlottedAriaDescribedBy}></slot>
|
name="aria-describedby"
|
||||||
|
@slotchange=${this.updateSlottedAriaDescribedBy}></slot>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,15 +240,18 @@ export class Field extends LitElement {
|
|||||||
const labelText = `${this.label}${this.required ? '*' : ''}`;
|
const labelText = `${this.label}${this.required ? '*' : ''}`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<span class="label ${classMap(classes)}"
|
<span class="label ${classMap(classes)}" aria-hidden=${!visible}
|
||||||
aria-hidden=${!visible}
|
>${labelText}</span
|
||||||
>${labelText}</span>
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private animateLabelIfNeeded({wasFocused, wasPopulated}: {
|
private animateLabelIfNeeded({
|
||||||
wasFocused?: boolean,
|
wasFocused,
|
||||||
wasPopulated?: boolean
|
wasPopulated,
|
||||||
|
}: {
|
||||||
|
wasFocused?: boolean;
|
||||||
|
wasPopulated?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (!this.label) {
|
if (!this.label) {
|
||||||
return;
|
return;
|
||||||
@ -268,7 +281,9 @@ export class Field extends LitElement {
|
|||||||
// from appearing.
|
// from appearing.
|
||||||
// TODO(b/241113345): use animation tokens
|
// TODO(b/241113345): use animation tokens
|
||||||
this.labelAnimation = this.floatingLabelEl?.animate(
|
this.labelAnimation = this.floatingLabelEl?.animate(
|
||||||
this.getLabelKeyframes(), {duration: 150, easing: EASING.STANDARD});
|
this.getLabelKeyframes(),
|
||||||
|
{duration: 150, easing: EASING.STANDARD},
|
||||||
|
);
|
||||||
|
|
||||||
this.labelAnimation?.addEventListener('finish', () => {
|
this.labelAnimation?.addEventListener('finish', () => {
|
||||||
// At the end of the animation, update the visible label.
|
// At the end of the animation, update the visible label.
|
||||||
@ -282,10 +297,16 @@ export class Field extends LitElement {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const {x: floatingX, y: floatingY, height: floatingHeight} =
|
const {
|
||||||
floatingLabelEl.getBoundingClientRect();
|
x: floatingX,
|
||||||
const {x: restingX, y: restingY, height: restingHeight} =
|
y: floatingY,
|
||||||
restingLabelEl.getBoundingClientRect();
|
height: floatingHeight,
|
||||||
|
} = floatingLabelEl.getBoundingClientRect();
|
||||||
|
const {
|
||||||
|
x: restingX,
|
||||||
|
y: restingY,
|
||||||
|
height: restingHeight,
|
||||||
|
} = restingLabelEl.getBoundingClientRect();
|
||||||
const floatingScrollWidth = floatingLabelEl.scrollWidth;
|
const floatingScrollWidth = floatingLabelEl.scrollWidth;
|
||||||
const restingScrollWidth = restingLabelEl.scrollWidth;
|
const restingScrollWidth = restingLabelEl.scrollWidth;
|
||||||
// Scale by width ratio instead of font size since letter-spacing will scale
|
// 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
|
// 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
|
// exactly match because of this. We need to adjust by half of what the
|
||||||
// final scaled floating label's height will be.
|
// final scaled floating label's height will be.
|
||||||
const yDelta = restingY - floatingY +
|
const yDelta =
|
||||||
|
restingY -
|
||||||
|
floatingY +
|
||||||
Math.round((restingHeight - floatingHeight * scale) / 2);
|
Math.round((restingHeight - floatingHeight * scale) / 2);
|
||||||
|
|
||||||
// Create the two transforms: floating to resting (using the calculations
|
// Create the two transforms: floating to resting (using the calculations
|
||||||
// above), and resting to floating (re-setting the transform to initial
|
// above), and resting to floating (re-setting the transform to initial
|
||||||
// values).
|
// values).
|
||||||
const restTransform =
|
const restTransform = `translateX(${xDelta}px) translateY(${yDelta}px) scale(${scale})`;
|
||||||
`translateX(${xDelta}px) translateY(${yDelta}px) scale(${scale})`;
|
|
||||||
const floatTransform = `translateX(0) translateY(0) scale(1)`;
|
const floatTransform = `translateX(0) translateY(0) scale(1)`;
|
||||||
|
|
||||||
// Constrain the floating labels width to a scaled percentage of the
|
// 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` : '';
|
const width = isRestingClipped ? `${restingClientWidth / scale}px` : '';
|
||||||
if (this.focused || this.populated) {
|
if (this.focused || this.populated) {
|
||||||
return [
|
return [
|
||||||
{transform: restTransform, width}, {transform: floatTransform, width}
|
{transform: restTransform, width},
|
||||||
|
{transform: floatTransform, width},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{transform: floatTransform, width}, {transform: restTransform, width}
|
{transform: floatTransform, width},
|
||||||
|
{transform: restTransform, width},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,11 @@ class TestField extends Field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
didErrorAnnounce() {
|
didErrorAnnounce() {
|
||||||
return this.renderRoot.querySelector('.supporting-text')
|
return (
|
||||||
?.getAttribute('role') === 'alert';
|
this.renderRoot
|
||||||
|
.querySelector('.supporting-text')
|
||||||
|
?.getAttribute('role') === 'alert'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure floating/resting labels are both rendered
|
// Ensure floating/resting labels are both rendered
|
||||||
@ -54,9 +57,8 @@ describe('Field', () => {
|
|||||||
.populated=${props.populated ?? false}
|
.populated=${props.populated ?? false}
|
||||||
.required=${props.required ?? false}
|
.required=${props.required ?? false}
|
||||||
.supportingText=${props.supportingText ?? ''}
|
.supportingText=${props.supportingText ?? ''}
|
||||||
.errorText=${props.errorText ?? ''}
|
.errorText=${props.errorText ?? ''}>
|
||||||
>
|
<input />
|
||||||
<input>
|
|
||||||
</md-test-field>
|
</md-test-field>
|
||||||
`;
|
`;
|
||||||
const root = env.render(template);
|
const root = env.render(template);
|
||||||
@ -292,7 +294,8 @@ describe('Field', () => {
|
|||||||
// Assertion.
|
// Assertion.
|
||||||
expect(instance.labelText)
|
expect(instance.labelText)
|
||||||
.withContext(
|
.withContext(
|
||||||
'label text should be empty string if label is not provided')
|
'label text should be empty string if label is not provided',
|
||||||
|
)
|
||||||
.toBe('');
|
.toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -315,19 +318,20 @@ describe('Field', () => {
|
|||||||
// Assertion.
|
// Assertion.
|
||||||
expect(instance.labelText)
|
expect(instance.labelText)
|
||||||
.withContext(
|
.withContext(
|
||||||
'label text should equal label with asterisk when required')
|
'label text should equal label with asterisk when required',
|
||||||
|
)
|
||||||
.toBe(`${labelValue}*`);
|
.toBe(`${labelValue}*`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render asterisk if required when there is no label',
|
it('should not render asterisk if required when there is no label', async () => {
|
||||||
async () => {
|
|
||||||
// Setup.
|
// Setup.
|
||||||
// Test case.
|
// Test case.
|
||||||
const {instance} = await setupTest({required: true, label: undefined});
|
const {instance} = await setupTest({required: true, label: undefined});
|
||||||
// Assertion.
|
// Assertion.
|
||||||
expect(instance.labelText)
|
expect(instance.labelText)
|
||||||
.withContext(
|
.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('');
|
.toBe('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -335,18 +339,22 @@ describe('Field', () => {
|
|||||||
describe('supporting text', () => {
|
describe('supporting text', () => {
|
||||||
it('should update to errorText when error is true', async () => {
|
it('should update to errorText when error is true', async () => {
|
||||||
const errorText = 'Error message';
|
const errorText = 'Error message';
|
||||||
const {instance} = await setupTest(
|
const {instance} = await setupTest({
|
||||||
{error: true, supportingText: 'Supporting text', errorText});
|
error: true,
|
||||||
|
supportingText: 'Supporting text',
|
||||||
|
errorText,
|
||||||
|
});
|
||||||
|
|
||||||
expect(instance.supportingTextContent).toEqual(errorText);
|
expect(instance.supportingTextContent).toEqual(errorText);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('error announcement', () => {
|
describe('error announcement', () => {
|
||||||
it('should announce errors when both error and errorText are set',
|
it('should announce errors when both error and errorText are set', async () => {
|
||||||
async () => {
|
const {instance} = await setupTest({
|
||||||
const {instance} =
|
error: true,
|
||||||
await setupTest({error: true, errorText: 'Error message'});
|
errorText: 'Error message',
|
||||||
|
});
|
||||||
|
|
||||||
expect(instance.didErrorAnnounce())
|
expect(instance.didErrorAnnounce())
|
||||||
.withContext('instance.didErrorAnnounce()')
|
.withContext('instance.didErrorAnnounce()')
|
||||||
@ -365,8 +373,10 @@ describe('Field', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should re-announce when reannounceError() is called', async () => {
|
it('should re-announce when reannounceError() is called', async () => {
|
||||||
const {instance} =
|
const {instance} = await setupTest({
|
||||||
await setupTest({error: true, errorText: 'Error message'});
|
error: true,
|
||||||
|
errorText: 'Error message',
|
||||||
|
});
|
||||||
|
|
||||||
instance.reannounceError();
|
instance.reannounceError();
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
|
@ -4,18 +4,23 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Focus', [
|
'Focus',
|
||||||
new Knob('inward', {ui: boolInput(), defaultValue: false}),
|
[new Knob('inward', {ui: boolInput(), defaultValue: false})],
|
||||||
]);
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -55,13 +55,13 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-focus-ring ?inward=${inward}></md-focus-ring>
|
<md-focus-ring ?inward=${inward}></md-focus-ring>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const multiAction: MaterialStoryInit<StoryKnobs> = {
|
const multiAction: MaterialStoryInit<StoryKnobs> = {
|
||||||
name: 'Multi-action components',
|
name: 'Multi-action components',
|
||||||
styles: css`
|
styles: css`
|
||||||
[role="list"] {
|
[role='list'] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background: var(--md-sys-color-surface);
|
background: var(--md-sys-color-surface);
|
||||||
@ -76,11 +76,11 @@ const multiAction: MaterialStoryInit<StoryKnobs> = {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
[role="list"]:focus-within {
|
[role='list']:focus-within {
|
||||||
background: var(--md-sys-color-surface-variant);
|
background: var(--md-sys-color-surface-variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
[role="listitem"] {
|
[role='listitem'] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ const multiAction: MaterialStoryInit<StoryKnobs> = {
|
|||||||
--md-focus-ring-shape: 32px;
|
--md-focus-ring-shape: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[role="list"]::before,
|
[role='list']::before,
|
||||||
#secondary::before {
|
#secondary::before {
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
border: 1px solid var(--md-sys-color-outline);
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
@ -120,20 +120,24 @@ const multiAction: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-focus-ring for="primary" ?inward=${inward}></md-focus-ring>
|
<md-focus-ring for="primary" ?inward=${inward}></md-focus-ring>
|
||||||
|
|
||||||
<div role="listitem">
|
<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
|
Action
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="listitem">
|
<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
|
X
|
||||||
<md-focus-ring ?inward=${inward}></md-focus-ring>
|
<md-focus-ring ?inward=${inward}></md-focus-ring>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Focus ring stories. */
|
/** Focus ring stories. */
|
||||||
|
@ -7,7 +7,10 @@
|
|||||||
import {isServer, LitElement, PropertyValues} from 'lit';
|
import {isServer, LitElement, PropertyValues} from 'lit';
|
||||||
import {property} from 'lit/decorators.js';
|
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.
|
* Events that the focus ring listens to.
|
||||||
@ -45,8 +48,10 @@ export class FocusRing extends LitElement implements Attachable {
|
|||||||
this.attachableController.control = control;
|
this.attachableController.control = control;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly attachableController =
|
private readonly attachableController = new AttachableController(
|
||||||
new AttachableController(this, this.onControlChange.bind(this));
|
this,
|
||||||
|
this.onControlChange.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
attach(control: HTMLElement) {
|
attach(control: HTMLElement) {
|
||||||
this.attachableController.attach(control);
|
this.attachableController.attach(control);
|
||||||
|
@ -64,8 +64,7 @@ describe('focus ring', () => {
|
|||||||
expect(focusRing.control).withContext('focusRing.control').toBe(button);
|
expect(focusRing.control).withContext('focusRing.control').toBe(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update a referenced element when for attribute changes',
|
it('should update a referenced element when for attribute changes', async () => {
|
||||||
async () => {
|
|
||||||
const {root, focusRing} = setupTest(html`
|
const {root, focusRing} = setupTest(html`
|
||||||
<button id="first"></button>
|
<button id="first"></button>
|
||||||
<button id="second"></button>
|
<button id="second"></button>
|
||||||
|
@ -23,8 +23,7 @@ describe('<md-icon>', () => {
|
|||||||
|
|
||||||
describe('accessiblity', () => {
|
describe('accessiblity', () => {
|
||||||
it('sets aria-hidden to true by default', async () => {
|
it('sets aria-hidden to true by default', async () => {
|
||||||
const root = env.render(html`
|
const root = env.render(html` <md-icon>check</md-icon>`);
|
||||||
<md-icon>check</md-icon>`);
|
|
||||||
const icon = root.querySelector('md-icon')!;
|
const icon = root.querySelector('md-icon')!;
|
||||||
|
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
@ -33,8 +32,9 @@ describe('<md-icon>', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets aria-hidden is removed when initalized as false', async () => {
|
it('sets aria-hidden is removed when initalized as false', async () => {
|
||||||
const root = env.render(html`
|
const root = env.render(html` <md-icon aria-hidden="false"
|
||||||
<md-icon aria-hidden="false">check</md-icon>`);
|
>check</md-icon
|
||||||
|
>`);
|
||||||
const icon = root.querySelector('md-icon')!;
|
const icon = root.querySelector('md-icon')!;
|
||||||
|
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
@ -43,8 +43,7 @@ describe('<md-icon>', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('allows overriding aria-hidden after first render', async () => {
|
it('allows overriding aria-hidden after first render', async () => {
|
||||||
const root = env.render(html`
|
const root = env.render(html` <md-icon>check</md-icon>`);
|
||||||
<md-icon>check</md-icon>`);
|
|
||||||
const icon = root.querySelector('md-icon')!;
|
const icon = root.querySelector('md-icon')!;
|
||||||
|
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
@ -58,8 +57,8 @@ describe('<md-icon>', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('overrides invalid aria-hidden values to true', async () => {
|
it('overrides invalid aria-hidden values to true', async () => {
|
||||||
const root = env.render(html`
|
const root =
|
||||||
<!-- @ts-ignore:disable-next-line:no-incompatible-type-binding -->
|
env.render(html` <!-- @ts-ignore:disable-next-line:no-incompatible-type-binding -->
|
||||||
<md-icon aria-hidden="foo">check</md-icon>`);
|
<md-icon aria-hidden="foo">check</md-icon>`);
|
||||||
const icon = root.querySelector('md-icon')!;
|
const icon = root.querySelector('md-icon')!;
|
||||||
|
|
||||||
|
@ -4,20 +4,27 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob, textInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Icon Button', [
|
'Icon Button',
|
||||||
|
[
|
||||||
new Knob('icon', {ui: textInput(), defaultValue: 'check'}),
|
new Knob('icon', {ui: textInput(), defaultValue: 'check'}),
|
||||||
new Knob('selectedIcon', {ui: textInput(), defaultValue: 'close'}),
|
new Knob('selectedIcon', {ui: textInput(), defaultValue: 'close'}),
|
||||||
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
import '@material/web/icon/icon.js';
|
import '@material/web/icon/icon.js';
|
||||||
import '@material/web/iconbutton/filled-icon-button.js';
|
import '@material/web/iconbutton/filled-icon-button.js';
|
||||||
import '@material/web/iconbutton/filled-tonal-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/icon-button.js';
|
||||||
|
import '@material/web/iconbutton/outlined-icon-button.js';
|
||||||
|
|
||||||
import {MaterialStoryInit} from './material-collection.js';
|
import {MaterialStoryInit} from './material-collection.js';
|
||||||
import {css, html} from 'lit';
|
import {css, html} from 'lit';
|
||||||
@ -40,10 +40,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
|
|||||||
return html`
|
return html`
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<md-icon-button
|
<md-icon-button aria-label="Standard icon" ?disabled=${disabled}>
|
||||||
aria-label="Standard icon"
|
|
||||||
?disabled=${disabled}
|
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
|
|
||||||
@ -53,10 +50,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-outlined-icon-button>
|
</md-outlined-icon-button>
|
||||||
|
|
||||||
<md-filled-icon-button
|
<md-filled-icon-button aria-label="Filled icon" ?disabled=${disabled}>
|
||||||
aria-label="Filled icon"
|
|
||||||
?disabled=${disabled}
|
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-filled-icon-button>
|
</md-filled-icon-button>
|
||||||
|
|
||||||
@ -68,7 +62,7 @@ const buttons: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggles: MaterialStoryInit<StoryKnobs> = {
|
const toggles: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -81,8 +75,7 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-icon-button
|
<md-icon-button
|
||||||
aria-label="Standard icon"
|
aria-label="Standard icon"
|
||||||
toggle
|
toggle
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}>
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
<md-icon slot="selected">${selectedIcon}</md-icon>
|
<md-icon slot="selected">${selectedIcon}</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
@ -90,8 +83,7 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-outlined-icon-button
|
<md-outlined-icon-button
|
||||||
aria-label="Outlined icon"
|
aria-label="Outlined icon"
|
||||||
toggle
|
toggle
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}>
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
<md-icon slot="selected">${selectedIcon}</md-icon>
|
<md-icon slot="selected">${selectedIcon}</md-icon>
|
||||||
</md-outlined-icon-button>
|
</md-outlined-icon-button>
|
||||||
@ -99,8 +91,7 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-filled-icon-button
|
<md-filled-icon-button
|
||||||
aria-label="Filled icon"
|
aria-label="Filled icon"
|
||||||
toggle
|
toggle
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}>
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
<md-icon slot="selected">${selectedIcon}</md-icon>
|
<md-icon slot="selected">${selectedIcon}</md-icon>
|
||||||
</md-filled-icon-button>
|
</md-filled-icon-button>
|
||||||
@ -108,15 +99,14 @@ const toggles: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-filled-tonal-icon-button
|
<md-filled-tonal-icon-button
|
||||||
aria-label="Filled tonal icon"
|
aria-label="Filled tonal icon"
|
||||||
toggle
|
toggle
|
||||||
?disabled=${disabled}
|
?disabled=${disabled}>
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
<md-icon slot="selected">${selectedIcon}</md-icon>
|
<md-icon slot="selected">${selectedIcon}</md-icon>
|
||||||
</md-filled-tonal-icon-button>
|
</md-filled-tonal-icon-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const links: MaterialStoryInit<StoryKnobs> = {
|
const links: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -129,38 +119,34 @@ const links: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<md-icon-button
|
<md-icon-button
|
||||||
aria-label="Standard icon"
|
aria-label="Standard icon"
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank">
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
|
|
||||||
<md-outlined-icon-button
|
<md-outlined-icon-button
|
||||||
aria-label="Outlined icon"
|
aria-label="Outlined icon"
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank">
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-outlined-icon-button>
|
</md-outlined-icon-button>
|
||||||
|
|
||||||
<md-filled-icon-button
|
<md-filled-icon-button
|
||||||
aria-label="Filled icon"
|
aria-label="Filled icon"
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank">
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-filled-icon-button>
|
</md-filled-icon-button>
|
||||||
|
|
||||||
<md-filled-tonal-icon-button
|
<md-filled-tonal-icon-button
|
||||||
aria-label="Filled tonal icon"
|
aria-label="Filled tonal icon"
|
||||||
href="https://google.com"
|
href="https://google.com"
|
||||||
target="_blank"
|
target="_blank">
|
||||||
>
|
|
||||||
<md-icon>${icon}</md-icon>
|
<md-icon>${icon}</md-icon>
|
||||||
</md-filled-tonal-icon-button>
|
</md-filled-tonal-icon-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Icon button stories. */
|
/** Icon button stories. */
|
||||||
|
@ -44,7 +44,8 @@ describe('icon button tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('md-icon-button', () => {
|
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',
|
'button element',
|
||||||
async () => {
|
async () => {
|
||||||
const {element} = await setUpTest('button');
|
const {element} = await setUpTest('button');
|
||||||
@ -57,9 +58,11 @@ describe('icon button tests', () => {
|
|||||||
element.disabled = false;
|
element.disabled = false;
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.hasAttribute('disabled')).toBeFalse();
|
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',
|
'button element',
|
||||||
async () => {
|
async () => {
|
||||||
const {element} = await setUpTest('button');
|
const {element} = await setUpTest('button');
|
||||||
@ -68,11 +71,13 @@ describe('icon button tests', () => {
|
|||||||
element.ariaLabel = 'test';
|
element.ariaLabel = 'test';
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.getAttribute('aria-label')).toBe('test');
|
expect(button.getAttribute('aria-label')).toBe('test');
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('md-icon-button link', () => {
|
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',
|
'tag',
|
||||||
async () => {
|
async () => {
|
||||||
const {element} = await setUpTest('link');
|
const {element} = await setUpTest('link');
|
||||||
@ -82,11 +87,13 @@ describe('icon button tests', () => {
|
|||||||
element.ariaLabel = 'test';
|
element.ariaLabel = 'test';
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(anchor.getAttribute('aria-label')).toBe('test');
|
expect(anchor.getAttribute('aria-label')).toBe('test');
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('md-icon-button toggle', () => {
|
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',
|
'button element',
|
||||||
async () => {
|
async () => {
|
||||||
const {element} = await setUpTest('toggle');
|
const {element} = await setUpTest('toggle');
|
||||||
@ -99,9 +106,11 @@ describe('icon button tests', () => {
|
|||||||
element.disabled = false;
|
element.disabled = false;
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.hasAttribute('disabled')).toBeFalse();
|
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',
|
'button element',
|
||||||
async () => {
|
async () => {
|
||||||
const {element} = await setUpTest('toggle');
|
const {element} = await setUpTest('toggle');
|
||||||
@ -110,7 +119,8 @@ describe('icon button tests', () => {
|
|||||||
element.ariaLabel = 'test';
|
element.ariaLabel = 'test';
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.getAttribute('aria-label')).toBe('test');
|
expect(button.getAttribute('aria-label')).toBe('test');
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it('toggles the `selected` state when button is clicked', async () => {
|
it('toggles the `selected` state when button is clicked', async () => {
|
||||||
const {element, harness} = await setUpTest('toggle');
|
const {element, harness} = await setUpTest('toggle');
|
||||||
@ -126,8 +136,8 @@ describe('icon button tests', () => {
|
|||||||
const {element, harness} = await setUpTest('toggle');
|
const {element, harness} = await setUpTest('toggle');
|
||||||
let changeEvent = false;
|
let changeEvent = false;
|
||||||
let inputEvent = false;
|
let inputEvent = false;
|
||||||
element.addEventListener('input', () => inputEvent = true);
|
element.addEventListener('input', () => (inputEvent = true));
|
||||||
element.addEventListener('change', () => changeEvent = true);
|
element.addEventListener('change', () => (changeEvent = true));
|
||||||
expect(element.selected).toBeFalse();
|
expect(element.selected).toBeFalse();
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
expect(element.selected).toBeTrue();
|
expect(element.selected).toBeTrue();
|
||||||
@ -135,8 +145,7 @@ describe('icon button tests', () => {
|
|||||||
expect(changeEvent).toBeTrue();
|
expect(changeEvent).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('setting `selected` updates the aria-pressed attribute on the native button element',
|
it('setting `selected` updates the aria-pressed attribute on the native button element', async () => {
|
||||||
async () => {
|
|
||||||
const {element} = await setUpTest('toggle');
|
const {element} = await setUpTest('toggle');
|
||||||
|
|
||||||
element.selected = true;
|
element.selected = true;
|
||||||
@ -167,8 +176,7 @@ describe('icon button tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('if `flipsIconInRtl=true`, flips icon in an RTL context', async () => {
|
it('if `flipsIconInRtl=true`, flips icon in an RTL context', async () => {
|
||||||
const template = html`
|
const template = html` <div dir="rtl">
|
||||||
<div dir="rtl">
|
|
||||||
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
||||||
star
|
star
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
@ -179,10 +187,8 @@ describe('icon button tests', () => {
|
|||||||
expect((element as unknown as IconButtonInternals).flipIcon).toBeTrue();
|
expect((element as unknown as IconButtonInternals).flipIcon).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('if `flipsIconInRtl=true`, does not flip icon in an LTR context',
|
it('if `flipsIconInRtl=true`, does not flip icon in an LTR context', async () => {
|
||||||
async () => {
|
const template = html` <div dir="ltr">
|
||||||
const template = html`
|
|
||||||
<div dir="ltr">
|
|
||||||
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
||||||
star
|
star
|
||||||
</md-icon-button>
|
</md-icon-button>
|
||||||
@ -190,14 +196,13 @@ describe('icon button tests', () => {
|
|||||||
const element = env.render(template).querySelector('md-icon-button')!;
|
const element = env.render(template).querySelector('md-icon-button')!;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
|
|
||||||
expect((element as unknown as IconButtonInternals).flipIcon)
|
expect((element as unknown as IconButtonInternals).flipIcon).toBeFalse();
|
||||||
.toBeFalse();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow preventing toggle click event', async () => {
|
it('should allow preventing toggle click event', async () => {
|
||||||
const {element, harness} = await setUpTest('toggle');
|
const {element, harness} = await setUpTest('toggle');
|
||||||
|
|
||||||
element.addEventListener('click', event => {
|
element.addEventListener('click', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,12 +10,16 @@ import '../../ripple/ripple.js';
|
|||||||
import {html, LitElement, nothing} from 'lit';
|
import {html, LitElement, nothing} from 'lit';
|
||||||
import {property, state} from 'lit/decorators.js';
|
import {property, state} from 'lit/decorators.js';
|
||||||
import {classMap} from 'lit/directives/class-map.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 {ARIAMixinStrict} from '../../internal/aria/aria.js';
|
||||||
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
||||||
import {internals} from '../../internal/controller/element-internals.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';
|
import {isRtl} from '../../internal/controller/is-rtl.js';
|
||||||
|
|
||||||
type LinkTarget = '_blank' | '_parent' | '_self' | '_top';
|
type LinkTarget = '_blank' | '_parent' | '_self' | '_top';
|
||||||
@ -31,8 +35,10 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
static readonly formAssociated = true;
|
static readonly formAssociated = true;
|
||||||
|
|
||||||
/** @nocollapse */
|
/** @nocollapse */
|
||||||
static override shadowRootOptions:
|
static override shadowRootOptions: ShadowRootInit = {
|
||||||
ShadowRootInit = {mode: 'open', delegatesFocus: true};
|
mode: 'open',
|
||||||
|
delegatesFocus: true,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disables the icon button and makes it non-interactive.
|
* 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);
|
@state() private flipIcon = isRtl(this, this.flipIconInRtl);
|
||||||
|
|
||||||
/** @private */
|
/** @private */
|
||||||
[internals] =
|
[internals] = (this as HTMLElement) /* needed for closure */
|
||||||
(this as HTMLElement /* needed for closure */).attachInternals();
|
.attachInternals();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link buttons cannot be disabled.
|
* Link buttons cannot be disabled.
|
||||||
@ -121,16 +127,17 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
const ariaPressedValue = !this.toggle ? nothing : this.selected;
|
const ariaPressedValue = !this.toggle ? nothing : this.selected;
|
||||||
let ariaLabelValue: string | null | typeof nothing = nothing;
|
let ariaLabelValue: string | null | typeof nothing = nothing;
|
||||||
if (!this.href) {
|
if (!this.href) {
|
||||||
ariaLabelValue = (hasToggledAriaLabel && this.selected) ?
|
ariaLabelValue =
|
||||||
this.ariaLabelSelected :
|
hasToggledAriaLabel && this.selected
|
||||||
ariaLabel;
|
? this.ariaLabelSelected
|
||||||
|
: ariaLabel;
|
||||||
}
|
}
|
||||||
return staticHtml`<${tag}
|
return staticHtml`<${tag}
|
||||||
class="icon-button ${classMap(this.getRenderClasses())}"
|
class="icon-button ${classMap(this.getRenderClasses())}"
|
||||||
id="button"
|
id="button"
|
||||||
aria-label="${ariaLabelValue || nothing}"
|
aria-label="${ariaLabelValue || nothing}"
|
||||||
aria-haspopup="${!this.href && ariaHasPopup || nothing}"
|
aria-haspopup="${(!this.href && ariaHasPopup) || nothing}"
|
||||||
aria-expanded="${!this.href && ariaExpanded || nothing}"
|
aria-expanded="${(!this.href && ariaExpanded) || nothing}"
|
||||||
aria-pressed="${ariaPressedValue}"
|
aria-pressed="${ariaPressedValue}"
|
||||||
?disabled="${!this.href && this.disabled}"
|
?disabled="${!this.href && this.disabled}"
|
||||||
@click="${this.handleClick}">
|
@click="${this.handleClick}">
|
||||||
@ -147,12 +154,12 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
// Needed for closure conformance
|
// Needed for closure conformance
|
||||||
const {ariaLabel} = this as ARIAMixinStrict;
|
const {ariaLabel} = this as ARIAMixinStrict;
|
||||||
return html`
|
return html`
|
||||||
<a class="link"
|
<a
|
||||||
|
class="link"
|
||||||
id="link"
|
id="link"
|
||||||
href="${this.href}"
|
href="${this.href}"
|
||||||
target="${this.target || nothing}"
|
target="${this.target || nothing}"
|
||||||
aria-label="${ariaLabel || nothing}"
|
aria-label="${ariaLabel || nothing}"></a>
|
||||||
></a>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,7 +176,9 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
|
|
||||||
private renderSelectedIcon() {
|
private renderSelectedIcon() {
|
||||||
// Use default slot as fallback to not require specifying multiple icons
|
// 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() {
|
private renderTouchTarget() {
|
||||||
@ -177,15 +186,15 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderFocusRing() {
|
private renderFocusRing() {
|
||||||
return html`<md-focus-ring part="focus-ring" for=${
|
return html`<md-focus-ring
|
||||||
this.href ? 'link' : 'button'}></md-focus-ring>`;
|
part="focus-ring"
|
||||||
|
for=${this.href ? 'link' : 'button'}></md-focus-ring>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderRipple() {
|
private renderRipple() {
|
||||||
return html`<md-ripple
|
return html`<md-ripple
|
||||||
for=${this.href ? 'link' : nothing}
|
for=${this.href ? 'link' : nothing}
|
||||||
?disabled="${!this.href && this.disabled}"
|
?disabled="${!this.href && this.disabled}"></md-ripple>`;
|
||||||
></md-ripple>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
@ -202,7 +211,8 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
|
|
||||||
this.selected = !this.selected;
|
this.selected = !this.selected;
|
||||||
this.dispatchEvent(
|
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>
|
// Bubbles but does not compose to mimic native browser <input> & <select>
|
||||||
// Additionally, native change event is not an InputEvent.
|
// Additionally, native change event is not an InputEvent.
|
||||||
this.dispatchEvent(new Event('change', {bubbles: true}));
|
this.dispatchEvent(new Event('change', {bubbles: true}));
|
||||||
|
@ -87,19 +87,24 @@ export function isAriaAttribute(attribute: string): attribute is ARIAAttribute {
|
|||||||
* @return The aria attribute.
|
* @return The aria attribute.
|
||||||
*/
|
*/
|
||||||
export function ariaPropertyToAttribute<K extends ARIAProperty | 'role'>(
|
export function ariaPropertyToAttribute<K extends ARIAProperty | 'role'>(
|
||||||
property: K) {
|
property: K,
|
||||||
return property
|
) {
|
||||||
|
return (
|
||||||
|
property
|
||||||
.replace('aria', 'aria-')
|
.replace('aria', 'aria-')
|
||||||
// IDREF attributes also include an "Element" or "Elements" suffix
|
// IDREF attributes also include an "Element" or "Elements" suffix
|
||||||
.replace(/Elements?/g, '')
|
.replace(/Elements?/g, '')
|
||||||
.toLowerCase() as ARIAPropertyToAttribute<K>;
|
.toLowerCase() as ARIAPropertyToAttribute<K>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts an `ariaFoo` string type to an `aria-foo` string type.
|
// Converts an `ariaFoo` string type to an `aria-foo` string type.
|
||||||
type ARIAPropertyToAttribute<K extends string> =
|
type ARIAPropertyToAttribute<K extends string> =
|
||||||
K extends `aria${infer Suffix}Element${infer OptS}` ?
|
K extends `aria${infer Suffix}Element${infer OptS}`
|
||||||
`aria-${Lowercase < Suffix >}` :
|
? `aria-${Lowercase<Suffix>}`
|
||||||
K extends `aria${infer Suffix}` ? `aria-${Lowercase < Suffix >}` : K;
|
: K extends `aria${infer Suffix}`
|
||||||
|
? `aria-${Lowercase<Suffix>}`
|
||||||
|
: K;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An extension of `ARIAMixin` that enforces strict value types for aria
|
* An extension of `ARIAMixin` that enforces strict value types for aria
|
||||||
@ -125,10 +130,26 @@ export interface ARIAMixinStrict extends ARIAMixin {
|
|||||||
ariaColCount: `${number}` | null;
|
ariaColCount: `${number}` | null;
|
||||||
ariaColIndex: `${number}` | null;
|
ariaColIndex: `${number}` | null;
|
||||||
ariaColSpan: `${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;
|
ariaDisabled: 'true' | 'false' | null;
|
||||||
ariaExpanded: '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;
|
ariaHidden: 'true' | 'false' | null;
|
||||||
ariaInvalid: 'true' | 'false' | null;
|
ariaInvalid: 'true' | 'false' | null;
|
||||||
ariaKeyShortcuts: string | null;
|
ariaKeyShortcuts: string | null;
|
||||||
@ -162,24 +183,116 @@ export interface ARIAMixinStrict extends ARIAMixin {
|
|||||||
* Valid values for `role`.
|
* Valid values for `role`.
|
||||||
*/
|
*/
|
||||||
export type ARIARole =
|
export type ARIARole =
|
||||||
'alert'|'alertdialog'|'button'|'checkbox'|'dialog'|'gridcell'|'link'|'log'|
|
| 'alert'
|
||||||
'marquee'|'menuitem'|'menuitemcheckbox'|'menuitemradio'|'option'|
|
| 'alertdialog'
|
||||||
'progressbar'|'radio'|'scrollbar'|'searchbox'|'slider'|'spinbutton'|
|
| 'button'
|
||||||
'status'|'switch'|'tab'|'tabpanel'|'textbox'|'timer'|'tooltip'|'treeitem'|
|
| 'checkbox'
|
||||||
'combobox'|'grid'|'listbox'|'menu'|'menubar'|'radiogroup'|'tablist'|'tree'|
|
| 'dialog'
|
||||||
'treegrid'|'application'|'article'|'cell'|'columnheader'|'definition'|
|
| 'gridcell'
|
||||||
'directory'|'document'|'feed'|'figure'|'group'|'heading'|'img'|'list'|
|
| 'link'
|
||||||
'listitem'|'math'|'none'|'note'|'presentation'|'region'|'row'|'rowgroup'|
|
| 'log'
|
||||||
'rowheader'|'separator'|'table'|'term'|'text'|'toolbar'|'banner'|
|
| 'marquee'
|
||||||
'complementary'|'contentinfo'|'form'|'main'|'navigation'|'region'|'search'|
|
| 'menuitem'
|
||||||
'doc-abstract'|'doc-acknowledgments'|'doc-afterword'|'doc-appendix'|
|
| 'menuitemcheckbox'
|
||||||
'doc-backlink'|'doc-biblioentry'|'doc-bibliography'|'doc-biblioref'|
|
| 'menuitemradio'
|
||||||
'doc-chapter'|'doc-colophon'|'doc-conclusion'|'doc-cover'|'doc-credit'|
|
| 'option'
|
||||||
'doc-credits'|'doc-dedication'|'doc-endnote'|'doc-endnotes'|'doc-epigraph'|
|
| 'progressbar'
|
||||||
'doc-epilogue'|'doc-errata'|'doc-example'|'doc-footnote'|'doc-foreword'|
|
| 'radio'
|
||||||
'doc-glossary'|'doc-glossref'|'doc-index'|'doc-introduction'|'doc-noteref'|
|
| 'scrollbar'
|
||||||
'doc-notice'|'doc-pagebreak'|'doc-pagelist'|'doc-part'|'doc-preface'|
|
| 'searchbox'
|
||||||
'doc-prologue'|'doc-pullquote'|'doc-qna'|'doc-subtitle'|'doc-tip'|'doc-toc';
|
| '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.
|
* 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.
|
* @param options Options to configure the element's host aria.
|
||||||
*/
|
*/
|
||||||
export function setupHostAria(
|
export function setupHostAria(
|
||||||
ctor: typeof ReactiveElement, {focusable}: SetupHostAriaOptions = {}) {
|
ctor: typeof ReactiveElement,
|
||||||
|
{focusable}: SetupHostAriaOptions = {},
|
||||||
|
) {
|
||||||
if (focusable !== false) {
|
if (focusable !== false) {
|
||||||
ctor.addInitializer(host => {
|
ctor.addInitializer((host) => {
|
||||||
host.addController({
|
host.addController({
|
||||||
hostConnected() {
|
hostConnected() {
|
||||||
if (host.hasAttribute('tabindex')) {
|
if (host.hasAttribute('tabindex')) {
|
||||||
@ -206,7 +321,7 @@ export function setupHostAria(
|
|||||||
}
|
}
|
||||||
|
|
||||||
host.tabIndex = 0;
|
host.tabIndex = 0;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -262,7 +377,9 @@ export interface SetupHostAriaOptions {
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export function polyfillElementInternalsAria(
|
export function polyfillElementInternalsAria(
|
||||||
host: ReactiveElement, internals: ElementInternals) {
|
host: ReactiveElement,
|
||||||
|
internals: ElementInternals,
|
||||||
|
) {
|
||||||
if (checkIfElementInternalsSupportsAria(internals)) {
|
if (checkIfElementInternalsSupportsAria(internals)) {
|
||||||
return internals;
|
return internals;
|
||||||
}
|
}
|
||||||
@ -271,8 +388,10 @@ export function polyfillElementInternalsAria(
|
|||||||
throw new Error('Missing setupHostAria()');
|
throw new Error('Missing setupHostAria()');
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstConnectedCallbacks:
|
let firstConnectedCallbacks: Array<{
|
||||||
Array<{property: ARIAProperty | 'role', callback: () => void}> = [];
|
property: ARIAProperty | 'role';
|
||||||
|
callback: () => void;
|
||||||
|
}> = [];
|
||||||
let hasBeenConnected = false;
|
let hasBeenConnected = false;
|
||||||
|
|
||||||
// Add support for Firefox, which has not yet implement ElementInternals aria
|
// Add support for Firefox, which has not yet implement ElementInternals aria
|
||||||
@ -288,8 +407,10 @@ export function polyfillElementInternalsAria(
|
|||||||
const setValue = () => {
|
const setValue = () => {
|
||||||
internalAriaValue = value;
|
internalAriaValue = value;
|
||||||
if (!hasBeenConnected) {
|
if (!hasBeenConnected) {
|
||||||
firstConnectedCallbacks.push(
|
firstConnectedCallbacks.push({
|
||||||
{property: ariaProperty, callback: setValue});
|
property: ariaProperty,
|
||||||
|
callback: setValue,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,13 +491,12 @@ export function polyfillElementInternalsAria(
|
|||||||
|
|
||||||
// Remove strong callback references
|
// Remove strong callback references
|
||||||
firstConnectedCallbacks = [];
|
firstConnectedCallbacks = [];
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return internals;
|
return internals;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Separate function so that typescript doesn't complain about internals being
|
// Separate function so that typescript doesn't complain about internals being
|
||||||
// "never".
|
// "never".
|
||||||
function checkIfElementInternalsSupportsAria(internals: ElementInternals) {
|
function checkIfElementInternalsSupportsAria(internals: ElementInternals) {
|
||||||
|
@ -9,7 +9,13 @@
|
|||||||
import {html, LitElement} from 'lit';
|
import {html, LitElement} from 'lit';
|
||||||
import {customElement} from 'lit/decorators.js';
|
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('aria', () => {
|
||||||
describe('isAriaAttribute()', () => {
|
describe('isAriaAttribute()', () => {
|
||||||
@ -40,8 +46,9 @@ describe('aria', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should convert aria idref properties', () => {
|
it('should convert aria idref properties', () => {
|
||||||
expect(ariaPropertyToAttribute('ariaLabelledByElements' as ARIAProperty))
|
expect(
|
||||||
.toBe('aria-labelledby');
|
ariaPropertyToAttribute('ariaLabelledByElements' as ARIAProperty),
|
||||||
|
).toBe('aria-labelledby');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -98,8 +105,7 @@ describe('aria', () => {
|
|||||||
|
|
||||||
if (!('role' in Element.prototype)) {
|
if (!('role' in Element.prototype)) {
|
||||||
describe('polyfill', () => {
|
describe('polyfill', () => {
|
||||||
it('should hydrate aria attributes when ARIAMixin is not supported',
|
it('should hydrate aria attributes when ARIAMixin is not supported', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
element.role = 'button';
|
element.role = 'button';
|
||||||
@ -160,8 +166,7 @@ describe('aria', () => {
|
|||||||
.toEqual('Foo');
|
.toEqual('Foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hydrate role attributes when set before connection',
|
it('should hydrate role attributes when set before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
// TestElement() sets role in constructor
|
// TestElement() sets role in constructor
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
@ -173,8 +178,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hydrate aria attributes when set before connection',
|
it('should hydrate aria attributes when set before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.internals.ariaLabel = 'Foo';
|
element.internals.ariaLabel = 'Foo';
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
@ -198,8 +202,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not override aria attributes on host when set before connection',
|
it('should not override aria attributes on host when set before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.setAttribute('aria-label', 'Value set by user');
|
element.setAttribute('aria-label', 'Value set by user');
|
||||||
element.internals.ariaLabel = 'Value set on internals';
|
element.internals.ariaLabel = 'Value set on internals';
|
||||||
@ -215,8 +218,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not override aria properties on host when set before connection',
|
it('should not override aria properties on host when set before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.ariaLabel = 'Value set by user';
|
element.ariaLabel = 'Value set by user';
|
||||||
element.internals.ariaLabel = 'Value set on internals';
|
element.internals.ariaLabel = 'Value set on internals';
|
||||||
@ -235,8 +237,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not override role attribute on host when set before connection',
|
it('should not override role attribute on host when set before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.setAttribute('role', 'Value set by user');
|
element.setAttribute('role', 'Value set by user');
|
||||||
element.internals.role = 'Value set on internals';
|
element.internals.role = 'Value set on internals';
|
||||||
@ -252,8 +253,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not override role property on host when set before connection',
|
it('should not override role property on host when set before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.role = 'Value set by user';
|
element.role = 'Value set by user';
|
||||||
element.internals.role = 'Value set on internals';
|
element.internals.role = 'Value set on internals';
|
||||||
@ -272,8 +272,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle setting role multiple times before connection',
|
it('should handle setting role multiple times before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.internals.role = 'button';
|
element.internals.role = 'button';
|
||||||
element.internals.role = 'checkbox';
|
element.internals.role = 'checkbox';
|
||||||
@ -290,8 +289,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle setting role multiple times before connection when property is set on host',
|
it('should handle setting role multiple times before connection when property is set on host', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.role = 'radio';
|
element.role = 'radio';
|
||||||
element.internals.role = 'button';
|
element.internals.role = 'button';
|
||||||
@ -309,8 +307,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle setting aria properties multiple times before connection',
|
it('should handle setting aria properties multiple times before connection', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.internals.ariaLabel = 'First';
|
element.internals.ariaLabel = 'First';
|
||||||
element.internals.ariaLabel = 'Second';
|
element.internals.ariaLabel = 'Second';
|
||||||
@ -327,8 +324,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle setting aria properties multiple times before connection when property is set on host',
|
it('should handle setting aria properties multiple times before connection when property is set on host', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.ariaLabel = 'First';
|
element.ariaLabel = 'First';
|
||||||
element.internals.ariaLabel = 'First';
|
element.internals.ariaLabel = 'First';
|
||||||
@ -346,8 +342,7 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle setting role after first connection while disconnected',
|
it('should handle setting role after first connection while disconnected', async () => {
|
||||||
async () => {
|
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.internals.role = 'button';
|
element.internals.role = 'button';
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
|
@ -46,11 +46,11 @@ export function requestUpdateOnAriaChange(ctor: typeof ReactiveElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ctor.addInitializer(element => {
|
ctor.addInitializer((element) => {
|
||||||
const controller = {
|
const controller = {
|
||||||
hostConnected() {
|
hostConnected() {
|
||||||
element.setAttribute('role', 'presentation');
|
element.setAttribute('role', 'presentation');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
element.addController(controller);
|
element.addController(controller);
|
||||||
|
@ -37,8 +37,8 @@ describe('aria', () => {
|
|||||||
|
|
||||||
async function setupTest({ariaLabel}: {ariaLabel?: string} = {}) {
|
async function setupTest({ariaLabel}: {ariaLabel?: string} = {}) {
|
||||||
const root = env.render(html`
|
const root = env.render(html`
|
||||||
<test-aria-delegate aria-label=${
|
<test-aria-delegate
|
||||||
ariaLabel || nothing}></test-aria-delegate>
|
aria-label=${ariaLabel || nothing}></test-aria-delegate>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const host = root.querySelector('test-aria-delegate');
|
const host = root.querySelector('test-aria-delegate');
|
||||||
@ -82,8 +82,7 @@ describe('aria', () => {
|
|||||||
.toEqual(ariaLabel);
|
.toEqual(ariaLabel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update delegated aria attributes when host attribute changes',
|
it('should update delegated aria attributes when host attribute changes', async () => {
|
||||||
async () => {
|
|
||||||
const {host, child} = await setupTest({ariaLabel: 'First aria label'});
|
const {host, child} = await setupTest({ariaLabel: 'First aria label'});
|
||||||
|
|
||||||
host.setAttribute('aria-label', 'Second aria label');
|
host.setAttribute('aria-label', 'Second aria label');
|
||||||
@ -93,8 +92,7 @@ describe('aria', () => {
|
|||||||
.toEqual('Second aria label');
|
.toEqual('Second aria label');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove delegated aria attributes when host attribute is removed',
|
it('should remove delegated aria attributes when host attribute is removed', async () => {
|
||||||
async () => {
|
|
||||||
const {host, child} = await setupTest({ariaLabel: 'First aria label'});
|
const {host, child} = await setupTest({ariaLabel: 'First aria label'});
|
||||||
|
|
||||||
host.removeAttribute('aria-label');
|
host.removeAttribute('aria-label');
|
||||||
|
@ -81,12 +81,13 @@ if (!isServer) {
|
|||||||
* `Attachable` elements. If the `for` attribute changes, the controller will
|
* `Attachable` elements. If the `for` attribute changes, the controller will
|
||||||
* re-attach to the new referenced element.
|
* re-attach to the new referenced element.
|
||||||
*/
|
*/
|
||||||
FOR_ATTRIBUTE_OBSERVER = new MutationObserver(records => {
|
FOR_ATTRIBUTE_OBSERVER = new MutationObserver((records) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
// When a control's `for` attribute changes, inform its
|
// When a control's `for` attribute changes, inform its
|
||||||
// `AttachableController` to update to a new control.
|
// `AttachableController` to update to a new control.
|
||||||
(record.target as AttachableControllerHost)[ATTACHABLE_CONTROLLER]
|
(record.target as AttachableControllerHost)[
|
||||||
?.hostConnected();
|
ATTACHABLE_CONTROLLER
|
||||||
|
]?.hostConnected();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -131,8 +132,9 @@ export class AttachableController implements ReactiveController, Attachable {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (this.host.getRootNode() as Document | ShadowRoot)
|
return (
|
||||||
.querySelector<HTMLElement>(`#${this.htmlFor}`);
|
this.host.getRootNode() as Document | ShadowRoot
|
||||||
|
).querySelector<HTMLElement>(`#${this.htmlFor}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.currentControl || this.host.parentElement;
|
return this.currentControl || this.host.parentElement;
|
||||||
@ -157,8 +159,11 @@ export class AttachableController implements ReactiveController, Attachable {
|
|||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private readonly host: AttachableControllerHost,
|
private readonly host: AttachableControllerHost,
|
||||||
private readonly onControlChange:
|
private readonly onControlChange: (
|
||||||
(prev: HTMLElement|null, next: HTMLElement|null) => void) {
|
prev: HTMLElement | null,
|
||||||
|
next: HTMLElement | null,
|
||||||
|
) => void,
|
||||||
|
) {
|
||||||
host.addController(this);
|
host.addController(this);
|
||||||
host[ATTACHABLE_CONTROLLER] = this;
|
host[ATTACHABLE_CONTROLLER] = this;
|
||||||
FOR_ATTRIBUTE_OBSERVER?.observe(host, {attributeFilter: ['for']});
|
FOR_ATTRIBUTE_OBSERVER?.observe(host, {attributeFilter: ['for']});
|
||||||
|
@ -6,14 +6,19 @@
|
|||||||
|
|
||||||
// import 'jasmine'; (google3-only)
|
// import 'jasmine'; (google3-only)
|
||||||
|
|
||||||
import {dispatchActivationClick, isActivationClick, redispatchEvent} from './events.js';
|
import {
|
||||||
|
dispatchActivationClick,
|
||||||
|
isActivationClick,
|
||||||
|
redispatchEvent,
|
||||||
|
} from './events.js';
|
||||||
|
|
||||||
describe('events', () => {
|
describe('events', () => {
|
||||||
let instance: HTMLDivElement;
|
let instance: HTMLDivElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
instance = document.createElement('div');
|
instance = document.createElement('div');
|
||||||
instance.attachShadow({mode: 'open'})
|
instance
|
||||||
|
.attachShadow({mode: 'open'})
|
||||||
.append(document.createElement('slot'));
|
.append(document.createElement('slot'));
|
||||||
// To have event.target set correctly, the EventTarget instance must be
|
// To have event.target set correctly, the EventTarget instance must be
|
||||||
// attached to the DOM.
|
// attached to the DOM.
|
||||||
@ -38,7 +43,8 @@ describe('events', () => {
|
|||||||
.not.toBe(event);
|
.not.toBe(event);
|
||||||
expect(redispatchedEvent.target)
|
expect(redispatchedEvent.target)
|
||||||
.withContext(
|
.withContext(
|
||||||
'target should be the instance that redispatched the event')
|
'target should be the instance that redispatched the event',
|
||||||
|
)
|
||||||
.toBe(instance);
|
.toBe(instance);
|
||||||
expect(redispatchedEvent.type)
|
expect(redispatchedEvent.type)
|
||||||
.withContext('should be the same event type')
|
.withContext('should be the same event type')
|
||||||
@ -77,8 +83,9 @@ describe('events', () => {
|
|||||||
|
|
||||||
it('should preventDefault() on the original event if canceled', () => {
|
it('should preventDefault() on the original event if canceled', () => {
|
||||||
const event = new Event('foo', {cancelable: true});
|
const event = new Event('foo', {cancelable: true});
|
||||||
const fooHandler =
|
const fooHandler = jasmine
|
||||||
jasmine.createSpy('fooHandler').and.callFake((event: Event) => {
|
.createSpy('fooHandler')
|
||||||
|
.and.callFake((event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
instance.addEventListener('foo', fooHandler);
|
instance.addEventListener('foo', fooHandler);
|
||||||
@ -119,7 +126,8 @@ describe('events', () => {
|
|||||||
listener.and.callThrough();
|
listener.and.callThrough();
|
||||||
instance.addEventListener('click', listener);
|
instance.addEventListener('click', listener);
|
||||||
instance.dispatchEvent(
|
instance.dispatchEvent(
|
||||||
new MouseEvent('click', {bubbles: true, composed: true}));
|
new MouseEvent('click', {bubbles: true, composed: true}),
|
||||||
|
);
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
expect(listener.calls.mostRecent().returnValue).toBe(true);
|
expect(listener.calls.mostRecent().returnValue).toBe(true);
|
||||||
});
|
});
|
||||||
@ -131,7 +139,8 @@ describe('events', () => {
|
|||||||
const innerEl = document.createElement('div');
|
const innerEl = document.createElement('div');
|
||||||
instance.shadowRoot!.append(innerEl);
|
instance.shadowRoot!.append(innerEl);
|
||||||
innerEl.dispatchEvent(
|
innerEl.dispatchEvent(
|
||||||
new MouseEvent('click', {bubbles: true, composed: true}));
|
new MouseEvent('click', {bubbles: true, composed: true}),
|
||||||
|
);
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
expect(listener.calls.mostRecent().returnValue).toBe(false);
|
expect(listener.calls.mostRecent().returnValue).toBe(false);
|
||||||
});
|
});
|
||||||
@ -144,7 +153,8 @@ describe('events', () => {
|
|||||||
instance.append(slottedEl);
|
instance.append(slottedEl);
|
||||||
|
|
||||||
slottedEl.dispatchEvent(
|
slottedEl.dispatchEvent(
|
||||||
new MouseEvent('click', {bubbles: true, composed: true}));
|
new MouseEvent('click', {bubbles: true, composed: true}),
|
||||||
|
);
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
expect(listener.calls.mostRecent().returnValue).toBe(false);
|
expect(listener.calls.mostRecent().returnValue).toBe(false);
|
||||||
});
|
});
|
||||||
@ -162,8 +172,7 @@ describe('events', () => {
|
|||||||
expect(listener).toHaveBeenCalledTimes(2);
|
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');
|
const innerEl = document.createElement('div');
|
||||||
instance.shadowRoot!.append(innerEl);
|
instance.shadowRoot!.append(innerEl);
|
||||||
const listener = jasmine.createSpy('listener');
|
const listener = jasmine.createSpy('listener');
|
||||||
|
@ -51,7 +51,8 @@ export interface FormSubmitter extends ReactiveElement, WithInternals {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FormSubmitterConstructor =
|
type FormSubmitterConstructor =
|
||||||
(new () => FormSubmitter)|(abstract new () => FormSubmitter);
|
| (new () => FormSubmitter)
|
||||||
|
| (abstract new () => FormSubmitter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up an element's constructor to enable form submission. The element
|
* Sets up an element's constructor to enable form submission. The element
|
||||||
@ -82,9 +83,9 @@ export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(ctor as unknown as typeof ReactiveElement).addInitializer(instance => {
|
(ctor as unknown as typeof ReactiveElement).addInitializer((instance) => {
|
||||||
const submitter = instance as FormSubmitter;
|
const submitter = instance as FormSubmitter;
|
||||||
submitter.addEventListener('click', async event => {
|
submitter.addEventListener('click', async (event) => {
|
||||||
const {type, [internals]: elementInternals} = submitter;
|
const {type, [internals]: elementInternals} = submitter;
|
||||||
const {form} = elementInternals;
|
const {form} = elementInternals;
|
||||||
if (!form || type === 'button') {
|
if (!form || type === 'button') {
|
||||||
@ -92,7 +93,7 @@ export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait a microtask for event bubbling to complete.
|
// Wait a microtask for event bubbling to complete.
|
||||||
await new Promise<void>(resolve => {
|
await new Promise<void>((resolve) => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -109,13 +110,17 @@ export function setupFormSubmitter(ctor: FormSubmitterConstructor) {
|
|||||||
// elements. This patches the dispatched submit event to add the correct
|
// elements. This patches the dispatched submit event to add the correct
|
||||||
// `submitter`.
|
// `submitter`.
|
||||||
// See https://github.com/WICG/webcomponents/issues/814
|
// See https://github.com/WICG/webcomponents/issues/814
|
||||||
form.addEventListener('submit', submitEvent => {
|
form.addEventListener(
|
||||||
|
'submit',
|
||||||
|
(submitEvent) => {
|
||||||
Object.defineProperty(submitEvent, 'submitter', {
|
Object.defineProperty(submitEvent, 'submitter', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
get: () => submitter,
|
get: () => submitter,
|
||||||
});
|
});
|
||||||
}, {capture: true, once: true});
|
},
|
||||||
|
{capture: true, once: true},
|
||||||
|
);
|
||||||
|
|
||||||
elementInternals.setFormValue(submitter.value);
|
elementInternals.setFormValue(submitter.value);
|
||||||
form.requestSubmit();
|
form.requestSubmit();
|
||||||
|
@ -36,13 +36,15 @@ class FormSubmitterButton extends LitElement {
|
|||||||
[internals] = this.attachInternals();
|
[internals] = this.attachInternals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
describe('setupFormSubmitter()', () => {
|
describe('setupFormSubmitter()', () => {
|
||||||
const env = new Environment();
|
const env = new Environment();
|
||||||
|
|
||||||
async function setupTest() {
|
async function setupTest() {
|
||||||
const root = env.render(
|
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');
|
const submitter = root.querySelector('test-form-submitter-button');
|
||||||
if (!submitter) {
|
if (!submitter) {
|
||||||
throw new Error(`Could not query rendered <test-form-submitter-button>`);
|
throw new Error(`Could not query rendered <test-form-submitter-button>`);
|
||||||
@ -92,9 +94,13 @@ describe('setupFormSubmitter()', () => {
|
|||||||
|
|
||||||
spyOn(form, 'requestSubmit');
|
spyOn(form, 'requestSubmit');
|
||||||
|
|
||||||
harness.element.addEventListener('click', (event: Event) => {
|
harness.element.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, {once: true});
|
},
|
||||||
|
{once: true},
|
||||||
|
);
|
||||||
|
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
|
|
||||||
@ -103,8 +109,9 @@ describe('setupFormSubmitter()', () => {
|
|||||||
|
|
||||||
it('should set the button as the SubmitEvent submitter', async () => {
|
it('should set the button as the SubmitEvent submitter', async () => {
|
||||||
const {harness, form} = await setupTest();
|
const {harness, form} = await setupTest();
|
||||||
const submitListener =
|
const submitListener = jasmine
|
||||||
jasmine.createSpy('submitListener').and.callFake((event: Event) => {
|
.createSpy('submitListener')
|
||||||
|
.and.callFake((event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -121,7 +128,7 @@ describe('setupFormSubmitter()', () => {
|
|||||||
|
|
||||||
it('should add name/value to form data when present', async () => {
|
it('should add name/value to form data when present', async () => {
|
||||||
const {harness, form} = await setupTest();
|
const {harness, form} = await setupTest();
|
||||||
form.addEventListener('submit', event => {
|
form.addEventListener('submit', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
* this parameter can be used as a conditional guard. Defaults to `true`.
|
* this parameter can be used as a conditional guard. Defaults to `true`.
|
||||||
*/
|
*/
|
||||||
export function isRtl(el: HTMLElement, shouldCheck = true) {
|
export function isRtl(el: HTMLElement, shouldCheck = true) {
|
||||||
return shouldCheck &&
|
return (
|
||||||
getComputedStyle(el).getPropertyValue('direction').trim() === 'rtl';
|
shouldCheck &&
|
||||||
|
getComputedStyle(el).getPropertyValue('direction').trim() === 'rtl'
|
||||||
|
);
|
||||||
}
|
}
|
@ -10,5 +10,5 @@ export const stringConverter = {
|
|||||||
},
|
},
|
||||||
toAttribute(value: string): string | null {
|
toAttribute(value: string): string | null {
|
||||||
return value || null;
|
return value || null;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
@ -98,10 +98,12 @@ export function createAnimationSignal(): AnimationSignal {
|
|||||||
export function createThrottle() {
|
export function createThrottle() {
|
||||||
const stack = new Set();
|
const stack = new Set();
|
||||||
return async (
|
return async (
|
||||||
key = '', cb: (...args: unknown[]) => unknown,
|
key = '',
|
||||||
|
cb: (...args: unknown[]) => unknown,
|
||||||
timeout = async () => {
|
timeout = async () => {
|
||||||
await new Promise(requestAnimationFrame);
|
await new Promise(requestAnimationFrame);
|
||||||
}) => {
|
},
|
||||||
|
) => {
|
||||||
if (!stack.has(key)) {
|
if (!stack.has(key)) {
|
||||||
stack.add(key);
|
stack.add(key);
|
||||||
await timeout();
|
await timeout();
|
||||||
|
@ -5,7 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// import 'jasmine'; (google3-only)
|
// import 'jasmine'; (google3-only)
|
||||||
import {AnimationSignal, createAnimationSignal, createThrottle, msFromTimeCSSValue} from './animation.js';
|
import {
|
||||||
|
AnimationSignal,
|
||||||
|
createAnimationSignal,
|
||||||
|
createThrottle,
|
||||||
|
msFromTimeCSSValue,
|
||||||
|
} from './animation.js';
|
||||||
|
|
||||||
describe('createAnimationSignal()', () => {
|
describe('createAnimationSignal()', () => {
|
||||||
let task: AnimationSignal;
|
let task: AnimationSignal;
|
||||||
@ -55,14 +60,13 @@ describe('createAnimationSignal()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('createThrottle()', () => {
|
describe('createThrottle()', () => {
|
||||||
it('throttles calls', async () => {
|
it('throttles calls', async () => {
|
||||||
const throttle = createThrottle();
|
const throttle = createThrottle();
|
||||||
const key = 'foo';
|
const key = 'foo';
|
||||||
const fn = jasmine.createSpy();
|
const fn = jasmine.createSpy();
|
||||||
let timeoutResolver: Function | null = null;
|
let timeoutResolver: Function | null = null;
|
||||||
const timeout = new Promise(r => {
|
const timeout = new Promise((r) => {
|
||||||
timeoutResolver = r;
|
timeoutResolver = r;
|
||||||
});
|
});
|
||||||
const timeoutFn = async () => {
|
const timeoutFn = async () => {
|
||||||
|
@ -47,8 +47,9 @@ const updateTabIndex = Symbol('updateTabIndex');
|
|||||||
* @param base The class to mix functionality into.
|
* @param base The class to mix functionality into.
|
||||||
* @return The provided class with `Focusable` mixed in.
|
* @return The provided class with `Focusable` mixed in.
|
||||||
*/
|
*/
|
||||||
export function mixinFocusable<T extends MixinBase<LitElement>>(base: T):
|
export function mixinFocusable<T extends MixinBase<LitElement>>(
|
||||||
MixinReturn<T, Focusable> {
|
base: T,
|
||||||
|
): MixinReturn<T, Focusable> {
|
||||||
abstract class FocusableElement extends base implements Focusable {
|
abstract class FocusableElement extends base implements Focusable {
|
||||||
@property({reflect: true}) declare tabIndex: number;
|
@property({reflect: true}) declare tabIndex: number;
|
||||||
|
|
||||||
@ -76,7 +77,10 @@ export function mixinFocusable<T extends MixinBase<LitElement>>(base: T):
|
|||||||
}
|
}
|
||||||
|
|
||||||
override attributeChangedCallback(
|
override attributeChangedCallback(
|
||||||
name: string, old: string|null, value: string|null) {
|
name: string,
|
||||||
|
old: string | null,
|
||||||
|
value: string | null,
|
||||||
|
) {
|
||||||
super.attributeChangedCallback(name, old, value);
|
super.attributeChangedCallback(name, old, value);
|
||||||
if (name !== 'tabindex' || this[isUpdatingTabIndex]) {
|
if (name !== 'tabindex' || this[isUpdatingTabIndex]) {
|
||||||
return;
|
return;
|
||||||
|
@ -17,8 +17,7 @@ describe('mixinFocusable()', () => {
|
|||||||
// tslint:disable-next-line:enforce-name-casing MixinClassCase
|
// tslint:disable-next-line:enforce-name-casing MixinClassCase
|
||||||
const FocusableLitElement = mixinFocusable(LitElement);
|
const FocusableLitElement = mixinFocusable(LitElement);
|
||||||
@customElement('test-focusable')
|
@customElement('test-focusable')
|
||||||
class TestFocusable extends FocusableLitElement {
|
class TestFocusable extends FocusableLitElement {}
|
||||||
}
|
|
||||||
|
|
||||||
const env = new Environment();
|
const env = new Environment();
|
||||||
|
|
||||||
@ -53,8 +52,7 @@ describe('mixinFocusable()', () => {
|
|||||||
expect(element.requestUpdate).toHaveBeenCalled();
|
expect(element.requestUpdate).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not override user-set tabindex="0" when isFocusable is false',
|
it('should not override user-set tabindex="0" when isFocusable is false', async () => {
|
||||||
async () => {
|
|
||||||
const element = await setupTest();
|
const element = await setupTest();
|
||||||
element[isFocusable] = false;
|
element[isFocusable] = false;
|
||||||
element.tabIndex = 0;
|
element.tabIndex = 0;
|
||||||
@ -62,16 +60,14 @@ describe('mixinFocusable()', () => {
|
|||||||
expect(element.tabIndex).withContext('tabIndex').toBe(0);
|
expect(element.tabIndex).withContext('tabIndex').toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not override user-set tabindex="-1" when isFocusable is true',
|
it('should not override user-set tabindex="-1" when isFocusable is true', async () => {
|
||||||
async () => {
|
|
||||||
const element = await setupTest();
|
const element = await setupTest();
|
||||||
element.tabIndex = -1;
|
element.tabIndex = -1;
|
||||||
expect(element[isFocusable]).withContext('isFocusable').toBeTrue();
|
expect(element[isFocusable]).withContext('isFocusable').toBeTrue();
|
||||||
expect(element.tabIndex).withContext('tabIndex').toBe(-1);
|
expect(element.tabIndex).withContext('tabIndex').toBe(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should restore default tabindex when user-set tabindex attribute is removed',
|
it('should restore default tabindex when user-set tabindex attribute is removed', async () => {
|
||||||
async () => {
|
|
||||||
const element = await setupTest();
|
const element = await setupTest();
|
||||||
element.tabIndex = -1;
|
element.tabIndex = -1;
|
||||||
element.removeAttribute('tabindex');
|
element.removeAttribute('tabindex');
|
||||||
|
@ -26,10 +26,11 @@
|
|||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
export type MixinBase<ExpectedBase = object> = abstract new (
|
||||||
// Mixins must have a constructor with `...args: any[]`
|
// Mixins must have a constructor with `...args: any[]`
|
||||||
// tslint:disable-next-line:no-any
|
// tslint:disable-next-line:no-any
|
||||||
export type MixinBase<ExpectedBase = object> = abstract new (...args: any[]) =>
|
...args: any[]
|
||||||
ExpectedBase;
|
) => ExpectedBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The return value of a mixin.
|
* 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> =
|
export type MixinReturn<MixinBase, MixinClass = object> =
|
||||||
// Mixins must have a constructor with `...args: any[]`
|
// Mixins must have a constructor with `...args: any[]`
|
||||||
// tslint:disable-next-line:no-any
|
// tslint:disable-next-line:no-any
|
||||||
|
@ -4,21 +4,28 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob, textInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Item', [
|
'Item',
|
||||||
|
[
|
||||||
new Knob('overline', {ui: textInput()}),
|
new Knob('overline', {ui: textInput()}),
|
||||||
new Knob('trailingSupportingText', {ui: textInput()}),
|
new Knob('trailingSupportingText', {ui: textInput()}),
|
||||||
new Knob('leadingIcon', {ui: boolInput()}),
|
new Knob('leadingIcon', {ui: boolInput()}),
|
||||||
new Knob('trailingIcon', {ui: boolInput()}),
|
new Knob('trailingIcon', {ui: boolInput()}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
collection.addStories(...materialInitsToStoryInits(stories));
|
||||||
|
|
||||||
|
@ -64,10 +64,7 @@ const items: MaterialStoryInit<StoryKnobs> = {
|
|||||||
render(knobs) {
|
render(knobs) {
|
||||||
return html`
|
return html`
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<md-item>
|
<md-item> Single line item ${getKnobContent(knobs)} </md-item>
|
||||||
Single line item
|
|
||||||
${getKnobContent(knobs)}
|
|
||||||
</md-item>
|
|
||||||
|
|
||||||
<md-item>
|
<md-item>
|
||||||
Two line item
|
Two line item
|
||||||
@ -85,7 +82,7 @@ const items: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</md-item>
|
</md-item>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const longText: MaterialStoryInit<StoryKnobs> = {
|
const longText: MaterialStoryInit<StoryKnobs> = {
|
||||||
@ -96,9 +93,7 @@ const longText: MaterialStoryInit<StoryKnobs> = {
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<md-item class="nowrap">
|
<md-item class="nowrap">
|
||||||
Item with a truncated headline and supporting text.
|
Item with a truncated headline and supporting text.
|
||||||
<div slot="supporting-text">
|
<div slot="supporting-text"> Supporting text. ${LOREM_IPSUM} </div>
|
||||||
Supporting text. ${LOREM_IPSUM}
|
|
||||||
</div>
|
|
||||||
${getKnobContent(knobs)}
|
${getKnobContent(knobs)}
|
||||||
</md-item>
|
</md-item>
|
||||||
|
|
||||||
@ -112,37 +107,36 @@ const longText: MaterialStoryInit<StoryKnobs> = {
|
|||||||
|
|
||||||
<md-item>
|
<md-item>
|
||||||
Item that always shows long wrapping text.
|
Item that always shows long wrapping text.
|
||||||
<div slot="supporting-text">
|
<div slot="supporting-text"> Supporting text. ${LOREM_IPSUM} </div>
|
||||||
Supporting text. ${LOREM_IPSUM}
|
|
||||||
</div>
|
|
||||||
${getKnobContent(knobs, /* threeLines */ true)}
|
${getKnobContent(knobs, /* threeLines */ true)}
|
||||||
</md-item>
|
</md-item>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getKnobContent(knobs: StoryKnobs, threeLines = false) {
|
function getKnobContent(knobs: StoryKnobs, threeLines = false) {
|
||||||
const overline = knobs.overline ?
|
const overline = knobs.overline
|
||||||
html`<div slot="overline">${knobs.overline}</div>` :
|
? html`<div slot="overline">${knobs.overline}</div>`
|
||||||
nothing;
|
: nothing;
|
||||||
|
|
||||||
const classes = {
|
const classes = {
|
||||||
'align-start': threeLines,
|
'align-start': threeLines,
|
||||||
};
|
};
|
||||||
|
|
||||||
const trailingText = knobs.trailingSupportingText ?
|
const trailingText = knobs.trailingSupportingText
|
||||||
html`<div class=${classMap(classes)} slot="trailing-supporting-text">${
|
? html`<div class=${classMap(classes)} slot="trailing-supporting-text"
|
||||||
knobs.trailingSupportingText}</div>` :
|
>${knobs.trailingSupportingText}</div
|
||||||
nothing;
|
>`
|
||||||
|
: nothing;
|
||||||
|
|
||||||
const leadingIcon = knobs.leadingIcon ?
|
const leadingIcon = knobs.leadingIcon
|
||||||
html`<md-icon class=${classMap(classes)} slot="start">event</md-icon>` :
|
? html`<md-icon class=${classMap(classes)} slot="start">event</md-icon>`
|
||||||
nothing;
|
: nothing;
|
||||||
|
|
||||||
const trailingIcon = knobs.trailingIcon ?
|
const trailingIcon = knobs.trailingIcon
|
||||||
html`<md-icon class=${classMap(classes)} slot="end">star</md-icon>` :
|
? html`<md-icon class=${classMap(classes)} slot="end">star</md-icon>`
|
||||||
nothing;
|
: nothing;
|
||||||
|
|
||||||
return [overline, trailingText, leadingIcon, trailingIcon];
|
return [overline, trailingText, leadingIcon, trailingIcon];
|
||||||
}
|
}
|
||||||
|
@ -27,13 +27,13 @@ export class Item extends LitElement {
|
|||||||
<slot name="container"></slot>
|
<slot name="container"></slot>
|
||||||
<slot class="non-text" name="start"></slot>
|
<slot class="non-text" name="start"></slot>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
<slot name="overline"
|
<slot name="overline" @slotchange=${this.handleTextSlotChange}></slot>
|
||||||
|
<slot
|
||||||
|
class="default-slot"
|
||||||
@slotchange=${this.handleTextSlotChange}></slot>
|
@slotchange=${this.handleTextSlotChange}></slot>
|
||||||
<slot class="default-slot"
|
<slot name="headline" @slotchange=${this.handleTextSlotChange}></slot>
|
||||||
@slotchange=${this.handleTextSlotChange}></slot>
|
<slot
|
||||||
<slot name="headline"
|
name="supporting-text"
|
||||||
@slotchange=${this.handleTextSlotChange}></slot>
|
|
||||||
<slot name="supporting-text"
|
|
||||||
@slotchange=${this.handleTextSlotChange}></slot>
|
@slotchange=${this.handleTextSlotChange}></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot class="non-text" name="trailing-supporting-text"></slot>
|
<slot class="non-text" name="trailing-supporting-text"></slot>
|
||||||
|
@ -4,23 +4,30 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.js';
|
|
||||||
import './material-collection.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 {boolInput, Knob, textInput} from './index.js';
|
||||||
|
|
||||||
import {stories, StoryKnobs} from './stories.js';
|
import {stories, StoryKnobs} from './stories.js';
|
||||||
|
|
||||||
const collection =
|
const collection = new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>(
|
||||||
new MaterialCollection<KnobTypesToKnobs<StoryKnobs>>('Navigation Bar', [
|
'Navigation Bar',
|
||||||
|
[
|
||||||
new Knob('hideInactiveLabels', {ui: boolInput(), defaultValue: false}),
|
new Knob('hideInactiveLabels', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
|
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
|
||||||
new Knob('showBadge', {ui: boolInput(), defaultValue: false}),
|
new Knob('showBadge', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('badgeValue', {ui: textInput(), defaultValue: ''}),
|
new Knob('badgeValue', {ui: textInput(), defaultValue: ''}),
|
||||||
new Knob('active icon', {ui: textInput(), defaultValue: 'star'}),
|
new Knob('active icon', {ui: textInput(), defaultValue: 'star'}),
|
||||||
new Knob('inactive icon', {ui: textInput(), defaultValue: 'star_border'}),
|
new Knob('inactive icon', {ui: textInput(), defaultValue: 'star_border'}),
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
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 '@material/web/labs/navigationtab/navigation-tab.js';
|
||||||
|
|
||||||
import {MaterialStoryInit} from './material-collection.js';
|
import {MaterialStoryInit} from './material-collection.js';
|
||||||
import {html, css} from 'lit';
|
import {css, html} from 'lit';
|
||||||
|
|
||||||
/** Knob types for nav bar stories. */
|
/** Knob types for nav bar stories. */
|
||||||
export interface StoryKnobs {
|
export interface StoryKnobs {
|
||||||
@ -27,11 +27,11 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
:host {
|
:host {
|
||||||
/* Material Symbols does not include filled star. */
|
/* Material Symbols does not include filled star. */
|
||||||
--md-icon-font: 'Material Icons';
|
--md-icon-font: 'Material Icons';
|
||||||
}`,
|
}
|
||||||
|
`,
|
||||||
render(knobs) {
|
render(knobs) {
|
||||||
const {hideInactiveLabels, label, showBadge, badgeValue} = knobs;
|
const {hideInactiveLabels, label, showBadge, badgeValue} = knobs;
|
||||||
return html`
|
return html` <div style="width:400px">
|
||||||
<div style="width:400px">
|
|
||||||
<md-navigation-bar
|
<md-navigation-bar
|
||||||
activeIndex="1"
|
activeIndex="1"
|
||||||
.hideInactiveLabels=${hideInactiveLabels}>
|
.hideInactiveLabels=${hideInactiveLabels}>
|
||||||
@ -68,7 +68,7 @@ const standard: MaterialStoryInit<StoryKnobs> = {
|
|||||||
</md-navigation-tab>
|
</md-navigation-tab>
|
||||||
</md-navigation-bar>
|
</md-navigation-bar>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Nav Bar stories. */
|
/** 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