mirror of
https://github.com/material-components/material-web.git
synced 2024-08-16 17:10:22 +03:00
chore: format files with prettier
PiperOrigin-RevId: 576601342
This commit is contained in:
parent
d09bdc47e6
commit
c390291687
@ -4,19 +4,26 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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('disabled', {ui: boolInput(), defaultValue: false}),
|
new Knob('label', {ui: textInput(), defaultValue: ''}),
|
||||||
]);
|
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.
|
||||||
@ -48,7 +57,7 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
|||||||
* Where to display the linked `href` URL for a link button. Common options
|
* Where to display the linked `href` URL for a link button. Common options
|
||||||
* include `_blank` to open in a new tab.
|
* include `_blank` to open in a new tab.
|
||||||
*/
|
*/
|
||||||
@property() target: '_blank'|'_parent'|'_self'|'_top'|'' = '';
|
@property() target: '_blank' | '_parent' | '_self' | '_top' | '' = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to render the icon at the inline end of the label rather than the
|
* Whether to render the icon at the inline end of the label rather than the
|
||||||
@ -81,14 +90,14 @@ export abstract class Button extends LitElement implements FormSubmitter {
|
|||||||
return this[internals].form;
|
return this[internals].form;
|
||||||
}
|
}
|
||||||
|
|
||||||
@query('.button') private readonly buttonElement!: HTMLElement|null;
|
@query('.button') private readonly buttonElement!: HTMLElement | null;
|
||||||
|
|
||||||
@queryAssignedElements({slot: 'icon', flatten: true})
|
@queryAssignedElements({slot: 'icon', flatten: true})
|
||||||
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();
|
||||||
|
@ -57,4 +57,4 @@ export class CatalogComponentHeaderTitle extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -17,13 +17,10 @@ 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 class="image"></slot>
|
||||||
<slot
|
</div>`;
|
||||||
class="image"
|
|
||||||
></slot>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
|
@ -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
|
||||||
@ -37,7 +43,7 @@ export class CopyCodeButton extends LitElement {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
private timeoutId: number|undefined;
|
private timeoutId: number | undefined;
|
||||||
|
|
||||||
@state() private showCheckmark = false;
|
@state() private showCheckmark = false;
|
||||||
|
|
||||||
@ -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>
|
||||||
|
@ -48,7 +48,7 @@ export class HCTSlider extends LitElement {
|
|||||||
/**
|
/**
|
||||||
* The type of HCT slider to display
|
* The type of HCT slider to display
|
||||||
*/
|
*/
|
||||||
@property({type: String}) type: 'hue'|'chroma'|'tone' = 'hue';
|
@property({type: String}) type: 'hue' | 'chroma' | 'tone' = 'hue';
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
let range = HUE_RANGE;
|
let range = HUE_RANGE;
|
||||||
@ -62,21 +62,19 @@ export class HCTSlider extends LitElement {
|
|||||||
return html`<section>
|
return html`<section>
|
||||||
<span id="label" class="color-on-surface-text">${this.label}</span>
|
<span id="label" class="color-on-surface-text">${this.label}</span>
|
||||||
<md-slider
|
<md-slider
|
||||||
id="source"
|
id="source"
|
||||||
labeled
|
labeled
|
||||||
aria-label=${this.label}
|
aria-label=${this.label}
|
||||||
.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,18 +76,16 @@ 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({
|
properties: ['opacity'],
|
||||||
properties: ['opacity'],
|
keyframeOptions: {
|
||||||
keyframeOptions: {
|
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';
|
||||||
|
|
||||||
@ -37,7 +41,7 @@ export class ThemeChanger extends LitElement {
|
|||||||
/**
|
/**
|
||||||
* The currently selected color mode.
|
* The currently selected color mode.
|
||||||
*/
|
*/
|
||||||
@state() selectedColorMode: ColorMode|null = null;
|
@state() selectedColorMode: ColorMode | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The currently selected hex color.
|
* The currently selected hex color.
|
||||||
@ -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>
|
||||||
@ -111,27 +111,24 @@ export class ThemeChanger extends LitElement {
|
|||||||
private renderHctPicker() {
|
private renderHctPicker() {
|
||||||
return html`<div class="sliders">
|
return html`<div class="sliders">
|
||||||
<hct-slider
|
<hct-slider
|
||||||
.value=${live(this.hue)}
|
.value=${live(this.hue)}
|
||||||
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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,9 +137,8 @@ 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>
|
||||||
|
@ -8,4 +8,4 @@ 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/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';
|
||||||
|
@ -4,4 +4,4 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@material/web/checkbox/checkbox.js';
|
import '@material/web/checkbox/checkbox.js';
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
* 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/fab/branded-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';
|
||||||
|
@ -7,4 +7,4 @@
|
|||||||
import '@material/web/button/filled-button.js';
|
import '@material/web/button/filled-button.js';
|
||||||
import '@material/web/menu/menu.js';
|
import '@material/web/menu/menu.js';
|
||||||
import '@material/web/menu/menu-item.js';
|
import '@material/web/menu/menu-item.js';
|
||||||
import '@material/web/menu/sub-menu.js';
|
import '@material/web/menu/sub-menu.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-file-editor.js';
|
||||||
import 'playground-elements/playground-preview.js';
|
import 'playground-elements/playground-preview.js';
|
||||||
import 'playground-elements/playground-file-editor.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,14 +38,15 @@ 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
|
||||||
.addEventListener('change', () => {
|
.matchMedia('(prefers-color-scheme: dark)')
|
||||||
if (getCurrentMode() !== 'auto') {
|
.addEventListener('change', () => {
|
||||||
return;
|
if (getCurrentMode() !== 'auto') {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
changeColor(getCurrentSeedColor()!);
|
changeColor(getCurrentSeedColor()!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,4 +9,4 @@ import {signal} from './signal-element.js';
|
|||||||
/**
|
/**
|
||||||
* Whether or not the sidebar drawer should be open.
|
* Whether or not the sidebar drawer should be open.
|
||||||
*/
|
*/
|
||||||
export const drawerOpenSignal = signal(false);
|
export const drawerOpenSignal = signal(false);
|
||||||
|
@ -15,4 +15,4 @@ export const inertContentSignal = signal(false);
|
|||||||
/**
|
/**
|
||||||
* Whether or not the sidebar should be inert.
|
* Whether or not the sidebar should be inert.
|
||||||
*/
|
*/
|
||||||
export const inertSidebarSignal = signal(false);
|
export const inertSidebarSignal = signal(false);
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -39,4 +40,4 @@ export function SignalElement<T extends ReactiveElementConstructor>(Base: T):
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -10,4 +10,4 @@ import {hydrateShadowRoots} from '@webcomponents/template-shadowroot/template-sh
|
|||||||
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
|
if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot')) {
|
||||||
hydrateShadowRoots(document.body);
|
hydrateShadowRoots(document.body);
|
||||||
}
|
}
|
||||||
document.body.removeAttribute('dsd-pending');
|
document.body.removeAttribute('dsd-pending');
|
||||||
|
@ -6,4 +6,4 @@
|
|||||||
|
|
||||||
// Must be loaded before any lit element see /site/_includes/default.html for
|
// Must be loaded before any lit element see /site/_includes/default.html for
|
||||||
// usage. Allows hydrating SSRd lit elements.
|
// usage. Allows hydrating SSRd lit elements.
|
||||||
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
|
import '@lit-labs/ssr-client/lit-element-hydrate-support.js';
|
||||||
|
@ -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(
|
||||||
// Removes the feature in which 11ty island removes DOM to render a fallback.
|
'lit-island',
|
||||||
override forceFallback() {}
|
class extends Island {
|
||||||
});
|
// Removes the feature in which 11ty island removes DOM to render a fallback.
|
||||||
|
override forceFallback() {}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
|
|
||||||
// This file imports only files that will be SSRd e.g. if you can't SSR a
|
// 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
|
||||||
|
@ -32,7 +32,7 @@ export class ChangeDarkModeEvent extends Event {
|
|||||||
/**
|
/**
|
||||||
* @param mode The new color mode to apply.
|
* @param mode The new color mode to apply.
|
||||||
*/
|
*/
|
||||||
constructor(public mode: 'light'|'dark'|'auto') {
|
constructor(public mode: 'light' | 'dark' | 'auto') {
|
||||||
super('change-mode', {bubbles: true, composed: true});
|
super('change-mode', {bubbles: true, composed: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
catalog/src/types/is-land.d.ts
vendored
2
catalog/src/types/is-land.d.ts
vendored
@ -8,4 +8,4 @@ declare module '@11ty/is-land' {
|
|||||||
export class Island extends HTMLElement {
|
export class Island extends HTMLElement {
|
||||||
forceFallback(): void;
|
forceFallback(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,11 +38,13 @@ 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
|
||||||
?.setAttribute('content', surfaceContainer);
|
.querySelector('meta[name="theme-color"]')
|
||||||
|
?.setAttribute('content', surfaceContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
sheet.replaceSync(themeString);
|
sheet.replaceSync(themeString);
|
||||||
|
@ -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,9 +4,13 @@
|
|||||||
* 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';
|
||||||
|
|
||||||
export type ColorMode = 'light'|'dark'|'auto';
|
/** Color mode, either overriding light/dark or the user's preference. */
|
||||||
|
export type ColorMode = 'light' | 'dark' | 'auto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a Material Theme from a given color and dark mode boolean, and
|
* Generates a Material Theme from a given color and dark mode boolean, and
|
||||||
@ -56,7 +60,7 @@ export function isModeDark(mode: ColorMode, saveAutoMode = true) {
|
|||||||
*
|
*
|
||||||
* @return The current stringified material theme css string.
|
* @return The current stringified material theme css string.
|
||||||
*/
|
*/
|
||||||
export function getCurrentThemeString(): string|null {
|
export function getCurrentThemeString(): string | null {
|
||||||
return localStorage.getItem('material-theme');
|
return localStorage.getItem('material-theme');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +69,7 @@ export function getCurrentThemeString(): string|null {
|
|||||||
*
|
*
|
||||||
* @return The current color mode.
|
* @return The current color mode.
|
||||||
*/
|
*/
|
||||||
export function getCurrentMode(): ColorMode|null {
|
export function getCurrentMode(): ColorMode | null {
|
||||||
return localStorage.getItem('color-mode') as ColorMode | null;
|
return localStorage.getItem('color-mode') as ColorMode | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +87,7 @@ export function saveColorMode(mode: ColorMode) {
|
|||||||
*
|
*
|
||||||
* @return The current seed color.
|
* @return The current seed color.
|
||||||
*/
|
*/
|
||||||
export function getCurrentSeedColor(): string|null {
|
export function getCurrentSeedColor(): string | null {
|
||||||
return localStorage.getItem('seed-color');
|
return localStorage.getItem('seed-color');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,7 +118,7 @@ export function getLastSavedAutoColorMode() {
|
|||||||
* @param mode The last applied color mode while in "auto" to be saved to local
|
* @param mode The last applied color mode while in "auto" to be saved to local
|
||||||
* storage.
|
* storage.
|
||||||
*/
|
*/
|
||||||
export function saveLastSavedAutoColorMode(mode: 'light'|'dark') {
|
export function saveLastSavedAutoColorMode(mode: 'light' | 'dark') {
|
||||||
localStorage.setItem('last-auto-color-mode', mode);
|
localStorage.setItem('last-auto-color-mode', mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,9 +131,8 @@ export class KnobColorSelector extends LitElement {
|
|||||||
</span>
|
</span>
|
||||||
<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,15 +375,15 @@ 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. */
|
||||||
export function selectDropdown<T extends string>({
|
export function selectDropdown<T extends string>({
|
||||||
options,
|
options,
|
||||||
}: SelectDropdownConfig<T>): KnobUi<T|undefined> {
|
}: SelectDropdownConfig<T>): KnobUi<T | undefined> {
|
||||||
return {
|
return {
|
||||||
render(knob: Knob<T|undefined>, onChange: (val: T) => void) {
|
render(knob: Knob<T | undefined>, onChange: (val: T) => void) {
|
||||||
const valueChanged = (e: Event) => {
|
const valueChanged = (e: Event) => {
|
||||||
onChange((e.target as HTMLInputElement).value as T);
|
onChange((e.target as HTMLInputElement).value as T);
|
||||||
};
|
};
|
||||||
@ -391,15 +391,14 @@ 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>
|
||||||
<md-filled-select
|
<md-filled-select
|
||||||
@change="${valueChanged}"
|
@change="${valueChanged}"
|
||||||
menu-positioning="fixed"
|
menu-positioning="fixed"
|
||||||
style=${styleMap(sharedTextFieldStyles)}>
|
style=${styleMap(sharedTextFieldStyles)}>
|
||||||
${listItems}
|
${listItems}
|
||||||
</md-filled-select>
|
</md-filled-select>
|
||||||
${knob.name}
|
${knob.name}
|
||||||
@ -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);
|
||||||
|
@ -66,10 +66,10 @@ export class StoriesRenderer extends LitElement {
|
|||||||
@property({type: Boolean}) hideLabels = false;
|
@property({type: Boolean}) hideLabels = false;
|
||||||
@property({type: Boolean, reflect: true}) hasKnobs = false;
|
@property({type: Boolean, reflect: true}) hasKnobs = false;
|
||||||
@state() knobsOpen = true;
|
@state() knobsOpen = true;
|
||||||
@state() knobsPanelType: 'modal'|'inline' = 'inline';
|
@state() knobsPanelType: 'modal' | 'inline' = 'inline';
|
||||||
|
|
||||||
private observedKnobs: undefined|KnobValues<PolymorphicArrayOfKnobs> =
|
private observedKnobs: undefined | KnobValues<PolymorphicArrayOfKnobs> =
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const collection = this.collection;
|
const collection = this.collection;
|
||||||
@ -90,11 +90,12 @@ export class StoriesRenderer extends LitElement {
|
|||||||
|
|
||||||
private renderStories(stories: Story[]): TemplateResult[] {
|
private renderStories(stories: Story[]): TemplateResult[] {
|
||||||
return stories.map((story) => {
|
return stories.map((story) => {
|
||||||
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}
|
||||||
@ -113,7 +114,7 @@ export class StoriesRenderer extends LitElement {
|
|||||||
private renderKnobs(collection: Collection) {
|
private renderKnobs(collection: Collection) {
|
||||||
const knobs = collection.knobs;
|
const knobs = collection.knobs;
|
||||||
|
|
||||||
let knobsSection: string|TemplateResult = '';
|
let knobsSection: string | TemplateResult = '';
|
||||||
|
|
||||||
this.hasKnobs = !this.hideKnobs && !knobs.empty;
|
this.hasKnobs = !this.hideKnobs && !knobs.empty;
|
||||||
|
|
||||||
@ -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>
|
||||||
`;
|
`;
|
||||||
@ -163,7 +163,7 @@ export class StoriesRenderer extends LitElement {
|
|||||||
|
|
||||||
private updateObservedKnobs() {
|
private updateObservedKnobs() {
|
||||||
if (this.collection?.knobs === this.observedKnobs) {
|
if (this.collection?.knobs === this.observedKnobs) {
|
||||||
return; // nothing to do;
|
return; // nothing to do;
|
||||||
}
|
}
|
||||||
// Stop watching the knobs that we're currently observing.
|
// Stop watching the knobs that we're currently observing.
|
||||||
this.unobserveKnobs();
|
this.unobserveKnobs();
|
||||||
@ -183,8 +183,9 @@ export class StoriesRenderer extends LitElement {
|
|||||||
for (const story of this.focusStories) {
|
for (const story of this.focusStories) {
|
||||||
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';
|
||||||
@ -29,13 +29,13 @@ export const DEFAULT_DIMENSIONS = {
|
|||||||
*/
|
*/
|
||||||
@customElement('story-knob-panel')
|
@customElement('story-knob-panel')
|
||||||
export class StoryKnobPanel extends LitElement {
|
export class StoryKnobPanel extends LitElement {
|
||||||
@query('.dragBar') dragBar!: HTMLElement|null;
|
@query('.dragBar') dragBar!: HTMLElement | null;
|
||||||
|
|
||||||
@property({type: Boolean}) showCloseIcon = true;
|
@property({type: Boolean}) showCloseIcon = true;
|
||||||
@property({type: Boolean, reflect: true}) open = false;
|
@property({type: Boolean, reflect: true}) open = false;
|
||||||
@property({type: Boolean, reflect: true}) override draggable = false;
|
@property({type: Boolean, reflect: true}) override draggable = false;
|
||||||
@property({type: Boolean}) hideDragIcon = false;
|
@property({type: Boolean}) hideDragIcon = false;
|
||||||
@property({type: String, reflect: true}) type: 'modal'|'inline' = 'inline';
|
@property({type: String, reflect: true}) type: 'modal' | 'inline' = 'inline';
|
||||||
private isDragging = false;
|
private isDragging = false;
|
||||||
private previousX = 0;
|
private previousX = 0;
|
||||||
private currentX = 0;
|
private currentX = 0;
|
||||||
@ -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(
|
||||||
detail: {
|
new CustomEvent('open-changed', {
|
||||||
open: this.open,
|
detail: {
|
||||||
},
|
open: this.open,
|
||||||
}));
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,11 +181,12 @@ 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 =
|
||||||
(DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET);
|
window.innerHeight -
|
||||||
|
(DEFAULT_DIMENSIONS.DRAG_BAR_HEIGHT + DEFAULT_DIMENSIONS.TOP_OFFSET);
|
||||||
|
|
||||||
// do not allow drag outside right bound
|
// do not allow drag outside right bound
|
||||||
if (x > rightBound) {
|
if (x > rightBound) {
|
||||||
|
@ -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';
|
||||||
@ -24,7 +24,7 @@ export class StoryRenderer extends LitElement {
|
|||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@property({attribute: false}) story?: Story = undefined;
|
@property({attribute: false}) story?: Story = undefined;
|
||||||
private storyRenderComplete: Promise<void>|undefined = undefined;
|
private storyRenderComplete: Promise<void> | undefined = undefined;
|
||||||
|
|
||||||
override updated(propertiesChanged: PropertyValues) {
|
override updated(propertiesChanged: PropertyValues) {
|
||||||
super.updated(propertiesChanged);
|
super.updated(propertiesChanged);
|
||||||
|
@ -10,4 +10,4 @@ export * from './knobs.js';
|
|||||||
export * from './story.js';
|
export * from './story.js';
|
||||||
|
|
||||||
// This file is resolved by base.json
|
// This file is resolved by base.json
|
||||||
import './components/stories-renderer.js';
|
import './components/stories-renderer.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();
|
||||||
@ -80,25 +81,28 @@ export class Knob<T, Name extends string = string> extends EventTarget {
|
|||||||
* Connect the knob's wiring, if any, up to a container of a rendered story.
|
* Connect the knob's wiring, if any, up to a container of a rendered story.
|
||||||
* This is fast and idempotent, so it's fine to call frequently.
|
* This is fast and idempotent, so it's fine to call frequently.
|
||||||
*/
|
*/
|
||||||
connectWiring(containerOfRenderedStory: HTMLElement|DocumentFragment) {
|
connectWiring(containerOfRenderedStory: HTMLElement | DocumentFragment) {
|
||||||
// Fast path the common case where we have no wiring.
|
// Fast path the common case where we have no wiring.
|
||||||
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.latestValue !== undefined &&
|
this.dirty ||
|
||||||
this.latestValue === this.defaultValue)) {
|
(this.latestValue !== undefined &&
|
||||||
|
this.latestValue === this.defaultValue)
|
||||||
|
) {
|
||||||
this.wiring?.(this, this.latestValue!, containerOfRenderedStory);
|
this.wiring?.(this, this.latestValue!, containerOfRenderedStory);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectWiring(containerOfRenderedStory: HTMLElement|DocumentFragment) {
|
disconnectWiring(containerOfRenderedStory: HTMLElement | DocumentFragment) {
|
||||||
return this.renderedStoryContainers.delete(containerOfRenderedStory);
|
return this.renderedStoryContainers.delete(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}`);
|
||||||
@ -210,7 +223,7 @@ export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> extends
|
|||||||
* Unlikely that any code outside of the stories system internals would
|
* Unlikely that any code outside of the stories system internals would
|
||||||
* call this.
|
* call this.
|
||||||
*/
|
*/
|
||||||
connectWiring(container: HTMLElement|DocumentFragment) {
|
connectWiring(container: HTMLElement | DocumentFragment) {
|
||||||
for (const knob of this.byName.values()) {
|
for (const knob of this.byName.values()) {
|
||||||
if (container instanceof DocumentFragment) {
|
if (container instanceof DocumentFragment) {
|
||||||
container = container.firstElementChild as HTMLElement;
|
container = container.firstElementChild as HTMLElement;
|
||||||
@ -225,7 +238,7 @@ export class KnobValues<Knobs extends PolymorphicArrayOfKnobs> extends
|
|||||||
*
|
*
|
||||||
* Returns false if the container wasn't actually connected.
|
* Returns false if the container wasn't actually connected.
|
||||||
*/
|
*/
|
||||||
disconnectWiring(container: HTMLElement|DocumentFragment) {
|
disconnectWiring(container: HTMLElement | DocumentFragment) {
|
||||||
let disconnected = false;
|
let disconnected = false;
|
||||||
for (const knob of this.byName.values()) {
|
for (const knob of this.byName.values()) {
|
||||||
disconnected = knob.disconnectWiring(container) || disconnected;
|
disconnected = knob.disconnectWiring(container) || disconnected;
|
||||||
@ -261,8 +274,11 @@ export interface KnobUi<T> {
|
|||||||
* and wiring may treat this case differently (e.g. restoring a value
|
* 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;
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,11 @@ export function title(): KnobUi<void> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export type KnobTypesToKnobs<
|
export type KnobTypesToKnobs<
|
||||||
// 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.
|
||||||
T extends {[name: string]: any},
|
T extends {[name: string]: any},
|
||||||
Names extends Extract<keyof T, string> = Extract<keyof T, string>,
|
Names extends Extract<keyof T, string> = Extract<keyof T, string>,
|
||||||
// tslint:disable-next-line:no-any We need to "map" the union type to knobs.
|
// tslint:disable-next-line:no-any We need to "map" the union type to knobs.
|
||||||
> = ReadonlyArray<Names extends any ? Knob<T[Names], Names>: never>;
|
> = ReadonlyArray<Names extends any ? Knob<T[Names], Names> : never>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An init object for Material Stories. This should be exposed to the user.
|
* An init object for Material Stories. This should be exposed to the user.
|
||||||
@ -86,7 +86,7 @@ export type KnobTypesToKnobs<
|
|||||||
export interface MaterialStoryInit<T extends {[name: string]: any}> {
|
export interface MaterialStoryInit<T extends {[name: string]: any}> {
|
||||||
name: string;
|
name: string;
|
||||||
render: (knobs: T) => TemplateResult | Promise<TemplateResult>;
|
render: (knobs: T) => TemplateResult | Promise<TemplateResult>;
|
||||||
styles?: CSSResult|CSSResult[];
|
styles?: CSSResult | CSSResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,8 +94,8 @@ export interface MaterialStoryInit<T extends {[name: string]: any}> {
|
|||||||
*/
|
*/
|
||||||
// tslint:disable-next-line:no-any No way to represent this type clearly.
|
// 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,
|
||||||
@ -119,4 +119,4 @@ export function setUpDemo(collection: LitCollection): void {
|
|||||||
const renderer = document.createElement('stories-renderer');
|
const renderer = document.createElement('stories-renderer');
|
||||||
renderer.collection = collection;
|
renderer.collection = collection;
|
||||||
document.body.appendChild(renderer);
|
document.body.appendChild(renderer);
|
||||||
}
|
}
|
||||||
|
@ -24,22 +24,23 @@ 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;
|
||||||
readonly render: (container: HTMLElement|DocumentFragment) => Promise<void>;
|
readonly render: (container: HTMLElement | DocumentFragment) => Promise<void>;
|
||||||
readonly dispose: (container: HTMLElement|DocumentFragment) => void;
|
readonly dispose: (container: HTMLElement | DocumentFragment) => void;
|
||||||
readonly knobs: KnobValues<Knobs>;
|
readonly knobs: KnobValues<Knobs>;
|
||||||
|
|
||||||
private readonly initStyles: CSSStyleSheet[]|undefined;
|
private readonly initStyles: CSSStyleSheet[] | undefined;
|
||||||
|
|
||||||
get styles() {
|
get styles() {
|
||||||
let styles = [...this.collection.customStyles];
|
let styles = [...this.collection.customStyles];
|
||||||
@ -54,19 +55,22 @@ 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 =
|
||||||
`The name can, but if so you have to give an ` +
|
`A story id can't contain a '/' character. ` +
|
||||||
`explicit id that doesn't.`;
|
`The name can, but if so you have to give an ` +
|
||||||
|
`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;
|
||||||
|
|
||||||
this.knobs = collection.knobs;
|
this.knobs = collection.knobs;
|
||||||
|
|
||||||
this.render = async (container: HTMLElement|DocumentFragment) => {
|
this.render = async (container: HTMLElement | DocumentFragment) => {
|
||||||
let wrapperDiv = wrapperDivMap.get(container);
|
let wrapperDiv = wrapperDivMap.get(container);
|
||||||
if (wrapperDiv === undefined) {
|
if (wrapperDiv === undefined) {
|
||||||
wrapperDiv = document.createElement('div');
|
wrapperDiv = document.createElement('div');
|
||||||
@ -77,7 +81,7 @@ class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
|
|||||||
await init.render(wrapperDiv, this.knobs);
|
await init.render(wrapperDiv, this.knobs);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispose = (container: HTMLElement|DocumentFragment) => {
|
this.dispose = (container: HTMLElement | DocumentFragment) => {
|
||||||
wrapperDivMap.delete(container);
|
wrapperDivMap.delete(container);
|
||||||
render(nothing, container);
|
render(nothing, container);
|
||||||
};
|
};
|
||||||
@ -93,8 +97,9 @@ class StoryImpl<Knobs extends PolymorphicArrayOfKnobs =
|
|||||||
* The type is exposed here, but the implementation isn't, because a Story
|
* 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,9 +111,10 @@ 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>();
|
||||||
readonly knobs: KnobValues<T>;
|
readonly knobs: KnobValues<T>;
|
||||||
@ -136,7 +142,7 @@ export class Collection<T extends PolymorphicArrayOfKnobs =
|
|||||||
return stories;
|
return stories;
|
||||||
}
|
}
|
||||||
|
|
||||||
get tree(): ReadonlyMap<string, Story<T>|Collection<T>> {
|
get tree(): ReadonlyMap<string, Story<T> | Collection<T>> {
|
||||||
return this.children;
|
return this.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,10 +175,10 @@ 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[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLitStoryInit(init: Partial<LitStoryInit>): init is LitStoryInit {
|
function isLitStoryInit(init: Partial<LitStoryInit>): init is LitStoryInit {
|
||||||
@ -181,19 +188,21 @@ function isLitStoryInit(init: Partial<LitStoryInit>): init is LitStoryInit {
|
|||||||
/**
|
/**
|
||||||
* A collection with convenience methods for rendering lit-html templates.
|
* 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,63 +20,68 @@ 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',
|
||||||
render: () => html`<md-checkbox checked></md-checkbox>`,
|
render: () => html`<md-checkbox checked></md-checkbox>`,
|
||||||
assertValue(formData) {
|
assertValue(formData) {
|
||||||
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',
|
||||||
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
|
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
|
||||||
assertValue(formData) {
|
assertValue(formData) {
|
||||||
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',
|
||||||
render: () =>
|
render: () =>
|
||||||
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',
|
||||||
render: () =>
|
render: () =>
|
||||||
html`<md-checkbox name="checkbox" checked disabled></md-checkbox>`,
|
html`<md-checkbox name="checkbox" checked disabled></md-checkbox>`,
|
||||||
assertValue(formData) {
|
assertValue(formData) {
|
||||||
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: [
|
||||||
{
|
{
|
||||||
@ -87,36 +92,36 @@ describe('<md-checkbox>', () => {
|
|||||||
},
|
},
|
||||||
assertReset(checkbox) {
|
assertReset(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',
|
||||||
render: () =>
|
render: () =>
|
||||||
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
|
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
|
||||||
change(checkbox) {
|
change(checkbox) {
|
||||||
checkbox.checked = false;
|
checkbox.checked = false;
|
||||||
},
|
},
|
||||||
assertReset(checkbox) {
|
assertReset(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',
|
||||||
render: () =>
|
render: () =>
|
||||||
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
|
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
|
||||||
change(checkbox) {
|
change(checkbox) {
|
||||||
checkbox.indeterminate = false;
|
checkbox.indeterminate = false;
|
||||||
},
|
},
|
||||||
assertReset(checkbox) {
|
assertReset(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: [
|
||||||
{
|
{
|
||||||
@ -124,31 +129,31 @@ describe('<md-checkbox>', () => {
|
|||||||
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
|
render: () => html`<md-checkbox name="checkbox"></md-checkbox>`,
|
||||||
assertRestored(checkbox) {
|
assertRestored(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',
|
||||||
render: () =>
|
render: () =>
|
||||||
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
|
html`<md-checkbox name="checkbox" checked></md-checkbox>`,
|
||||||
assertRestored(checkbox) {
|
assertRestored(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',
|
||||||
render: () =>
|
render: () =>
|
||||||
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
|
html`<md-checkbox name="checkbox" indeterminate></md-checkbox>`,
|
||||||
assertRestored(checkbox) {
|
assertRestored(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('indeterminate', {defaultValue: false, ui: boolInput()}),
|
new Knob('checked', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
new Knob('indeterminate', {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 */
|
||||||
@ -126,12 +130,12 @@ export class Checkbox extends LitElement {
|
|||||||
@state() private prevChecked = false;
|
@state() private prevChecked = false;
|
||||||
@state() private prevDisabled = false;
|
@state() private prevDisabled = false;
|
||||||
@state() private prevIndeterminate = false;
|
@state() private prevIndeterminate = false;
|
||||||
@query('input') private readonly input!: HTMLInputElement|null;
|
@query('input') private readonly input!: HTMLInputElement | null;
|
||||||
// Needed for Safari, see https://bugs.webkit.org/show_bug.cgi?id=261432
|
// 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,12 +200,15 @@ 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 =
|
||||||
changed.get('indeterminate') ?? this.indeterminate;
|
changed.get('indeterminate') ?? this.indeterminate;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldAddFormValue = this.checked && !this.indeterminate;
|
const shouldAddFormValue = this.checked && !this.indeterminate;
|
||||||
@ -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,84 +85,78 @@ 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();
|
expect(input.checked).toEqual(true);
|
||||||
expect(input.checked).toEqual(true);
|
harness.element.checked = false;
|
||||||
harness.element.checked = false;
|
await env.waitForStability();
|
||||||
await env.waitForStability();
|
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.
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(input.checked).toEqual(true);
|
expect(input.checked).toEqual(true);
|
||||||
expect(harness.element.checked).toEqual(true);
|
expect(harness.element.checked).toEqual(true);
|
||||||
|
|
||||||
// Set custom element checked to false.
|
// Set custom element checked to false.
|
||||||
harness.element.checked = false;
|
harness.element.checked = false;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(input.checked).toEqual(false);
|
expect(input.checked).toEqual(false);
|
||||||
expect(harness.element.checked).toEqual(false);
|
expect(harness.element.checked).toEqual(false);
|
||||||
|
|
||||||
// Set custom element checked to true.
|
// Set custom element checked to true.
|
||||||
harness.element.checked = true;
|
harness.element.checked = true;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(input.checked).toEqual(true);
|
expect(input.checked).toEqual(true);
|
||||||
expect(harness.element.checked).toEqual(true);
|
expect(harness.element.checked).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
expect(input.indeterminate).toEqual(true);
|
expect(input.indeterminate).toEqual(true);
|
||||||
expect(input.getAttribute('aria-checked')).toEqual('mixed');
|
expect(input.getAttribute('aria-checked')).toEqual('mixed');
|
||||||
|
|
||||||
harness.element.indeterminate = false;
|
harness.element.indeterminate = false;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
|
|
||||||
expect(input.indeterminate).toEqual(false);
|
expect(input.indeterminate).toEqual(false);
|
||||||
expect(input.getAttribute('aria-checked')).not.toEqual('mixed');
|
expect(input.getAttribute('aria-checked')).not.toEqual('mixed');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
expect(input.disabled).toEqual(true);
|
expect(input.disabled).toEqual(true);
|
||||||
harness.element.disabled = false;
|
harness.element.disabled = false;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(input.disabled).toEqual(false);
|
expect(input.disabled).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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 ?? ''}></md-test-checkbox>
|
||||||
.value=${propsInit.value ?? ''}
|
</form>`);
|
||||||
></md-test-checkbox>
|
|
||||||
</form>`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it('does not submit if not checked', async () => {
|
it('does not submit if not checked', async () => {
|
||||||
@ -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');
|
||||||
});
|
});
|
||||||
@ -195,17 +196,21 @@ describe('checkbox', () => {
|
|||||||
describe('label activation', () => {
|
describe('label activation', () => {
|
||||||
async function setupLabelTest() {
|
async function setupLabelTest() {
|
||||||
const test = await setupTest(html`
|
const test = await setupTest(html`
|
||||||
<label>
|
<label>
|
||||||
<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();
|
||||||
@ -221,8 +226,8 @@ describe('checkbox', () => {
|
|||||||
harness.element.required = true;
|
harness.element.required = true;
|
||||||
|
|
||||||
expect(harness.element.validity.valueMissing)
|
expect(harness.element.validity.valueMissing)
|
||||||
.withContext('checkbox.validity.valueMissing')
|
.withContext('checkbox.validity.valueMissing')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set valueMissing when required and checked', async () => {
|
it('should not set valueMissing when required and checked', async () => {
|
||||||
@ -231,8 +236,8 @@ describe('checkbox', () => {
|
|||||||
harness.element.checked = true;
|
harness.element.checked = true;
|
||||||
|
|
||||||
expect(harness.element.validity.valueMissing)
|
expect(harness.element.validity.valueMissing)
|
||||||
.withContext('checkbox.validity.valueMissing')
|
.withContext('checkbox.validity.valueMissing')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set valueMissing when required and indeterminate', async () => {
|
it('should set valueMissing when required and indeterminate', async () => {
|
||||||
@ -241,8 +246,8 @@ describe('checkbox', () => {
|
|||||||
harness.element.indeterminate = true;
|
harness.element.indeterminate = true;
|
||||||
|
|
||||||
expect(harness.element.validity.valueMissing)
|
expect(harness.element.validity.valueMissing)
|
||||||
.withContext('checkbox.validity.valueMissing')
|
.withContext('checkbox.validity.valueMissing')
|
||||||
.toBeTrue();
|
.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('elevated', {defaultValue: false, ui: boolInput()}),
|
new Knob('label', {defaultValue: '', ui: textInput()}),
|
||||||
new Knob('disabled', {defaultValue: false, ui: boolInput()}),
|
new Knob('elevated', {defaultValue: false, ui: boolInput()}),
|
||||||
new Knob('scrolling', {defaultValue: false, ui: boolInput()}),
|
new Knob('disabled', {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,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -12,16 +12,18 @@ import {Chip} from './internal/chip.js';
|
|||||||
* Test harness for chips.
|
* Test harness for chips.
|
||||||
*/
|
*/
|
||||||
export class ChipHarness extends Harness<Chip> {
|
export class ChipHarness extends Harness<Chip> {
|
||||||
action: 'primary'|'trailing' = 'primary';
|
action: 'primary' | 'trailing' = 'primary';
|
||||||
|
|
||||||
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 =
|
||||||
this.element.renderRoot.querySelector<HTMLElement>(`#${primaryId}`);
|
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,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import {Chip} from './chip.js';
|
|||||||
export class AssistChip extends Chip {
|
export class AssistChip extends Chip {
|
||||||
@property({type: Boolean}) elevated = false;
|
@property({type: Boolean}) elevated = false;
|
||||||
@property() href = '';
|
@property() href = '';
|
||||||
@property() target: '_blank'|'_parent'|'_self'|'_top'|'' = '';
|
@property() target: '_blank' | '_parent' | '_self' | '_top' | '' = '';
|
||||||
|
|
||||||
protected get primaryId() {
|
protected get primaryId() {
|
||||||
return this.href ? 'link' : 'button';
|
return this.href ? 'link' : 'button';
|
||||||
@ -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
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ describe('Assist chip', () => {
|
|||||||
await chip.updateComplete;
|
await chip.updateComplete;
|
||||||
|
|
||||||
expect(chip.renderRoot.querySelector('a'))
|
expect(chip.renderRoot.querySelector('a'))
|
||||||
.withContext('should have a rendered <a> link')
|
.withContext('should have a rendered <a> link')
|
||||||
.toBeTruthy();
|
.toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow link chips to be disabled', async () => {
|
it('should not allow link chips to be disabled', async () => {
|
||||||
@ -42,8 +42,8 @@ describe('Assist chip', () => {
|
|||||||
await chip.updateComplete;
|
await chip.updateComplete;
|
||||||
|
|
||||||
expect(chip.renderRoot.querySelector('.disabled,:disabled'))
|
expect(chip.renderRoot.querySelector('.disabled,:disabled'))
|
||||||
.withContext('should not have any disabled styling or behavior')
|
.withContext('should not have any disabled styling or behavior')
|
||||||
.toBeNull();
|
.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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.
|
||||||
@ -120,7 +127,7 @@ export class ChipSet extends LitElement {
|
|||||||
// The chip that should be focusable is either the chip that currently has
|
// The chip that should be focusable is either the chip that currently has
|
||||||
// focus or the first chip that can be focused.
|
// focus or the first chip that can be focused.
|
||||||
const {chips} = this;
|
const {chips} = this;
|
||||||
let chipToFocus: Chip|undefined;
|
let chipToFocus: Chip | undefined;
|
||||||
for (const chip of chips) {
|
for (const chip of chips) {
|
||||||
const isChipFocusable = chip.alwaysFocusable || !chip.disabled;
|
const isChipFocusable = chip.alwaysFocusable || !chip.disabled;
|
||||||
const chipIsFocused = chip.matches(':focus-within');
|
const chipIsFocused = chip.matches(':focus-within');
|
||||||
@ -147,5 +154,5 @@ export class ChipSet extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MaybeMultiActionChip extends Chip {
|
interface MaybeMultiActionChip extends Chip {
|
||||||
focus(options?: FocusOptions&{trailing?: boolean}): void;
|
focus(options?: FocusOptions & {trailing?: boolean}): void;
|
||||||
}
|
}
|
||||||
|
@ -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,42 +58,51 @@ 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')
|
||||||
.toBe('0');
|
.toBe('0');
|
||||||
expect(chipSet.chips[1].getAttribute('tabindex'))
|
expect(chipSet.chips[1].getAttribute('tabindex'))
|
||||||
.withContext('second tabindex')
|
.withContext('second tabindex')
|
||||||
.toBe('-1');
|
.toBe('-1');
|
||||||
expect(chipSet.chips[2].getAttribute('tabindex'))
|
expect(chipSet.chips[2].getAttribute('tabindex'))
|
||||||
.withContext('third tabindex')
|
.withContext('third tabindex')
|
||||||
.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
|
||||||
current?.focus();
|
current?.focus();
|
||||||
await harness.keypress(ltrKey);
|
await harness.keypress(ltrKey);
|
||||||
expect(next.matches(':focus-within'))
|
expect(next.matches(':focus-within'))
|
||||||
.withContext(`next chip is focused in LTR after ${ltrKey}`)
|
.withContext(`next chip is focused in LTR after ${ltrKey}`)
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
|
|
||||||
next.blur();
|
next.blur();
|
||||||
chipSet.style.direction = 'rtl';
|
chipSet.style.direction = 'rtl';
|
||||||
current?.focus();
|
current?.focus();
|
||||||
await harness.keypress(rtlKey);
|
await harness.keypress(rtlKey);
|
||||||
expect(next.matches(':focus-within'))
|
expect(next.matches(':focus-within'))
|
||||||
.withContext(`next chip is focused in RTL after ${rtlKey}`)
|
.withContext(`next chip is focused in RTL after ${rtlKey}`)
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should navigate forward on horizontal arrow keys', async () => {
|
it('should navigate forward on horizontal arrow keys', async () => {
|
||||||
@ -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,39 +157,37 @@ 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();
|
const chipSet = await setupTest([first, second, third]);
|
||||||
const chipSet = await setupTest([first, second, third]);
|
await testNavigation({
|
||||||
await testNavigation({
|
chipSet,
|
||||||
chipSet,
|
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();
|
const chipSet = await setupTest([first, second, third]);
|
||||||
const chipSet = await setupTest([first, second, third]);
|
await testNavigation({
|
||||||
await testNavigation({
|
chipSet,
|
||||||
chipSet,
|
ltrKey: 'ArrowLeft',
|
||||||
ltrKey: 'ArrowLeft',
|
rtlKey: 'ArrowRight',
|
||||||
rtlKey: 'ArrowRight',
|
current: null,
|
||||||
current: null,
|
next: third,
|
||||||
next: third
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip over disabled chips', async () => {
|
it('should skip over disabled chips', async () => {
|
||||||
const first = new TestAssistChip();
|
const first = new TestAssistChip();
|
||||||
@ -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,11 +230,12 @@ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore other keyboard events', async () => {
|
it('should ignore other keyboard events', async () => {
|
||||||
@ -244,8 +249,8 @@ describe('Chip set', () => {
|
|||||||
first.focus();
|
first.focus();
|
||||||
await harness.keypress('Enter');
|
await harness.keypress('Enter');
|
||||||
expect(first.matches(':focus-within'))
|
expect(first.matches(':focus-within'))
|
||||||
.withContext('first chip is still focused')
|
.withContext('first chip is still focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if there are not at least two chips', async () => {
|
it('should do nothing if there are not at least two chips', async () => {
|
||||||
@ -257,8 +262,8 @@ describe('Chip set', () => {
|
|||||||
single.focus();
|
single.focus();
|
||||||
await harness.keypress('ArrowRight');
|
await harness.keypress('ArrowRight');
|
||||||
expect(single.matches(':focus-within'))
|
expect(single.matches(':focus-within'))
|
||||||
.withContext('single chip is still focused')
|
.withContext('single chip is still 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,10 +93,10 @@ 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,14 +33,13 @@ 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);
|
|
||||||
|
|
||||||
chip.disabled = true;
|
chip.disabled = true;
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(updateFocusListener).toHaveBeenCalled();
|
expect(updateFocusListener).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -27,9 +27,10 @@ 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;
|
||||||
|
|
||||||
protected override getContainerClasses() {
|
protected override getContainerClasses() {
|
||||||
return {
|
return {
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import {renderRemoveButton} from './trailing-icons.js';
|
|||||||
export class InputChip extends MultiActionChip {
|
export class InputChip extends MultiActionChip {
|
||||||
@property({type: Boolean}) avatar = false;
|
@property({type: Boolean}) avatar = false;
|
||||||
@property() href = '';
|
@property() href = '';
|
||||||
@property() target: '_blank'|'_parent'|'_self'|'_top'|'' = '';
|
@property() target: '_blank' | '_parent' | '_self' | '_top' | '' = '';
|
||||||
@property({type: Boolean, attribute: 'remove-only'}) removeOnly = false;
|
@property({type: Boolean, attribute: 'remove-only'}) removeOnly = false;
|
||||||
@property({type: Boolean, reflect: true}) selected = false;
|
@property({type: Boolean, reflect: true}) selected = false;
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ export class InputChip extends MultiActionChip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@query('.trailing.action')
|
@query('.trailing.action')
|
||||||
protected readonly trailingAction!: HTMLElement|null;
|
protected readonly trailingAction!: HTMLElement | null;
|
||||||
|
|
||||||
protected override getContainerClasses() {
|
protected override getContainerClasses() {
|
||||||
return {
|
return {
|
||||||
@ -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
|
||||||
|
>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ describe('Input chip', () => {
|
|||||||
await chip.updateComplete;
|
await chip.updateComplete;
|
||||||
|
|
||||||
expect(chip.renderRoot.querySelector('a'))
|
expect(chip.renderRoot.querySelector('a'))
|
||||||
.withContext('should have a rendered <a> link')
|
.withContext('should have a rendered <a> link')
|
||||||
.toBeTruthy();
|
.toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow link chips to be disabled', async () => {
|
it('should not allow link chips to be disabled', async () => {
|
||||||
@ -42,8 +42,8 @@ describe('Input chip', () => {
|
|||||||
await chip.updateComplete;
|
await chip.updateComplete;
|
||||||
|
|
||||||
expect(chip.renderRoot.querySelector('.disabled,:disabled'))
|
expect(chip.renderRoot.querySelector('.disabled,:disabled'))
|
||||||
.withContext('should not have any disabled styling or behavior')
|
.withContext('should not have any disabled styling or behavior')
|
||||||
.toBeNull();
|
.toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ export abstract class MultiActionChip extends Chip {
|
|||||||
const {ariaLabel} = this as ARIAMixinStrict;
|
const {ariaLabel} = this as ARIAMixinStrict;
|
||||||
return `Remove ${ariaLabel || this.label}`;
|
return `Remove ${ariaLabel || this.label}`;
|
||||||
}
|
}
|
||||||
set ariaLabelRemove(ariaLabel: string|null) {
|
set ariaLabelRemove(ariaLabel: string | null) {
|
||||||
const prev = this.ariaLabelRemove;
|
const prev = this.ariaLabelRemove;
|
||||||
if (ariaLabel === prev) {
|
if (ariaLabel === prev) {
|
||||||
return;
|
return;
|
||||||
@ -39,8 +39,8 @@ export abstract class MultiActionChip extends Chip {
|
|||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract readonly primaryAction: HTMLElement|null;
|
protected abstract readonly primaryAction: HTMLElement | null;
|
||||||
protected abstract readonly trailingAction: HTMLElement|null;
|
protected abstract readonly trailingAction: HTMLElement | null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -50,7 +50,7 @@ export abstract class MultiActionChip extends Chip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override focus(options?: FocusOptions&{trailing?: boolean}) {
|
override focus(options?: FocusOptions & {trailing?: boolean}) {
|
||||||
const isFocusable = this.alwaysFocusable || !this.disabled;
|
const isFocusable = this.alwaysFocusable || !this.disabled;
|
||||||
if (isFocusable && options?.trailing && this.trailingAction) {
|
if (isFocusable && options?.trailing && this.trailingAction) {
|
||||||
this.trailingAction.focus(options);
|
this.trailingAction.focus(options);
|
||||||
@ -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(
|
||||||
primaryAction.tabIndex = 0;
|
'focusout',
|
||||||
}, {once: true});
|
() => {
|
||||||
|
primaryAction.tabIndex = 0;
|
||||||
|
},
|
||||||
|
{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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,13 +63,13 @@ describe('Multi-action chips', () => {
|
|||||||
|
|
||||||
await primaryHarness.focusWithKeyboard();
|
await primaryHarness.focusWithKeyboard();
|
||||||
expect(chip.primaryAction.matches(':focus-within'))
|
expect(chip.primaryAction.matches(':focus-within'))
|
||||||
.withContext('primary action is focused')
|
.withContext('primary action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
|
|
||||||
await primaryHarness.keypress('ArrowRight');
|
await primaryHarness.keypress('ArrowRight');
|
||||||
expect(chip.trailingAction.matches(':focus-within'))
|
expect(chip.trailingAction.matches(':focus-within'))
|
||||||
.withContext('trailing action is focused')
|
.withContext('trailing action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move internal focus forwards in rtl', async () => {
|
it('should move internal focus forwards in rtl', async () => {
|
||||||
@ -75,13 +79,13 @@ describe('Multi-action chips', () => {
|
|||||||
|
|
||||||
await primaryHarness.focusWithKeyboard();
|
await primaryHarness.focusWithKeyboard();
|
||||||
expect(chip.primaryAction.matches(':focus-within'))
|
expect(chip.primaryAction.matches(':focus-within'))
|
||||||
.withContext('primary action is focused')
|
.withContext('primary action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
|
|
||||||
await primaryHarness.keypress('ArrowLeft');
|
await primaryHarness.keypress('ArrowLeft');
|
||||||
expect(chip.trailingAction.matches(':focus-within'))
|
expect(chip.trailingAction.matches(':focus-within'))
|
||||||
.withContext('trailing action is focused')
|
.withContext('trailing action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move internal focus backwards', async () => {
|
it('should move internal focus backwards', async () => {
|
||||||
@ -91,13 +95,13 @@ describe('Multi-action chips', () => {
|
|||||||
|
|
||||||
await trailingHarness.focusWithKeyboard();
|
await trailingHarness.focusWithKeyboard();
|
||||||
expect(chip.trailingAction.matches(':focus-within'))
|
expect(chip.trailingAction.matches(':focus-within'))
|
||||||
.withContext('trailing action is focused')
|
.withContext('trailing action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
|
|
||||||
await trailingHarness.keypress('ArrowLeft');
|
await trailingHarness.keypress('ArrowLeft');
|
||||||
expect(chip.primaryAction.matches(':focus-within'))
|
expect(chip.primaryAction.matches(':focus-within'))
|
||||||
.withContext('primary action is focused')
|
.withContext('primary action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should move internal focus backwards in rtl', async () => {
|
it('should move internal focus backwards in rtl', async () => {
|
||||||
@ -108,13 +112,13 @@ describe('Multi-action chips', () => {
|
|||||||
|
|
||||||
await trailingHarness.focusWithKeyboard();
|
await trailingHarness.focusWithKeyboard();
|
||||||
expect(chip.trailingAction.matches(':focus-within'))
|
expect(chip.trailingAction.matches(':focus-within'))
|
||||||
.withContext('trailing action is focused')
|
.withContext('trailing action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
|
|
||||||
await trailingHarness.keypress('ArrowRight');
|
await trailingHarness.keypress('ArrowRight');
|
||||||
expect(chip.primaryAction.matches(':focus-within'))
|
expect(chip.primaryAction.matches(':focus-within'))
|
||||||
.withContext('primary action is focused')
|
.withContext('primary action is focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not bubble when navigating internally', async () => {
|
it('should not bubble when navigating internally', async () => {
|
||||||
@ -132,38 +136,36 @@ 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';
|
const keydownHandler = jasmine.createSpy();
|
||||||
const keydownHandler = jasmine.createSpy();
|
if (!chip.parentElement) {
|
||||||
if (!chip.parentElement) {
|
throw new Error('Expected chip to have a parentElement for test.');
|
||||||
throw new Error('Expected chip to have a parentElement for test.');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
chip.parentElement.addEventListener('keydown', keydownHandler);
|
chip.parentElement.addEventListener('keydown', keydownHandler);
|
||||||
|
|
||||||
await trailingHarness.focusWithKeyboard();
|
await trailingHarness.focusWithKeyboard();
|
||||||
await trailingHarness.keypress('ArrowRight');
|
await trailingHarness.keypress('ArrowRight');
|
||||||
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();
|
if (!chip.parentElement) {
|
||||||
if (!chip.parentElement) {
|
throw new Error('Expected chip to have a parentElement for test.');
|
||||||
throw new Error('Expected chip to have a parentElement for test.');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
chip.parentElement.addEventListener('keydown', keydownHandler);
|
chip.parentElement.addEventListener('keydown', keydownHandler);
|
||||||
|
|
||||||
await primaryHarness.focusWithKeyboard();
|
await primaryHarness.focusWithKeyboard();
|
||||||
await primaryHarness.keypress('ArrowLeft');
|
await primaryHarness.keypress('ArrowLeft');
|
||||||
expect(keydownHandler).toHaveBeenCalledTimes(1);
|
expect(keydownHandler).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if it does not have multiple actions', async () => {
|
it('should do nothing if it does not have multiple actions', async () => {
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
@ -174,8 +176,8 @@ describe('Multi-action chips', () => {
|
|||||||
await primaryHarness.focusWithKeyboard();
|
await primaryHarness.focusWithKeyboard();
|
||||||
await primaryHarness.keypress('ArrowLeft');
|
await primaryHarness.keypress('ArrowLeft');
|
||||||
expect(chip.primaryAction.matches(':focus-within'))
|
expect(chip.primaryAction.matches(':focus-within'))
|
||||||
.withContext('primary action is still focused')
|
.withContext('primary action is still focused')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -186,12 +188,12 @@ describe('Multi-action chips', () => {
|
|||||||
harness.action = 'trailing';
|
harness.action = 'trailing';
|
||||||
|
|
||||||
expect(chip.parentElement)
|
expect(chip.parentElement)
|
||||||
.withContext('chip should be attached before removing')
|
.withContext('chip should be attached before removing')
|
||||||
.not.toBeNull();
|
.not.toBeNull();
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
expect(chip.parentElement)
|
expect(chip.parentElement)
|
||||||
.withContext('chip should be detached after removing')
|
.withContext('chip should be detached after removing')
|
||||||
.toBeNull();
|
.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dispatch a "remove" event when removed', async () => {
|
it('should dispatch a "remove" event when removed', async () => {
|
||||||
@ -205,20 +207,19 @@ 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();
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
expect(chip.parentElement)
|
expect(chip.parentElement)
|
||||||
.withContext('chip should still be attached')
|
.withContext('chip should still be attached')
|
||||||
.not.toBeNull();
|
.not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should provide a default "ariaLabelRemove" value', async () => {
|
it('should provide a default "ariaLabelRemove" value', async () => {
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
@ -227,14 +228,13 @@ 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';
|
|
||||||
|
|
||||||
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.ariaLabel}`);
|
expect(chip.ariaLabelRemove).toEqual(`Remove ${chip.ariaLabel}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow setting a custom "ariaLabelRemove"', async () => {
|
it('should allow setting a custom "ariaLabelRemove"', async () => {
|
||||||
const chip = await setupTest();
|
const chip = await setupTest();
|
||||||
|
@ -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('headline', {defaultValue: 'Dialog', ui: textInput()}),
|
new Knob('icon', {defaultValue: '', ui: textInput()}),
|
||||||
new Knob(
|
new Knob('headline', {defaultValue: 'Dialog', ui: textInput()}),
|
||||||
'supportingText',
|
new Knob('supportingText', {
|
||||||
{defaultValue: 'Just a simple dialog.', ui: textInput()}),
|
defaultValue: 'Just a simple dialog.',
|
||||||
]);
|
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(
|
||||||
resolve();
|
'closed',
|
||||||
}, {once: true});
|
() => {
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
{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,19 +137,18 @@ 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);
|
button.focus();
|
||||||
button.focus();
|
expect(document.activeElement).toBe(button);
|
||||||
expect(document.activeElement).toBe(button);
|
await harness.element.show();
|
||||||
await harness.element.show();
|
expect(document.activeElement).toBe(focusElement);
|
||||||
expect(document.activeElement).toBe(focusElement);
|
await harness.element.close();
|
||||||
await harness.element.close();
|
expect(document.activeElement).toBe(button);
|
||||||
expect(document.activeElement).toBe(button);
|
button.remove();
|
||||||
button.remove();
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set returnValue during the close event', async () => {
|
it('should set returnValue during the close event', async () => {
|
||||||
@ -159,14 +163,14 @@ describe('<md-dialog>', () => {
|
|||||||
const returnValue = 'foo';
|
const returnValue = 'foo';
|
||||||
await harness.element.close(returnValue);
|
await harness.element.close(returnValue);
|
||||||
expect(returnValueDuringClose)
|
expect(returnValueDuringClose)
|
||||||
.withContext('dialog.returnValue during close event')
|
.withContext('dialog.returnValue during close event')
|
||||||
.toBe(returnValue);
|
.toBe(returnValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -174,8 +178,8 @@ describe('<md-dialog>', () => {
|
|||||||
const prevReturnValue = harness.element.returnValue;
|
const prevReturnValue = harness.element.returnValue;
|
||||||
await harness.element.close('new return value');
|
await harness.element.close('new return value');
|
||||||
expect(harness.element.returnValue)
|
expect(harness.element.returnValue)
|
||||||
.withContext('dialog.returnValue after close event canceled')
|
.withContext('dialog.returnValue after close event canceled')
|
||||||
.toBe(prevReturnValue);
|
.toBe(prevReturnValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open on connected if opened before connected to DOM', async () => {
|
it('should open on connected if opened before connected to DOM', async () => {
|
||||||
@ -185,50 +189,49 @@ describe('<md-dialog>', () => {
|
|||||||
dialog.addEventListener('open', openListener);
|
dialog.addEventListener('open', openListener);
|
||||||
dialog.open = true;
|
dialog.open = true;
|
||||||
expect(openListener)
|
expect(openListener)
|
||||||
.withContext('should not trigger open before connected')
|
.withContext('should not trigger open before connected')
|
||||||
.not.toHaveBeenCalled();
|
.not.toHaveBeenCalled();
|
||||||
|
|
||||||
const root = env.render(html``);
|
const root = env.render(html``);
|
||||||
root.appendChild(dialog);
|
root.appendChild(dialog);
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(openListener)
|
expect(openListener)
|
||||||
.withContext('opens after connecting')
|
.withContext('opens after connecting')
|
||||||
.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);
|
dialog.addEventListener('open', openListener);
|
||||||
dialog.addEventListener('open', openListener);
|
dialog.open = true;
|
||||||
dialog.open = true;
|
await env.waitForStability();
|
||||||
await env.waitForStability();
|
dialog.open = false;
|
||||||
dialog.open = false;
|
const root = env.render(html``);
|
||||||
const root = env.render(html``);
|
root.appendChild(dialog);
|
||||||
root.appendChild(dialog);
|
await env.waitForStability();
|
||||||
await env.waitForStability();
|
expect(openListener)
|
||||||
expect(openListener)
|
.withContext('should not open on connected since close was called')
|
||||||
.withContext('should not open on connected since close was called')
|
.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);
|
dialog.addEventListener('open', openListener);
|
||||||
dialog.addEventListener('open', openListener);
|
dialog.open = true;
|
||||||
dialog.open = true;
|
const root = env.render(html``);
|
||||||
const root = env.render(html``);
|
root.appendChild(dialog);
|
||||||
root.appendChild(dialog);
|
dialog.open = false;
|
||||||
dialog.open = false;
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not dispatch close if closed while disconnected', async () => {
|
it('should not dispatch close if closed while disconnected', async () => {
|
||||||
const {harness, root} = await setupTest();
|
const {harness, root} = await setupTest();
|
||||||
@ -240,19 +243,19 @@ describe('<md-dialog>', () => {
|
|||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
|
|
||||||
expect(closeListener)
|
expect(closeListener)
|
||||||
.withContext('should not trigger close when disconnected')
|
.withContext('should not trigger close when disconnected')
|
||||||
.not.toHaveBeenCalled();
|
.not.toHaveBeenCalled();
|
||||||
|
|
||||||
await harness.element.close();
|
await harness.element.close();
|
||||||
expect(closeListener)
|
expect(closeListener)
|
||||||
.withContext('should not trigger close when disconnected')
|
.withContext('should not trigger close when disconnected')
|
||||||
.not.toHaveBeenCalled();
|
.not.toHaveBeenCalled();
|
||||||
|
|
||||||
root.appendChild(harness.element);
|
root.appendChild(harness.element);
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(closeListener)
|
expect(closeListener)
|
||||||
.withContext('should not trigger close when disconnected')
|
.withContext('should not trigger close when disconnected')
|
||||||
.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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,17 +96,17 @@ export class Dialog extends LitElement {
|
|||||||
// getIsConnectedPromise() immediately sets the resolve property.
|
// getIsConnectedPromise() immediately sets the resolve property.
|
||||||
private isConnectedPromiseResolve!: () => void;
|
private isConnectedPromiseResolve!: () => void;
|
||||||
private isConnectedPromise = this.getIsConnectedPromise();
|
private isConnectedPromise = this.getIsConnectedPromise();
|
||||||
@query('dialog') private readonly dialog!: HTMLDialogElement|null;
|
@query('dialog') private readonly dialog!: HTMLDialogElement | null;
|
||||||
@query('.scrim') private readonly scrim!: HTMLDialogElement|null;
|
@query('.scrim') private readonly scrim!: HTMLDialogElement | null;
|
||||||
@query('.container') private readonly container!: HTMLDialogElement|null;
|
@query('.container') private readonly container!: HTMLDialogElement | null;
|
||||||
@query('.headline') private readonly headline!: HTMLDialogElement|null;
|
@query('.headline') private readonly headline!: HTMLDialogElement | null;
|
||||||
@query('.content') private readonly content!: HTMLDialogElement|null;
|
@query('.content') private readonly content!: HTMLDialogElement | null;
|
||||||
@query('.actions') private readonly actions!: HTMLDialogElement|null;
|
@query('.actions') private readonly actions!: HTMLDialogElement | null;
|
||||||
@state() private isAtScrollTop = false;
|
@state() private isAtScrollTop = false;
|
||||||
@state() private isAtScrollBottom = false;
|
@state() private isAtScrollBottom = false;
|
||||||
@query('.scroller') private readonly scroller!: HTMLElement|null;
|
@query('.scroller') private readonly scroller!: HTMLElement | null;
|
||||||
@query('.top.anchor') private readonly topAnchor!: HTMLElement|null;
|
@query('.top.anchor') private readonly topAnchor!: HTMLElement | null;
|
||||||
@query('.bottom.anchor') private readonly bottomAnchor!: HTMLElement|null;
|
@query('.bottom.anchor') private readonly bottomAnchor!: HTMLElement | null;
|
||||||
private nextClickIsFromContent = false;
|
private nextClickIsFromContent = false;
|
||||||
private intersectionObserver?: IntersectionObserver;
|
private intersectionObserver?: IntersectionObserver;
|
||||||
// Dialogs should not be SSR'd while open, so we can just use runtime checks.
|
// Dialogs should not be SSR'd while open, so we can just use runtime checks.
|
||||||
@ -139,8 +144,9 @@ export class Dialog extends LitElement {
|
|||||||
return;
|
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;
|
||||||
@ -216,7 +223,7 @@ export class Dialog extends LitElement {
|
|||||||
|
|
||||||
protected override render() {
|
protected override render() {
|
||||||
const scrollable =
|
const scrollable =
|
||||||
this.open && !(this.isAtScrollTop && this.isAtScrollBottom);
|
this.open && !(this.isAtScrollTop && this.isAtScrollBottom);
|
||||||
const classes = {
|
const classes = {
|
||||||
'has-headline': this.hasHeadline,
|
'has-headline': this.hasHeadline,
|
||||||
'has-actions': this.hasActions,
|
'has-actions': this.hasActions,
|
||||||
@ -236,18 +243,16 @@ 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
|
||||||
@slotchange=${this.handleHeadlineChange}></slot>
|
name="headline"
|
||||||
|
@slotchange=${this.handleHeadlineChange}></slot>
|
||||||
</h2>
|
</h2>
|
||||||
<md-divider></md-divider>
|
<md-divider></md-divider>
|
||||||
</div>
|
</div>
|
||||||
@ -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(
|
||||||
for (const entry of entries) {
|
(entries) => {
|
||||||
this.handleAnchorIntersection(entry);
|
for (const entry of entries) {
|
||||||
}
|
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 (start)', {defaultValue: false, ui: boolInput()}),
|
new Knob('inset', {defaultValue: true, ui: boolInput()}),
|
||||||
new Knob('inset (end)', {defaultValue: false, ui: boolInput()}),
|
new Knob('inset (start)', {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. */
|
||||||
@ -17,7 +20,7 @@ export interface StoryKnobs {
|
|||||||
icon: string;
|
icon: string;
|
||||||
label: string;
|
label: string;
|
||||||
lowered: boolean;
|
lowered: boolean;
|
||||||
size: FabSize|undefined;
|
size: FabSize | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = css`
|
const styles = css`
|
||||||
@ -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,8 +156,9 @@ 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
|
||||||
.querySelector('md-branded-fab');
|
.render(html`<md-branded-fab></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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {SharedFab} from './shared.js';
|
|||||||
/**
|
/**
|
||||||
* The variants available to non-branded FABs.
|
* The variants available to non-branded FABs.
|
||||||
*/
|
*/
|
||||||
export type FabVariant = 'surface'|'primary'|'secondary'|'tertiary';
|
export type FabVariant = 'surface' | 'primary' | 'secondary' | 'tertiary';
|
||||||
|
|
||||||
// tslint:disable-next-line:enforce-comments-on-exported-symbols
|
// tslint:disable-next-line:enforce-comments-on-exported-symbols
|
||||||
export class Fab extends SharedFab {
|
export class Fab extends SharedFab {
|
||||||
|
@ -18,7 +18,7 @@ import {requestUpdateOnAriaChange} from '../../internal/aria/delegate.js';
|
|||||||
/**
|
/**
|
||||||
* Sizes variants available to non-extended FABs.
|
* Sizes variants available to non-extended FABs.
|
||||||
*/
|
*/
|
||||||
export type FabSize = 'medium'|'small'|'large';
|
export type FabSize = 'medium' | 'small' | 'large';
|
||||||
|
|
||||||
// tslint:disable-next-line:enforce-comments-on-exported-symbols
|
// tslint:disable-next-line:enforce-comments-on-exported-symbols
|
||||||
export abstract class SharedFab extends LitElement {
|
export abstract class SharedFab extends LitElement {
|
||||||
@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -90,12 +86,13 @@ export abstract class SharedFab extends LitElement {
|
|||||||
private renderIcon() {
|
private renderIcon() {
|
||||||
const {ariaLabel} = this as ARIAMixinStrict;
|
const {ariaLabel} = this as ARIAMixinStrict;
|
||||||
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'
|
||||||
<span></span>
|
: (nothing as unknown as 'false')}>
|
||||||
</slot>
|
<span></span>
|
||||||
</span>`;
|
</slot>
|
||||||
|
</span>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,32 +4,45 @@
|
|||||||
* 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(
|
new Knob('label', {ui: textInput(), defaultValue: 'Label'}),
|
||||||
'Supporting text',
|
new Knob('Supporting text', {
|
||||||
{ui: textInput(), defaultValue: 'Supporting text'}),
|
ui: textInput(),
|
||||||
new Knob('Error text', {ui: textInput(), defaultValue: 'Error text'}),
|
defaultValue: 'Supporting text',
|
||||||
new Knob('count', {ui: numberInput(), defaultValue: 0}),
|
}),
|
||||||
new Knob('max', {ui: numberInput(), defaultValue: 0}),
|
new Knob('Error text', {ui: textInput(), defaultValue: 'Error text'}),
|
||||||
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
new Knob('count', {ui: numberInput(), defaultValue: 0}),
|
||||||
new Knob('error', {ui: boolInput(), defaultValue: false}),
|
new Knob('max', {ui: numberInput(), defaultValue: 0}),
|
||||||
new Knob('focused', {ui: boolInput(), defaultValue: false}),
|
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('populated', {ui: boolInput(), defaultValue: false}),
|
new Knob('error', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('required', {ui: boolInput(), defaultValue: false}),
|
new Knob('focused', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('Leading icon', {ui: boolInput(), defaultValue: false}),
|
new Knob('populated', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('Trailing icon', {ui: boolInput(), defaultValue: false}),
|
new Knob('required', {ui: boolInput(), defaultValue: false}),
|
||||||
new Knob('resizable', {ui: boolInput(), defaultValue: false}),
|
new Knob('Leading icon', {ui: boolInput(), defaultValue: false}),
|
||||||
]);
|
new Knob('Trailing icon', {ui: boolInput(), defaultValue: false}),
|
||||||
|
new Knob('resizable', {ui: boolInput(), defaultValue: false}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
collection.addStories(...materialInitsToStoryInits(stories));
|
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. */
|
||||||
|
@ -30,6 +30,6 @@ export class FieldHarness extends Harness<Field> {
|
|||||||
protected override async getInteractiveElement() {
|
protected override async getInteractiveElement() {
|
||||||
await this.element.updateComplete;
|
await this.element.updateComplete;
|
||||||
return (this.element.querySelector(':not([slot])') ||
|
return (this.element.querySelector(':not([slot])') ||
|
||||||
this.element.renderRoot.querySelector('.field')) as HTMLElement;
|
this.element.renderRoot.querySelector('.field')) as HTMLElement;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,9 +67,10 @@ 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')
|
||||||
@query('.label.resting') private readonly restingLabelEl!: HTMLElement|null;
|
private readonly floatingLabelEl!: HTMLElement | null;
|
||||||
@query('.container') private readonly containerEl!: HTMLElement|null;
|
@query('.label.resting') private readonly restingLabelEl!: HTMLElement | null;
|
||||||
|
@query('.container') private readonly containerEl!: HTMLElement | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-announces the field's error supporting text to screen readers.
|
* Re-announces the field's error supporting text to screen readers.
|
||||||
@ -78,7 +86,7 @@ export class Field extends LitElement {
|
|||||||
protected override update(props: PropertyValues<Field>) {
|
protected override update(props: PropertyValues<Field>) {
|
||||||
// Client-side property updates
|
// Client-side property updates
|
||||||
const isDisabledChanging =
|
const isDisabledChanging =
|
||||||
props.has('disabled') && props.get('disabled') !== undefined;
|
props.has('disabled') && props.get('disabled') !== undefined;
|
||||||
if (isDisabledChanging) {
|
if (isDisabledChanging) {
|
||||||
this.disableTransitions = true;
|
this.disableTransitions = true;
|
||||||
}
|
}
|
||||||
@ -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,21 +189,22 @@ 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
|
||||||
// role="alert" attribute. Another render cycle will happen after an
|
// role="alert" attribute. Another render cycle will happen after an
|
||||||
// animation frame to re-add the role.
|
// animation frame to re-add the role.
|
||||||
const shouldErrorAnnounce =
|
const shouldErrorAnnounce =
|
||||||
this.error && this.errorText && !this.refreshErrorAlert;
|
this.error && this.errorText && !this.refreshErrorAlert;
|
||||||
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 =
|
||||||
Math.round((restingHeight - floatingHeight * scale) / 2);
|
restingY -
|
||||||
|
floatingY +
|
||||||
|
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);
|
||||||
@ -82,8 +84,8 @@ describe('Field', () => {
|
|||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
// Assertion.
|
// Assertion.
|
||||||
expect(instance.focused)
|
expect(instance.focused)
|
||||||
.withContext('focused is false after disabled is set to true')
|
.withContext('focused is false after disabled is set to true')
|
||||||
.toBe(false);
|
.toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow focus when disabled', async () => {
|
it('should not allow focus when disabled', async () => {
|
||||||
@ -94,8 +96,8 @@ describe('Field', () => {
|
|||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
// Assertion.
|
// Assertion.
|
||||||
expect(instance.focused)
|
expect(instance.focused)
|
||||||
.withContext('focused set back to false when disabled')
|
.withContext('focused set back to false when disabled')
|
||||||
.toBe(false);
|
.toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -291,9 +293,10 @@ describe('Field', () => {
|
|||||||
const {instance} = await setupTest({label: undefined});
|
const {instance} = await setupTest({label: undefined});
|
||||||
// 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('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render label', async () => {
|
it('should render label', async () => {
|
||||||
@ -303,8 +306,8 @@ describe('Field', () => {
|
|||||||
const {instance} = await setupTest({label: labelValue});
|
const {instance} = await setupTest({label: labelValue});
|
||||||
// Assertion.
|
// Assertion.
|
||||||
expect(instance.labelText)
|
expect(instance.labelText)
|
||||||
.withContext('label text should equal label when not required')
|
.withContext('label text should equal label when not required')
|
||||||
.toBe(labelValue);
|
.toBe(labelValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should adds asterisk if required', async () => {
|
it('should adds asterisk if required', async () => {
|
||||||
@ -314,44 +317,49 @@ describe('Field', () => {
|
|||||||
const {instance} = await setupTest({required: true, label: labelValue});
|
const {instance} = await setupTest({required: true, label: labelValue});
|
||||||
// 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('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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()')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not announce supporting text', async () => {
|
it('should not announce supporting text', async () => {
|
||||||
const {instance} = await setupTest();
|
const {instance} = await setupTest();
|
||||||
@ -360,26 +368,28 @@ describe('Field', () => {
|
|||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
|
|
||||||
expect(instance.didErrorAnnounce())
|
expect(instance.didErrorAnnounce())
|
||||||
.withContext('instance.didErrorAnnounce()')
|
.withContext('instance.didErrorAnnounce()')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
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();
|
||||||
// After lit update, but before re-render refresh
|
// After lit update, but before re-render refresh
|
||||||
expect(instance.didErrorAnnounce())
|
expect(instance.didErrorAnnounce())
|
||||||
.withContext('didErrorAnnounce() before refresh')
|
.withContext('didErrorAnnounce() before refresh')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
|
|
||||||
// After the second lit update render refresh
|
// After the second lit update render refresh
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
expect(instance.didErrorAnnounce())
|
expect(instance.didErrorAnnounce())
|
||||||
.withContext('didErrorAnnounce() after refresh')
|
.withContext('didErrorAnnounce() after refresh')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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.
|
||||||
@ -34,19 +37,21 @@ export class FocusRing extends LitElement implements Attachable {
|
|||||||
return this.attachableController.htmlFor;
|
return this.attachableController.htmlFor;
|
||||||
}
|
}
|
||||||
|
|
||||||
set htmlFor(htmlFor: string|null) {
|
set htmlFor(htmlFor: string | null) {
|
||||||
this.attachableController.htmlFor = htmlFor;
|
this.attachableController.htmlFor = htmlFor;
|
||||||
}
|
}
|
||||||
|
|
||||||
get control() {
|
get control() {
|
||||||
return this.attachableController.control;
|
return this.attachableController.control;
|
||||||
}
|
}
|
||||||
set control(control: HTMLElement|null) {
|
set control(control: HTMLElement | null) {
|
||||||
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);
|
||||||
@ -86,7 +91,7 @@ export class FocusRing extends LitElement implements Attachable {
|
|||||||
event[HANDLED_BY_FOCUS_RING] = true;
|
event[HANDLED_BY_FOCUS_RING] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onControlChange(prev: HTMLElement|null, next: HTMLElement|null) {
|
private onControlChange(prev: HTMLElement | null, next: HTMLElement | null) {
|
||||||
if (isServer) return;
|
if (isServer) return;
|
||||||
|
|
||||||
for (const event of EVENTS) {
|
for (const event of EVENTS) {
|
||||||
|
@ -64,27 +64,26 @@ 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>
|
<test-focus-ring for="first"></test-focus-ring>
|
||||||
<test-focus-ring for="first"></test-focus-ring>
|
`);
|
||||||
`);
|
|
||||||
|
|
||||||
const secondButton = root.querySelector<HTMLElement>('#second');
|
const secondButton = root.querySelector<HTMLElement>('#second');
|
||||||
if (!secondButton) {
|
if (!secondButton) {
|
||||||
throw new Error('Could not query rendered <button id="second">');
|
throw new Error('Could not query rendered <button id="second">');
|
||||||
}
|
}
|
||||||
|
|
||||||
focusRing.setAttribute('for', 'second');
|
focusRing.setAttribute('for', 'second');
|
||||||
expect(focusRing.control)
|
expect(focusRing.control)
|
||||||
.withContext('focusRing.control')
|
.withContext('focusRing.control')
|
||||||
.toBe(secondButton);
|
.toBe(secondButton);
|
||||||
await new Harness(secondButton).focusWithKeyboard();
|
await new Harness(secondButton).focusWithKeyboard();
|
||||||
|
|
||||||
expect(focusRing.visible).withContext('focusRing.visible').toBeTrue();
|
expect(focusRing.visible).withContext('focusRing.visible').toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to be imperatively attached', () => {
|
it('should be able to be imperatively attached', () => {
|
||||||
const {button, focusRing} = setupTest(html`
|
const {button, focusRing} = setupTest(html`
|
||||||
@ -104,12 +103,12 @@ describe('focus ring', () => {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
expect(focusRing.control)
|
expect(focusRing.control)
|
||||||
.withContext('focusRing.control before attach')
|
.withContext('focusRing.control before attach')
|
||||||
.toBe(button);
|
.toBe(button);
|
||||||
focusRing.attach(button);
|
focusRing.attach(button);
|
||||||
expect(focusRing.control)
|
expect(focusRing.control)
|
||||||
.withContext('focusRing.control after attach')
|
.withContext('focusRing.control after attach')
|
||||||
.toBe(button);
|
.toBe(button);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detach previous control when attaching a new one', async () => {
|
it('should detach previous control when attaching a new one', async () => {
|
||||||
@ -170,8 +169,8 @@ describe('focus ring', () => {
|
|||||||
|
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
expect(focusRing.visible)
|
expect(focusRing.visible)
|
||||||
.withContext('focusRing.visible after clickWithMouse')
|
.withContext('focusRing.visible after clickWithMouse')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be visible on keyboard focus', async () => {
|
it('should be visible on keyboard focus', async () => {
|
||||||
@ -183,8 +182,8 @@ describe('focus ring', () => {
|
|||||||
|
|
||||||
await harness.focusWithKeyboard();
|
await harness.focusWithKeyboard();
|
||||||
expect(focusRing.visible)
|
expect(focusRing.visible)
|
||||||
.withContext('focusRing.visible after focusWithKeyboard')
|
.withContext('focusRing.visible after focusWithKeyboard')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide on blur', async () => {
|
it('should hide on blur', async () => {
|
||||||
@ -197,7 +196,7 @@ describe('focus ring', () => {
|
|||||||
focusRing.visible = true;
|
focusRing.visible = true;
|
||||||
await harness.blur();
|
await harness.blur();
|
||||||
expect(focusRing.visible)
|
expect(focusRing.visible)
|
||||||
.withContext('focusRing.visible after blur')
|
.withContext('focusRing.visible after blur')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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,9 +57,9 @@ 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')!;
|
||||||
|
|
||||||
await env.waitForStability();
|
await env.waitForStability();
|
||||||
|
@ -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('selectedIcon', {ui: textInput(), defaultValue: 'close'}),
|
new Knob('icon', {ui: textInput(), defaultValue: 'check'}),
|
||||||
new Knob('disabled', {ui: boolInput(), defaultValue: false}),
|
new Knob('selectedIcon', {ui: textInput(), defaultValue: 'close'}),
|
||||||
]);
|
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. */
|
||||||
|
@ -14,4 +14,4 @@ describe('<md-filled-icon-button>', () => {
|
|||||||
describe('.styles', () => {
|
describe('.styles', () => {
|
||||||
createTokenTests(MdFilledIconButton.styles);
|
createTokenTests(MdFilledIconButton.styles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,4 +14,4 @@ describe('<md-filled-tonal-icon-button', () => {
|
|||||||
describe('.styles', () => {
|
describe('.styles', () => {
|
||||||
createTokenTests(MdFilledTonalIconButton.styles);
|
createTokenTests(MdFilledTonalIconButton.styles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -44,73 +44,83 @@ describe('icon button tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('md-icon-button', () => {
|
describe('md-icon-button', () => {
|
||||||
it('setting `disabled` updates the disabled attribute on the native ' +
|
it(
|
||||||
'button element',
|
'setting `disabled` updates the disabled attribute on the native ' +
|
||||||
async () => {
|
'button element',
|
||||||
const {element} = await setUpTest('button');
|
async () => {
|
||||||
const button = element.shadowRoot!.querySelector('button')!;
|
const {element} = await setUpTest('button');
|
||||||
|
const button = element.shadowRoot!.querySelector('button')!;
|
||||||
|
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.hasAttribute('disabled')).toBeTrue();
|
expect(button.hasAttribute('disabled')).toBeTrue();
|
||||||
|
|
||||||
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(
|
||||||
'button element',
|
'setting `ariaLabel` updates the aria-label attribute on the native ' +
|
||||||
async () => {
|
'button element',
|
||||||
const {element} = await setUpTest('button');
|
async () => {
|
||||||
const button = element.shadowRoot!.querySelector('button')!;
|
const {element} = await setUpTest('button');
|
||||||
|
const button = element.shadowRoot!.querySelector('button')!;
|
||||||
|
|
||||||
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(
|
||||||
'tag',
|
'setting `ariaLabel` updates the aria-label attribute on the anchor' +
|
||||||
async () => {
|
'tag',
|
||||||
const {element} = await setUpTest('link');
|
async () => {
|
||||||
const anchor = element.shadowRoot!.querySelector('a')!;
|
const {element} = await setUpTest('link');
|
||||||
expect(anchor).not.toBeNull();
|
const anchor = element.shadowRoot!.querySelector('a')!;
|
||||||
|
expect(anchor).not.toBeNull();
|
||||||
|
|
||||||
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(
|
||||||
'button element',
|
'setting `disabled` updates the disabled attribute on the native ' +
|
||||||
async () => {
|
'button element',
|
||||||
const {element} = await setUpTest('toggle');
|
async () => {
|
||||||
const button = element.shadowRoot!.querySelector('button')!;
|
const {element} = await setUpTest('toggle');
|
||||||
|
const button = element.shadowRoot!.querySelector('button')!;
|
||||||
|
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.hasAttribute('disabled')).toBeTrue();
|
expect(button.hasAttribute('disabled')).toBeTrue();
|
||||||
|
|
||||||
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(
|
||||||
'button element',
|
'setting `ariaLabel` updates the aria-label attribute on the native ' +
|
||||||
async () => {
|
'button element',
|
||||||
const {element} = await setUpTest('toggle');
|
async () => {
|
||||||
const button = element.shadowRoot!.querySelector('button')!;
|
const {element} = await setUpTest('toggle');
|
||||||
|
const button = element.shadowRoot!.querySelector('button')!;
|
||||||
|
|
||||||
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,19 +145,18 @@ 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;
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
const button = element.shadowRoot!.querySelector('button')!;
|
const button = element.shadowRoot!.querySelector('button')!;
|
||||||
expect(button.getAttribute('aria-pressed')).toEqual('true');
|
expect(button.getAttribute('aria-pressed')).toEqual('true');
|
||||||
|
|
||||||
element.selected = false;
|
element.selected = false;
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(button.getAttribute('aria-pressed')).toEqual('false');
|
expect(button.getAttribute('aria-pressed')).toEqual('false');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('button with toggled aria label toggles aria label', async () => {
|
it('button with toggled aria label toggles aria label', async () => {
|
||||||
const {element, harness} = await setUpTest('toggle');
|
const {element, harness} = await setUpTest('toggle');
|
||||||
@ -167,45 +176,41 @@ describe('icon button tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('if `flipsIconInRtl=true`, flips icon in an RTL context', async () => {
|
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>
|
</div>`;
|
||||||
</div>`;
|
|
||||||
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).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`
|
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
||||||
<div dir="ltr">
|
star
|
||||||
<md-icon-button aria-label="Star" .flipIconInRtl="${true}">
|
</md-icon-button>
|
||||||
star
|
</div>`;
|
||||||
</md-icon-button>
|
const element = env.render(template).querySelector('md-icon-button')!;
|
||||||
</div>`;
|
await env.waitForStability();
|
||||||
const element = env.render(template).querySelector('md-icon-button')!;
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(element.selected).withContext('selected before click').toBeFalse();
|
expect(element.selected).withContext('selected before click').toBeFalse();
|
||||||
await harness.clickWithMouse();
|
await harness.clickWithMouse();
|
||||||
expect(element.selected)
|
expect(element.selected)
|
||||||
.withContext('selected after prevent default click')
|
.withContext('selected after prevent default click')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,15 +10,19 @@ 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';
|
||||||
|
|
||||||
// tslint:disable-next-line:enforce-comments-on-exported-symbols
|
// tslint:disable-next-line:enforce-comments-on-exported-symbols
|
||||||
export class IconButton extends LitElement implements FormSubmitter {
|
export class IconButton extends LitElement implements FormSubmitter {
|
||||||
@ -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.
|
||||||
@ -53,7 +59,7 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
/**
|
/**
|
||||||
* Sets the underlying `HTMLAnchorElement`'s `target` attribute.
|
* Sets the underlying `HTMLAnchorElement`'s `target` attribute.
|
||||||
*/
|
*/
|
||||||
@property() target: LinkTarget|'' = '';
|
@property() target: LinkTarget | '' = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `aria-label` of the button when the button is toggleable and selected.
|
* The `aria-label` of the button when the button is toggleable and selected.
|
||||||
@ -101,8 +107,8 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
@state() private flipIcon = isRtl(this, this.flipIconInRtl);
|
@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.
|
||||||
@ -119,18 +125,19 @@ export class IconButton extends LitElement implements FormSubmitter {
|
|||||||
const {ariaLabel, ariaHasPopup, ariaExpanded} = this as ARIAMixinStrict;
|
const {ariaLabel, ariaHasPopup, ariaExpanded} = this as ARIAMixinStrict;
|
||||||
const hasToggledAriaLabel = ariaLabel && this.ariaLabelSelected;
|
const hasToggledAriaLabel = ariaLabel && this.ariaLabelSelected;
|
||||||
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}));
|
||||||
|
@ -14,4 +14,4 @@ describe('<md-outlined-icon-button>', () => {
|
|||||||
describe('.styles', () => {
|
describe('.styles', () => {
|
||||||
createTokenTests(MdOutlinedIconButton.styles);
|
createTokenTests(MdOutlinedIconButton.styles);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -86,20 +86,25 @@ export function isAriaAttribute(attribute: string): attribute is ARIAAttribute {
|
|||||||
* @param property The aria property.
|
* @param property The aria property.
|
||||||
* @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
|
) {
|
||||||
.replace('aria', 'aria-')
|
return (
|
||||||
// IDREF attributes also include an "Element" or "Elements" suffix
|
property
|
||||||
.replace(/Elements?/g, '')
|
.replace('aria', 'aria-')
|
||||||
.toLowerCase() as ARIAPropertyToAttribute<K>;
|
// IDREF attributes also include an "Element" or "Elements" suffix
|
||||||
|
.replace(/Elements?/g, '')
|
||||||
|
.toLowerCase() as ARIAPropertyToAttribute<K>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts an `ariaFoo` string type to an `aria-foo` string type.
|
// 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
|
||||||
@ -118,68 +123,176 @@ type ARIAPropertyToAttribute<K extends string> =
|
|||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export interface ARIAMixinStrict extends ARIAMixin {
|
export interface ARIAMixinStrict extends ARIAMixin {
|
||||||
ariaAtomic: 'true'|'false'|null;
|
ariaAtomic: 'true' | 'false' | null;
|
||||||
ariaAutoComplete: 'none'|'inline'|'list'|'both'|null;
|
ariaAutoComplete: 'none' | 'inline' | 'list' | 'both' | null;
|
||||||
ariaBusy: 'true'|'false'|null;
|
ariaBusy: 'true' | 'false' | null;
|
||||||
ariaChecked: 'true'|'false'|null;
|
ariaChecked: 'true' | 'false' | null;
|
||||||
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:
|
||||||
ariaDisabled: 'true'|'false'|null;
|
| 'page'
|
||||||
ariaExpanded: 'true'|'false'|null;
|
| 'step'
|
||||||
ariaHasPopup: 'false'|'true'|'menu'|'listbox'|'tree'|'grid'|'dialog'|null;
|
| 'location'
|
||||||
ariaHidden: 'true'|'false'|null;
|
| 'date'
|
||||||
ariaInvalid: 'true'|'false'|null;
|
| 'time'
|
||||||
ariaKeyShortcuts: string|null;
|
| 'true'
|
||||||
ariaLabel: string|null;
|
| 'false'
|
||||||
ariaLevel: `${number}`|null;
|
| null;
|
||||||
ariaLive: 'assertive'|'off'|'polite'|null;
|
ariaDisabled: 'true' | 'false' | null;
|
||||||
ariaModal: 'true'|'false'|null;
|
ariaExpanded: 'true' | 'false' | null;
|
||||||
ariaMultiLine: 'true'|'false'|null;
|
ariaHasPopup:
|
||||||
ariaMultiSelectable: 'true'|'false'|null;
|
| 'false'
|
||||||
ariaOrientation: 'horizontal'|'vertical'|'undefined'|null;
|
| 'true'
|
||||||
ariaPlaceholder: string|null;
|
| 'menu'
|
||||||
ariaPosInSet: `${number}`|null;
|
| 'listbox'
|
||||||
ariaPressed: 'true'|'false'|null;
|
| 'tree'
|
||||||
ariaReadOnly: 'true'|'false'|null;
|
| 'grid'
|
||||||
ariaRequired: 'true'|'false'|null;
|
| 'dialog'
|
||||||
ariaRoleDescription: string|null;
|
| null;
|
||||||
ariaRowCount: `${number}`|null;
|
ariaHidden: 'true' | 'false' | null;
|
||||||
ariaRowIndex: `${number}`|null;
|
ariaInvalid: 'true' | 'false' | null;
|
||||||
ariaRowSpan: `${number}`|null;
|
ariaKeyShortcuts: string | null;
|
||||||
ariaSelected: 'true'|'false'|null;
|
ariaLabel: string | null;
|
||||||
ariaSetSize: `${number}`|null;
|
ariaLevel: `${number}` | null;
|
||||||
ariaSort: 'ascending'|'descending'|'none'|'other'|null;
|
ariaLive: 'assertive' | 'off' | 'polite' | null;
|
||||||
ariaValueMax: `${number}`|null;
|
ariaModal: 'true' | 'false' | null;
|
||||||
ariaValueMin: `${number}`|null;
|
ariaMultiLine: 'true' | 'false' | null;
|
||||||
ariaValueNow: `${number}`|null;
|
ariaMultiSelectable: 'true' | 'false' | null;
|
||||||
ariaValueText: string|null;
|
ariaOrientation: 'horizontal' | 'vertical' | 'undefined' | null;
|
||||||
role: ARIARole|null;
|
ariaPlaceholder: string | null;
|
||||||
|
ariaPosInSet: `${number}` | null;
|
||||||
|
ariaPressed: 'true' | 'false' | null;
|
||||||
|
ariaReadOnly: 'true' | 'false' | null;
|
||||||
|
ariaRequired: 'true' | 'false' | null;
|
||||||
|
ariaRoleDescription: string | null;
|
||||||
|
ariaRowCount: `${number}` | null;
|
||||||
|
ariaRowIndex: `${number}` | null;
|
||||||
|
ariaRowSpan: `${number}` | null;
|
||||||
|
ariaSelected: 'true' | 'false' | null;
|
||||||
|
ariaSetSize: `${number}` | null;
|
||||||
|
ariaSort: 'ascending' | 'descending' | 'none' | 'other' | null;
|
||||||
|
ariaValueMax: `${number}` | null;
|
||||||
|
ariaValueMin: `${number}` | null;
|
||||||
|
ariaValueNow: `${number}` | null;
|
||||||
|
ariaValueText: string | null;
|
||||||
|
role: ARIARole | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid values for `role`.
|
* 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,25 +388,29 @@ 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
|
||||||
for (const ariaProperty of ARIA_PROPERTIES) {
|
for (const ariaProperty of ARIA_PROPERTIES) {
|
||||||
let internalAriaValue: string|null = null;
|
let internalAriaValue: string | null = null;
|
||||||
Object.defineProperty(internals, ariaProperty, {
|
Object.defineProperty(internals, ariaProperty, {
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
get() {
|
||||||
return internalAriaValue;
|
return internalAriaValue;
|
||||||
},
|
},
|
||||||
set(value: string|null) {
|
set(value: string | null) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -303,14 +424,14 @@ export function polyfillElementInternalsAria(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let internalRoleValue: string|null = null;
|
let internalRoleValue: string | null = null;
|
||||||
Object.defineProperty(internals, 'role', {
|
Object.defineProperty(internals, 'role', {
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
get() {
|
get() {
|
||||||
return internalRoleValue;
|
return internalRoleValue;
|
||||||
},
|
},
|
||||||
set(value: string|null) {
|
set(value: string | null) {
|
||||||
const setRole = () => {
|
const setRole = () => {
|
||||||
internalRoleValue = value;
|
internalRoleValue = value;
|
||||||
|
|
||||||
@ -341,17 +462,17 @@ export function polyfillElementInternalsAria(
|
|||||||
|
|
||||||
hasBeenConnected = true;
|
hasBeenConnected = true;
|
||||||
|
|
||||||
const propertiesSetByUser = new Set<ARIAProperty|'role'>();
|
const propertiesSetByUser = new Set<ARIAProperty | 'role'>();
|
||||||
|
|
||||||
// See which properties were set by the user on host before we apply
|
// See which properties were set by the user on host before we apply
|
||||||
// internals values as attributes to host. Needs to be done in another
|
// internals values as attributes to host. Needs to be done in another
|
||||||
// for loop because the callbacks set these attributes on host.
|
// for loop because the callbacks set these attributes on host.
|
||||||
for (const {property} of firstConnectedCallbacks) {
|
for (const {property} of firstConnectedCallbacks) {
|
||||||
const wasSetByUser =
|
const wasSetByUser =
|
||||||
host.getAttribute(ariaPropertyToAttribute(property)) !== null ||
|
host.getAttribute(ariaPropertyToAttribute(property)) !== null ||
|
||||||
// Dynamic lookup rather than hardcoding all properties.
|
// Dynamic lookup rather than hardcoding all properties.
|
||||||
// tslint:disable-next-line:no-dict-access-on-struct-type
|
// tslint:disable-next-line:no-dict-access-on-struct-type
|
||||||
host[property] !== undefined;
|
host[property] !== undefined;
|
||||||
|
|
||||||
if (wasSetByUser) {
|
if (wasSetByUser) {
|
||||||
propertiesSetByUser.add(property);
|
propertiesSetByUser.add(property);
|
||||||
@ -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,20 +9,26 @@
|
|||||||
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()', () => {
|
||||||
it('should return true for aria value attributes', () => {
|
it('should return true for aria value attributes', () => {
|
||||||
expect(isAriaAttribute('aria-label'))
|
expect(isAriaAttribute('aria-label'))
|
||||||
.withContext('aria-label input')
|
.withContext('aria-label input')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for aria idref attributes', () => {
|
it('should return true for aria idref attributes', () => {
|
||||||
expect(isAriaAttribute('aria-labelledby'))
|
expect(isAriaAttribute('aria-labelledby'))
|
||||||
.withContext('aria-labelledby input')
|
.withContext('aria-labelledby input')
|
||||||
.toBeTrue();
|
.toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for role', () => {
|
it('should return false for role', () => {
|
||||||
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -60,16 +67,16 @@ describe('aria', () => {
|
|||||||
it('should not hydrate tabindex attribute on creation', () => {
|
it('should not hydrate tabindex attribute on creation', () => {
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
expect(element.hasAttribute('tabindex'))
|
expect(element.hasAttribute('tabindex'))
|
||||||
.withContext('has tabindex attribute')
|
.withContext('has tabindex attribute')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set tabindex="0" on element once connected', () => {
|
it('should set tabindex="0" on element once connected', () => {
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
expect(element.getAttribute('tabindex'))
|
expect(element.getAttribute('tabindex'))
|
||||||
.withContext('tabindex attribute value')
|
.withContext('tabindex attribute value')
|
||||||
.toEqual('0');
|
.toEqual('0');
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
@ -79,8 +86,8 @@ describe('aria', () => {
|
|||||||
element.tabIndex = -1;
|
element.tabIndex = -1;
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
expect(element.getAttribute('tabindex'))
|
expect(element.getAttribute('tabindex'))
|
||||||
.withContext('tabindex attribute value')
|
.withContext('tabindex attribute value')
|
||||||
.toEqual('-1');
|
.toEqual('-1');
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
@ -92,24 +99,23 @@ describe('aria', () => {
|
|||||||
element.remove();
|
element.remove();
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
expect(element.getAttribute('tabindex'))
|
expect(element.getAttribute('tabindex'))
|
||||||
.withContext('tabindex attribute value')
|
.withContext('tabindex attribute value')
|
||||||
.toEqual('-1');
|
.toEqual('-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
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';
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('role'))
|
||||||
expect(element.getAttribute('role'))
|
.withContext('role attribute value')
|
||||||
.withContext('role attribute value')
|
.toEqual('button');
|
||||||
.toEqual('button');
|
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -138,8 +144,8 @@ describe('aria', () => {
|
|||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
expect(element.hasAttribute('role'))
|
expect(element.hasAttribute('role'))
|
||||||
.withContext('has role attribute')
|
.withContext('has role attribute')
|
||||||
.toBeFalse();
|
.toBeFalse();
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
@ -148,43 +154,41 @@ describe('aria', () => {
|
|||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
// TestElement() sets role in constructor
|
// TestElement() sets role in constructor
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('ElementInternals.role')
|
.withContext('ElementInternals.role')
|
||||||
.toEqual('button');
|
.toEqual('button');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should preserve aria values when set before connected', () => {
|
it('should preserve aria values when set before connected', () => {
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
element.internals.ariaLabel = 'Foo';
|
element.internals.ariaLabel = 'Foo';
|
||||||
expect(element.internals.ariaLabel)
|
expect(element.internals.ariaLabel)
|
||||||
.withContext('ElementInternals.ariaLabel')
|
.withContext('ElementInternals.ariaLabel')
|
||||||
.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);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('role'))
|
||||||
expect(element.getAttribute('role'))
|
.withContext('role attribute value')
|
||||||
.withContext('role attribute value')
|
.toEqual('button');
|
||||||
.toEqual('button');
|
|
||||||
|
|
||||||
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);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('aria-label'))
|
||||||
expect(element.getAttribute('aria-label'))
|
.withContext('aria-label attribute value')
|
||||||
.withContext('aria-label attribute value')
|
.toEqual('Foo');
|
||||||
.toEqual('Foo');
|
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set aria attributes when set after connection', async () => {
|
it('should set aria attributes when set after connection', async () => {
|
||||||
const element = new TestElement();
|
const element = new TestElement();
|
||||||
@ -192,180 +196,171 @@ describe('aria', () => {
|
|||||||
element.internals.ariaLabel = 'Value after construction';
|
element.internals.ariaLabel = 'Value after construction';
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(element.getAttribute('aria-label'))
|
expect(element.getAttribute('aria-label'))
|
||||||
.withContext('aria-label attribute value')
|
.withContext('aria-label attribute value')
|
||||||
.toEqual('Value after construction');
|
.toEqual('Value after construction');
|
||||||
|
|
||||||
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';
|
document.body.appendChild(element);
|
||||||
document.body.appendChild(element);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('aria-label'))
|
||||||
expect(element.getAttribute('aria-label'))
|
.withContext('aria-label attribute value on host')
|
||||||
.withContext('aria-label attribute value on host')
|
.toEqual('Value set by user');
|
||||||
.toEqual('Value set by user');
|
expect(element.internals.ariaLabel)
|
||||||
expect(element.internals.ariaLabel)
|
.withContext('ariaLabel internals property still the same')
|
||||||
.withContext('ariaLabel internals property still the same')
|
.toEqual('Value set on internals');
|
||||||
.toEqual('Value set on internals');
|
|
||||||
|
|
||||||
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';
|
document.body.appendChild(element);
|
||||||
document.body.appendChild(element);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('aria-label'))
|
||||||
expect(element.getAttribute('aria-label'))
|
.withContext('aria-label attribute value on host')
|
||||||
.withContext('aria-label attribute value on host')
|
.toEqual('Value set by user');
|
||||||
.toEqual('Value set by user');
|
expect(element.ariaLabel)
|
||||||
expect(element.ariaLabel)
|
.withContext('ariaLabel property value on host')
|
||||||
.withContext('ariaLabel property value on host')
|
.toEqual('Value set by user');
|
||||||
.toEqual('Value set by user');
|
expect(element.internals.ariaLabel)
|
||||||
expect(element.internals.ariaLabel)
|
.withContext('ariaLabel internals property still the same')
|
||||||
.withContext('ariaLabel internals property still the same')
|
.toEqual('Value set on internals');
|
||||||
.toEqual('Value set on internals');
|
|
||||||
|
|
||||||
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';
|
document.body.appendChild(element);
|
||||||
document.body.appendChild(element);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('role'))
|
||||||
expect(element.getAttribute('role'))
|
.withContext('role attribute value on host')
|
||||||
.withContext('role attribute value on host')
|
.toEqual('Value set by user');
|
||||||
.toEqual('Value set by user');
|
expect(element.internals.role)
|
||||||
expect(element.internals.role)
|
.withContext('role internals property still the same')
|
||||||
.withContext('role internals property still the same')
|
.toEqual('Value set on internals');
|
||||||
.toEqual('Value set on internals');
|
|
||||||
|
|
||||||
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';
|
document.body.appendChild(element);
|
||||||
document.body.appendChild(element);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
expect(element.getAttribute('role'))
|
||||||
expect(element.getAttribute('role'))
|
.withContext('role attribute value on host')
|
||||||
.withContext('role attribute value on host')
|
.toEqual('Value set by user');
|
||||||
.toEqual('Value set by user');
|
expect(element.role)
|
||||||
expect(element.role)
|
.withContext('role property value on host')
|
||||||
.withContext('role property value on host')
|
.toEqual('Value set by user');
|
||||||
.toEqual('Value set by user');
|
expect(element.internals.role)
|
||||||
expect(element.internals.role)
|
.withContext('role internals property still the same')
|
||||||
.withContext('role internals property still the same')
|
.toEqual('Value set on internals');
|
||||||
.toEqual('Value set on internals');
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('internals.role before connection')
|
.withContext('internals.role before connection')
|
||||||
.toEqual('checkbox');
|
.toEqual('checkbox');
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('internals.role after connection')
|
.withContext('internals.role after connection')
|
||||||
.toEqual('checkbox');
|
.toEqual('checkbox');
|
||||||
|
|
||||||
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';
|
element.internals.role = 'checkbox';
|
||||||
element.internals.role = 'checkbox';
|
|
||||||
|
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('internals.role before connection')
|
.withContext('internals.role before connection')
|
||||||
.toEqual('checkbox');
|
.toEqual('checkbox');
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('internals.role after connection')
|
.withContext('internals.role after connection')
|
||||||
.toEqual('checkbox');
|
.toEqual('checkbox');
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
expect(element.internals.ariaLabel)
|
expect(element.internals.ariaLabel)
|
||||||
.withContext('internals.ariaLabel before connection')
|
.withContext('internals.ariaLabel before connection')
|
||||||
.toEqual('Second');
|
.toEqual('Second');
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(element.internals.ariaLabel)
|
expect(element.internals.ariaLabel)
|
||||||
.withContext('internals.ariaLabel after connection')
|
.withContext('internals.ariaLabel after connection')
|
||||||
.toEqual('Second');
|
.toEqual('Second');
|
||||||
|
|
||||||
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';
|
element.internals.ariaLabel = 'Second';
|
||||||
element.internals.ariaLabel = 'Second';
|
|
||||||
|
|
||||||
expect(element.internals.ariaLabel)
|
expect(element.internals.ariaLabel)
|
||||||
.withContext('internals.ariaLabel before connection')
|
.withContext('internals.ariaLabel before connection')
|
||||||
.toEqual('Second');
|
.toEqual('Second');
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(element.internals.ariaLabel)
|
expect(element.internals.ariaLabel)
|
||||||
.withContext('internals.ariaLabel after connection')
|
.withContext('internals.ariaLabel after connection')
|
||||||
.toEqual('Second');
|
.toEqual('Second');
|
||||||
|
|
||||||
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);
|
await element.updateComplete;
|
||||||
await element.updateComplete;
|
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
element.internals.role = 'checkbox';
|
element.internals.role = 'checkbox';
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('internals.role after connected and disconnected')
|
.withContext('internals.role after connected and disconnected')
|
||||||
.toEqual('checkbox');
|
.toEqual('checkbox');
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
await element.updateComplete;
|
await element.updateComplete;
|
||||||
expect(element.internals.role)
|
expect(element.internals.role)
|
||||||
.withContext('internals.role after reconnected')
|
.withContext('internals.role after reconnected')
|
||||||
.toEqual('checkbox');
|
.toEqual('checkbox');
|
||||||
|
|
||||||
element.remove();
|
element.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user