mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
feat: add ESLint, improve types, improve a11y
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,33 +1,31 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlAlert from './alert';
|
||||
|
||||
describe('<sl-alert>', () => {
|
||||
it('should be visible with the open attribute', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
|
||||
it('should not be visible without the open attribute', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
|
||||
it('should emit sl-show and sl-after-show when calling show()', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
@@ -39,13 +37,13 @@ describe('<sl-alert>', () => {
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when calling hide()', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
void el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
@@ -57,7 +55,7 @@ describe('<sl-alert>', () => {
|
||||
|
||||
it('should emit sl-show and sl-after-show when setting open = true', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -75,7 +73,7 @@ describe('<sl-alert>', () => {
|
||||
|
||||
it('should emit sl-hide and sl-after-hide when setting open = false', async () => {
|
||||
const el = await fixture<SlAlert>(html` <sl-alert open>I am an alert</sl-alert> `);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import styles from './alert.styles';
|
||||
|
||||
import '../icon-button/icon-button';
|
||||
import '~/components/icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||
import { emit, waitForEvent } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry';
|
||||
|
||||
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
|
||||
|
||||
@@ -41,7 +39,7 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
|
||||
export default class SlAlert extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private autoHideTimeout: any;
|
||||
private autoHideTimeout: NodeJS.Timeout;
|
||||
|
||||
@query('[part="base"]') base: HTMLElement;
|
||||
|
||||
@@ -58,7 +56,7 @@ export default class SlAlert extends LitElement {
|
||||
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
|
||||
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`.
|
||||
*/
|
||||
@property({ type: Number }) duration: number = Infinity;
|
||||
@property({ type: Number }) duration = Infinity;
|
||||
|
||||
firstUpdated() {
|
||||
this.base.hidden = !this.open;
|
||||
@@ -67,7 +65,7 @@ export default class SlAlert extends LitElement {
|
||||
/** Shows the alert. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
@@ -77,7 +75,7 @@ export default class SlAlert extends LitElement {
|
||||
/** Hides the alert */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
@@ -91,7 +89,7 @@ export default class SlAlert extends LitElement {
|
||||
*/
|
||||
async toast() {
|
||||
return new Promise<void>(resolve => {
|
||||
if (!toastStack.parentElement) {
|
||||
if (toastStack.parentElement === null) {
|
||||
document.body.append(toastStack);
|
||||
}
|
||||
|
||||
@@ -99,8 +97,9 @@ export default class SlAlert extends LitElement {
|
||||
|
||||
// Wait for the toast stack to render
|
||||
requestAnimationFrame(() => {
|
||||
this.clientWidth; // force a reflow for the initial transition
|
||||
this.show();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
|
||||
this.clientWidth;
|
||||
void this.show();
|
||||
});
|
||||
|
||||
this.addEventListener(
|
||||
@@ -110,7 +109,7 @@ export default class SlAlert extends LitElement {
|
||||
resolve();
|
||||
|
||||
// Remove the toast stack from the DOM when there are no more alerts
|
||||
if (!toastStack.querySelector('sl-alert')) {
|
||||
if (toastStack.querySelector('sl-alert') === null) {
|
||||
toastStack.remove();
|
||||
}
|
||||
},
|
||||
@@ -122,12 +121,14 @@ export default class SlAlert extends LitElement {
|
||||
restartAutoHide() {
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
if (this.open && this.duration < Infinity) {
|
||||
this.autoHideTimeout = setTimeout(() => this.hide(), this.duration);
|
||||
this.autoHideTimeout = setTimeout(() => {
|
||||
void this.hide();
|
||||
}, this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
this.hide();
|
||||
void this.hide();
|
||||
}
|
||||
|
||||
handleMouseMove() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlAnimatedImage from './animated-image';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('<sl-animated-image>', () => {
|
||||
it('should render a component', async () => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { emit } from '../../internal/event';
|
||||
import styles from './animated-image.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
import { emit } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -63,7 +62,7 @@ export default class SlAnimatedImage extends LitElement {
|
||||
}
|
||||
|
||||
@watch('play')
|
||||
async handlePlayChange() {
|
||||
handlePlayChange() {
|
||||
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
||||
// won't trigger another request.
|
||||
if (this.play) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, queryAsync } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { animations } from './animations';
|
||||
import styles from './animation.styles';
|
||||
import { animations } from './animations';
|
||||
import { emit } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -20,7 +20,7 @@ import styles from './animation.styles';
|
||||
export default class SlAnimation extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private animation: Animation;
|
||||
private animation?: Animation;
|
||||
private hasStarted = false;
|
||||
|
||||
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
|
||||
@@ -56,13 +56,13 @@ export default class SlAnimation extends LitElement {
|
||||
@property() fill: FillMode = 'auto';
|
||||
|
||||
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
|
||||
@property({ type: Number }) iterations: number = Infinity;
|
||||
@property({ type: Number }) iterations = Infinity;
|
||||
|
||||
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
|
||||
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
|
||||
|
||||
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
|
||||
@property({ attribute: false }) keyframes: Keyframe[];
|
||||
@property({ attribute: false }) keyframes?: Keyframe[];
|
||||
|
||||
/**
|
||||
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
|
||||
@@ -73,18 +73,18 @@ export default class SlAnimation extends LitElement {
|
||||
|
||||
/** Gets and sets the current animation time. */
|
||||
get currentTime(): number {
|
||||
return this.animation?.currentTime || 0;
|
||||
return this.animation?.currentTime ?? 0;
|
||||
}
|
||||
|
||||
set currentTime(time: number) {
|
||||
if (this.animation) {
|
||||
if (typeof this.animation !== 'undefined') {
|
||||
this.animation.currentTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.createAnimation();
|
||||
void this.createAnimation();
|
||||
this.handleAnimationCancel = this.handleAnimationCancel.bind(this);
|
||||
this.handleAnimationFinish = this.handleAnimationFinish.bind(this);
|
||||
}
|
||||
@@ -104,12 +104,12 @@ export default class SlAnimation extends LitElement {
|
||||
@watch('iterations')
|
||||
@watch('iterationsStart')
|
||||
@watch('keyframes')
|
||||
async handleAnimationChange() {
|
||||
handleAnimationChange() {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createAnimation();
|
||||
void this.createAnimation();
|
||||
}
|
||||
|
||||
handleAnimationFinish() {
|
||||
@@ -126,39 +126,43 @@ export default class SlAnimation extends LitElement {
|
||||
|
||||
@watch('play')
|
||||
handlePlayChange() {
|
||||
if (this.animation) {
|
||||
if (typeof this.animation !== 'undefined') {
|
||||
if (this.play && !this.hasStarted) {
|
||||
this.hasStarted = true;
|
||||
emit(this, 'sl-start');
|
||||
}
|
||||
|
||||
this.play ? this.animation.play() : this.animation.pause();
|
||||
if (this.play) {
|
||||
this.animation.play();
|
||||
} else {
|
||||
this.animation.pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@watch('playbackRate')
|
||||
handlePlaybackRateChange() {
|
||||
if (this.animation) {
|
||||
if (typeof this.animation !== 'undefined') {
|
||||
this.animation.playbackRate = this.playbackRate;
|
||||
}
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
this.destroyAnimation();
|
||||
this.createAnimation();
|
||||
void this.createAnimation();
|
||||
}
|
||||
|
||||
async createAnimation() {
|
||||
const easing = animations.easings[this.easing] || this.easing;
|
||||
const keyframes: Keyframe[] = this.keyframes ? this.keyframes : (animations as any)[this.name];
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- The specified easing may not exist
|
||||
const easing = animations.easings[this.easing] ?? this.easing;
|
||||
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
|
||||
const slot = await this.defaultSlot;
|
||||
const element = slot.assignedElements()[0] as HTMLElement;
|
||||
const element = slot.assignedElements()[0] as HTMLElement | undefined;
|
||||
|
||||
if (!element) {
|
||||
if (typeof element === 'undefined' || typeof keyframes === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -188,7 +192,7 @@ export default class SlAnimation extends LitElement {
|
||||
}
|
||||
|
||||
destroyAnimation() {
|
||||
if (this.animation) {
|
||||
if (typeof this.animation !== 'undefined') {
|
||||
this.animation.cancel();
|
||||
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
|
||||
this.animation.removeEventListener('finish', this.handleAnimationFinish);
|
||||
@@ -198,16 +202,12 @@ export default class SlAnimation extends LitElement {
|
||||
|
||||
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */
|
||||
cancel() {
|
||||
try {
|
||||
this.animation.cancel();
|
||||
} catch {}
|
||||
this.animation?.cancel();
|
||||
}
|
||||
|
||||
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
|
||||
finish() {
|
||||
try {
|
||||
this.animation.finish();
|
||||
} catch {}
|
||||
this.animation?.finish();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlAvatar from './avatar';
|
||||
|
||||
describe('<sl-avatar>', () => {
|
||||
let el: SlAvatar;
|
||||
|
||||
describe('when provided no parameters', async () => {
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html` <sl-avatar label="Avatar"></sl-avatar> `);
|
||||
});
|
||||
@@ -15,14 +13,14 @@ describe('<sl-avatar>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should default to circle styling', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('should default to circle styling', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(el.getAttribute('shape')).to.eq('circle');
|
||||
expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an image and label parameter', async () => {
|
||||
describe('when provided an image and label parameter', () => {
|
||||
const image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||
const label = 'Small transparent square';
|
||||
before(async () => {
|
||||
@@ -40,20 +38,20 @@ describe('<sl-avatar>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('renders "image" part, with src and a role of presentation', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
|
||||
it('renders "image" part, with src and a role of presentation', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="image"]')!;
|
||||
|
||||
expect(part.getAttribute('src')).to.eq(image);
|
||||
});
|
||||
|
||||
it('renders the label attribute in the "base" part', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('renders the label attribute in the "base" part', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
|
||||
expect(part.getAttribute('aria-label')).to.eq(label);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided initials parameter', async () => {
|
||||
describe('when provided initials parameter', () => {
|
||||
const initials = 'SL';
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}" label="Avatar"></sl-avatar>`);
|
||||
@@ -63,8 +61,8 @@ describe('<sl-avatar>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('renders "initials" part, with initials as the text node', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="initials"]') as HTMLImageElement;
|
||||
it('renders "initials" part, with initials as the text node', () => {
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="initials"]')!;
|
||||
|
||||
expect(part.innerText).to.eq(initials);
|
||||
});
|
||||
@@ -73,15 +71,15 @@ describe('<sl-avatar>', () => {
|
||||
['square', 'rounded', 'circle'].forEach(shape => {
|
||||
describe(`when passed a shape attribute ${shape}`, () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape as any}" label="Shaped avatar"></sl-avatar>`);
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape}" label="Shaped avatar"></sl-avatar>`);
|
||||
});
|
||||
|
||||
it('passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('appends the appropriate class on the "base" part', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('appends the appropriate class on the "base" part', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
|
||||
expect(el.getAttribute('shape')).to.eq(shape);
|
||||
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
|
||||
@@ -89,7 +87,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed a <span>, on slot "icon"', async () => {
|
||||
describe('when passed a <span>, on slot "icon"', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar label="Avatar"><span slot="icon">random content</span></sl-avatar>`);
|
||||
});
|
||||
@@ -98,13 +96,13 @@ describe('<sl-avatar>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=icon]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
it('should accept as an assigned child in the shadow root', () => {
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=icon]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true }) as HTMLElement[];
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
|
||||
const span = <HTMLElement>childNodes[0];
|
||||
const span = childNodes[0];
|
||||
expect(span.innerHTML).to.eq('random content');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,7 @@ import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import styles from './avatar.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -27,13 +26,13 @@ export default class SlAvatar extends LitElement {
|
||||
@state() private hasError = false;
|
||||
|
||||
/** The image source to use for the avatar. */
|
||||
@property() image: string;
|
||||
@property() image?: string;
|
||||
|
||||
/** A label to use to describe the avatar to assistive devices. */
|
||||
@property() label: string;
|
||||
@property() label = '';
|
||||
|
||||
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
|
||||
@property() initials: string;
|
||||
@property() initials?: string;
|
||||
|
||||
/** The shape of the avatar. */
|
||||
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
|
||||
@@ -51,7 +50,7 @@ export default class SlAvatar extends LitElement {
|
||||
role="img"
|
||||
aria-label=${this.label}
|
||||
>
|
||||
${this.initials
|
||||
${typeof this.initials !== 'undefined'
|
||||
? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
|
||||
: html`
|
||||
<div part="icon" class="avatar__icon" aria-hidden="true">
|
||||
@@ -60,7 +59,7 @@ export default class SlAvatar extends LitElement {
|
||||
</slot>
|
||||
</div>
|
||||
`}
|
||||
${this.image && !this.hasError
|
||||
${typeof this.image !== 'undefined' && !this.hasError
|
||||
? html`
|
||||
<img
|
||||
part="image"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlBadge from './badge';
|
||||
|
||||
describe('<sl-badge>', () => {
|
||||
let el: SlBadge;
|
||||
|
||||
describe('when provided no parameters', async () => {
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBadge>(html` <sl-badge>Badge</sl-badge> `);
|
||||
});
|
||||
@@ -14,21 +12,21 @@ describe('<sl-badge>', () => {
|
||||
it('should render a component that passes accessibility test, with a role of status on the base part.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(part.getAttribute('role')).to.eq('status');
|
||||
});
|
||||
|
||||
it('should render the child content provided', async () => {
|
||||
it('should render the child content provided', () => {
|
||||
expect(el.innerText).to.eq('Badge');
|
||||
});
|
||||
|
||||
it('should default to square styling, with the primary color', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('should default to square styling, with the primary color', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq('badge badge--primary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a pill parameter', async () => {
|
||||
describe('when provided a pill parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `);
|
||||
});
|
||||
@@ -37,13 +35,13 @@ describe('<sl-badge>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should append the pill class to the classlist to render a pill', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('should append the pill class to the classlist to render a pill', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a pulse parameter', async () => {
|
||||
describe('when provided a pulse parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `);
|
||||
});
|
||||
@@ -52,8 +50,8 @@ describe('<sl-badge>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should append the pulse class to the classlist to render a pulse', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('should append the pulse class to the classlist to render a pulse', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse');
|
||||
});
|
||||
});
|
||||
@@ -61,15 +59,15 @@ describe('<sl-badge>', () => {
|
||||
['primary', 'success', 'neutral', 'warning', 'danger'].forEach(variant => {
|
||||
describe(`when passed a variant attribute ${variant}`, () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBadge>(html`<sl-badge variant="${variant as any}">Badge</sl-badge>`);
|
||||
el = await fixture<SlBadge>(html`<sl-badge variant="${variant}">Badge</sl-badge>`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should default to square styling, with the primary color', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
it('should default to square styling, with the primary color', () => {
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(part.classList.value.trim()).to.eq(`badge badge--${variant}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlBreadcrumbItem from './breadcrumb-item';
|
||||
|
||||
describe('<sl-breadcrumb-item>', () => {
|
||||
let el: SlBreadcrumbItem;
|
||||
|
||||
describe('when not provided a href attribute', async () => {
|
||||
describe('when not provided a href attribute', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `);
|
||||
});
|
||||
@@ -15,19 +13,19 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should hide the separator from screen readers', async () => {
|
||||
const separator: HTMLSpanElement = el.shadowRoot.querySelector('[part="separator"]');
|
||||
it('should hide the separator from screen readers', () => {
|
||||
const separator = el.shadowRoot!.querySelector<HTMLSpanElement>('[part="separator"]');
|
||||
expect(separator).attribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
|
||||
const button: HTMLButtonElement = el.shadowRoot.querySelector('[part="label"]');
|
||||
const button = el.shadowRoot!.querySelector<HTMLButtonElement>('[part="label"]');
|
||||
expect(button).to.exist;
|
||||
expect(button).attribute('type', 'button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a href attribute', async () => {
|
||||
describe('when provided a href attribute', () => {
|
||||
describe('and no target', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
@@ -40,7 +38,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
|
||||
const hyperlink: HTMLAnchorElement = el.shadowRoot.querySelector('[part="label"]');
|
||||
const hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
|
||||
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
||||
});
|
||||
});
|
||||
@@ -57,10 +55,10 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
describe('should render a HTMLAnchorElement as the part "label"', () => {
|
||||
let hyperlink: HTMLAnchorElement;
|
||||
let hyperlink: HTMLAnchorElement | null;
|
||||
|
||||
before(() => {
|
||||
hyperlink = el.shadowRoot.querySelector('[part="label"]');
|
||||
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('[part="label"]');
|
||||
});
|
||||
|
||||
it('should use the supplied href value, as the href attribute value', () => {
|
||||
@@ -87,10 +85,10 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
describe('should render a HTMLAnchorElement', () => {
|
||||
let hyperlink: HTMLAnchorElement;
|
||||
let hyperlink: HTMLAnchorElement | null;
|
||||
|
||||
before(() => {
|
||||
hyperlink = el.shadowRoot.querySelector('a');
|
||||
hyperlink = el.shadowRoot!.querySelector<HTMLAnchorElement>('a');
|
||||
});
|
||||
|
||||
it('should use the supplied href value, as the href attribute value', () => {
|
||||
@@ -104,7 +102,7 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "prefix" to support prefix icons', async () => {
|
||||
describe('when provided an element in the slot "prefix" to support prefix icons', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item>
|
||||
@@ -119,19 +117,19 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=prefix]');
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=prefix]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const part = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "suffix" to support suffix icons', async () => {
|
||||
describe('when provided an element in the slot "suffix" to support suffix icons', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item>
|
||||
@@ -146,14 +144,14 @@ describe('<sl-breadcrumb-item>', () => {
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=suffix]');
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=suffix]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const part = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
expect(part.classList.value.trim()).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@ import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './breadcrumb-item.styles';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -25,22 +25,22 @@ import styles from './breadcrumb-item.styles';
|
||||
export default class SlBreadcrumbItem extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
|
||||
private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix');
|
||||
|
||||
/**
|
||||
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
|
||||
* internally. When unset, a button will be rendered instead.
|
||||
*/
|
||||
@property() href: string;
|
||||
@property() href?: string;
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** The `rel` attribute to use on the link. Only used when `href` is set. */
|
||||
@property() rel: string = 'noreferrer noopener';
|
||||
@property() rel = 'noreferrer noopener';
|
||||
|
||||
render() {
|
||||
const isLink = this.href ? true : false;
|
||||
const isLink = typeof this.href !== 'undefined';
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -62,7 +62,7 @@ export default class SlBreadcrumbItem extends LitElement {
|
||||
class="breadcrumb-item__label breadcrumb-item__label--link"
|
||||
href="${this.href}"
|
||||
target="${this.target}"
|
||||
rel=${ifDefined(this.target ? this.rel : undefined)}
|
||||
rel=${ifDefined(typeof this.target !== 'undefined' ? this.rel : undefined)}
|
||||
>
|
||||
<slot></slot>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlBreadcrumb from './breadcrumb';
|
||||
|
||||
describe('<sl-breadcrumb>', () => {
|
||||
let el: SlBreadcrumb;
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and no parameters', async () => {
|
||||
describe('when provided a standard list of el-breadcrumb-item children and no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
@@ -22,18 +20,18 @@ describe('<sl-breadcrumb>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render sl-icon as separator', async () => {
|
||||
it('should render sl-icon as separator', () => {
|
||||
expect(el.querySelectorAll('sl-icon').length).to.eq(4);
|
||||
});
|
||||
|
||||
it('should attach aria-current "page" on the last breadcrumb item.', async () => {
|
||||
it('should attach aria-current "page" on the last breadcrumb item.', () => {
|
||||
const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item');
|
||||
const lastNode = breadcrumbItems[3];
|
||||
expect(lastNode).attribute('aria-current', 'page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "separator" to support Custom Separators', async () => {
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "separator" to support Custom Separators', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
@@ -49,20 +47,20 @@ describe('<sl-breadcrumb>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should accept "separator" as an assigned child in the shadow root', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=separator]');
|
||||
it('should accept "separator" as an assigned child in the shadow root', () => {
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=separator]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should replace the sl-icon separator with the provided separator', async () => {
|
||||
it('should replace the sl-icon separator with the provided separator', () => {
|
||||
expect(el.querySelectorAll('.replacement-separator').length).to.eq(4);
|
||||
expect(el.querySelectorAll('sl-icon').length).to.eq(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', async () => {
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
@@ -82,7 +80,7 @@ describe('<sl-breadcrumb>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', async () => {
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import styles from './breadcrumb.styles';
|
||||
import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item';
|
||||
|
||||
import '../icon/icon';
|
||||
import type SlBreadcrumbItem from '~/components/breadcrumb-item/breadcrumb-item';
|
||||
import '~/components/icon/icon';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -35,7 +34,9 @@ export default class SlBreadcrumb extends LitElement {
|
||||
|
||||
// Clone it, remove ids, and slot it
|
||||
const clone = separator.cloneNode(true) as HTMLElement;
|
||||
[clone, ...clone.querySelectorAll('[id]')].map(el => el.removeAttribute('id'));
|
||||
[clone, ...clone.querySelectorAll('[id]')].forEach(el => {
|
||||
el.removeAttribute('id');
|
||||
});
|
||||
clone.slot = 'separator';
|
||||
|
||||
return clone;
|
||||
@@ -46,10 +47,10 @@ export default class SlBreadcrumb extends LitElement {
|
||||
item => item.tagName.toLowerCase() === 'sl-breadcrumb-item'
|
||||
) as SlBreadcrumbItem[];
|
||||
|
||||
items.map((item, index) => {
|
||||
items.forEach((item, index) => {
|
||||
// Append separators to each item if they don't already have one
|
||||
const separator = item.querySelector('[slot="separator"]') as HTMLElement;
|
||||
if (!separator) {
|
||||
const separator = item.querySelector('[slot="separator"]');
|
||||
if (separator === null) {
|
||||
item.append(this.getSeparator());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -42,11 +42,11 @@ export default class SlButtonGroup extends LitElement {
|
||||
handleSlotChange() {
|
||||
const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[];
|
||||
|
||||
slottedElements.map(el => {
|
||||
slottedElements.forEach(el => {
|
||||
const index = slottedElements.indexOf(el);
|
||||
const button = findButton(el);
|
||||
|
||||
if (button) {
|
||||
if (button !== null) {
|
||||
button.classList.add('sl-button-group__button');
|
||||
button.classList.toggle('sl-button-group__button--first', index === 0);
|
||||
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
||||
@@ -56,6 +56,7 @@ export default class SlButtonGroup extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line lit-a11y/mouse-events-have-key-events -- focusout & focusin support bubbling where as focus & blur do not which is necessary here
|
||||
return html`
|
||||
<div
|
||||
part="base"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import styles from './button.styles';
|
||||
|
||||
import '../spinner/spinner';
|
||||
import '~/components/spinner/spinner';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController } from '~/internal/form-control';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -35,8 +34,8 @@ export default class SlButton extends LitElement {
|
||||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
|
||||
@state() private hasFocus = false;
|
||||
|
||||
@@ -72,19 +71,19 @@ export default class SlButton extends LitElement {
|
||||
@property() type: 'button' | 'submit' = 'button';
|
||||
|
||||
/** An optional name for the button. Ignored when `href` is set. */
|
||||
@property() name: string;
|
||||
@property() name?: string;
|
||||
|
||||
/** An optional value for the button. Ignored when `href` is set. */
|
||||
@property() value: string;
|
||||
@property() value?: string;
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property() href: string;
|
||||
@property() href?: string;
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||
@property() download: string;
|
||||
@property() download?: string;
|
||||
|
||||
/** Simulates a click on the button. */
|
||||
click() {
|
||||
@@ -124,9 +123,10 @@ export default class SlButton extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.href ? true : false;
|
||||
const isLink = typeof this.href !== 'undefined';
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
|
||||
/* eslint-disable lit/binding-positions, lit/no-invalid-html */
|
||||
return html`
|
||||
<${tag}
|
||||
part="base"
|
||||
@@ -161,7 +161,7 @@ export default class SlButton extends LitElement {
|
||||
href=${ifDefined(this.href)}
|
||||
target=${ifDefined(this.target)}
|
||||
download=${ifDefined(this.download)}
|
||||
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
|
||||
rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
|
||||
role="button"
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
tabindex=${this.disabled ? '-1' : '0'}
|
||||
@@ -199,6 +199,7 @@ export default class SlButton extends LitElement {
|
||||
${this.loading ? html`<sl-spinner></sl-spinner>` : ''}
|
||||
</${tag}>
|
||||
`;
|
||||
/* eslint-enable lit/binding-positions, lit/no-invalid-html */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlCard from './card';
|
||||
|
||||
describe('<sl-card>', () => {
|
||||
let el: SlCard;
|
||||
|
||||
describe('when provided no parameters', async () => {
|
||||
describe('when provided no parameters', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
|
||||
@@ -17,17 +15,17 @@ describe('<sl-card>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
it('should render the child content provided.', () => {
|
||||
expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.');
|
||||
});
|
||||
|
||||
it('should contain the class card.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
it('should contain the class card.', () => {
|
||||
const card = el.shadowRoot!.querySelector('.card')!;
|
||||
expect(card.classList.value.trim()).to.eq('card');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "header" to render a header', async () => {
|
||||
describe('when provided an element in the slot "header" to render a header', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html`<sl-card>
|
||||
@@ -41,29 +39,29 @@ describe('<sl-card>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
it('should render the child content provided.', () => {
|
||||
expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!');
|
||||
});
|
||||
|
||||
it('render the header content provided.', async () => {
|
||||
const header = <HTMLDivElement>el.querySelector('div[slot=header]');
|
||||
it('render the header content provided.', () => {
|
||||
const header = el.querySelector<HTMLElement>('div[slot=header]')!;
|
||||
expect(header.innerText).eq('Header Title');
|
||||
});
|
||||
|
||||
it('accept "header" as an assigned child in the shadow root.', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=header]');
|
||||
it('accept "header" as an assigned child in the shadow root.', () => {
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=header]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should contain the class card--has-header.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
it('should contain the class card--has-header.', () => {
|
||||
const card = el.shadowRoot!.querySelector('.card')!;
|
||||
expect(card.classList.value.trim()).to.eq('card card--has-header');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "footer" to render a footer', async () => {
|
||||
describe('when provided an element in the slot "footer" to render a footer', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html`<sl-card>
|
||||
@@ -78,35 +76,35 @@ describe('<sl-card>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
it('should render the child content provided.', () => {
|
||||
expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!');
|
||||
});
|
||||
|
||||
it('render the footer content provided.', async () => {
|
||||
const footer = <HTMLDivElement>el.querySelector('div[slot=footer]');
|
||||
it('render the footer content provided.', () => {
|
||||
const footer = el.querySelector<HTMLElement>('div[slot=footer]')!;
|
||||
expect(footer.innerText).eq('Footer Content');
|
||||
});
|
||||
|
||||
it('accept "footer" as an assigned child in the shadow root.', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=footer]');
|
||||
it('accept "footer" as an assigned child in the shadow root.', () => {
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=footer]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should contain the class card--has-footer.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
it('should contain the class card--has-footer.', () => {
|
||||
const card = el.shadowRoot!.querySelector('.card')!;
|
||||
expect(card.classList.value.trim()).to.eq('card card--has-footer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "image" to render a image', async () => {
|
||||
describe('when provided an element in the slot "image" to render a image', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html`<sl-card>
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
alt="A kitten walks towards camera on top of pallet."
|
||||
/>
|
||||
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
|
||||
@@ -118,21 +116,21 @@ describe('<sl-card>', () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
it('should render the child content provided.', () => {
|
||||
expect(el.innerText).to.contain(
|
||||
'This is a kitten, but not just any kitten. This kitten likes walking along pallets.'
|
||||
);
|
||||
});
|
||||
|
||||
it('accept "image" as an assigned child in the shadow root.', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=image]');
|
||||
it('accept "image" as an assigned child in the shadow root.', () => {
|
||||
const slot = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name=image]')!;
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should contain the class card--has-image.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
it('should contain the class card--has-image.', () => {
|
||||
const card = el.shadowRoot!.querySelector('.card')!;
|
||||
expect(card.classList.value.trim()).to.eq('card card--has-image');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './card.styles';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -28,7 +28,7 @@ import styles from './card.styles';
|
||||
export default class SlCard extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');
|
||||
|
||||
render() {
|
||||
return html`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlCheckbox from './checkbox';
|
||||
|
||||
describe('<sl-checkbox>', () => {
|
||||
@@ -9,7 +7,7 @@ describe('<sl-checkbox>', () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `);
|
||||
const checkbox = el.shadowRoot?.querySelector('input');
|
||||
|
||||
expect(checkbox.disabled).to.be.true;
|
||||
expect(checkbox!.disabled).to.be.true;
|
||||
});
|
||||
|
||||
it('should be valid by default', async () => {
|
||||
@@ -20,7 +18,9 @@ describe('<sl-checkbox>', () => {
|
||||
|
||||
it('should fire sl-change when clicked', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
setTimeout(() => el.shadowRoot?.querySelector('input').click());
|
||||
setTimeout(() => {
|
||||
el.shadowRoot!.querySelector('input')!.click();
|
||||
});
|
||||
const event = await oneEvent(el, 'sl-change');
|
||||
expect(event.target).to.equal(el);
|
||||
expect(el.checked).to.be.true;
|
||||
@@ -28,9 +28,11 @@ describe('<sl-checkbox>', () => {
|
||||
|
||||
it('should fire sl-change when toggled via keyboard', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
const input = el.shadowRoot?.querySelector('input');
|
||||
const input = el.shadowRoot!.querySelector('input')!;
|
||||
input.focus();
|
||||
setTimeout(() => sendKeys({ press: ' ' }));
|
||||
setTimeout(() => {
|
||||
void sendKeys({ press: ' ' });
|
||||
});
|
||||
const event = await oneEvent(el, 'sl-change');
|
||||
expect(event.target).to.equal(el);
|
||||
expect(el.checked).to.be.true;
|
||||
|
||||
@@ -3,12 +3,11 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import styles from './checkbox.styles';
|
||||
|
||||
let id = 0;
|
||||
import { autoIncrement } from '~/internal/autoIncrement';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController } from '~/internal/form-control';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -32,12 +31,13 @@ export default class SlCheckbox extends LitElement {
|
||||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this, {
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlCheckbox) => (control.checked ? control.value : undefined)
|
||||
});
|
||||
private inputId = `checkbox-${++id}`;
|
||||
private labelId = `checkbox-label-${id}`;
|
||||
private readonly attrId = autoIncrement();
|
||||
private readonly inputId = `checkbox-${this.attrId}`;
|
||||
private readonly labelId = `checkbox-label-${this.attrId}`;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
|
||||
@@ -103,13 +103,11 @@ export default class SlCheckbox extends LitElement {
|
||||
emit(this, 'sl-blur');
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.input) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
@@ -146,7 +144,6 @@ export default class SlCheckbox extends LitElement {
|
||||
.checked=${live(this.checked)}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
role="checkbox"
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
aria-labelledby=${this.labelId}
|
||||
@click=${this.handleClick}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlColorPicker from './color-picker';
|
||||
|
||||
describe('<sl-color-picker>', () => {
|
||||
it('should emit change and show correct color when the value changes', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot.querySelector('[part="trigger"]') as HTMLElement;
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part="trigger"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const color = 'rgb(255, 204, 0)';
|
||||
|
||||
@@ -22,21 +20,21 @@ describe('<sl-color-picker>', () => {
|
||||
|
||||
it('should render in a dropdown', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const dropdown = el.shadowRoot.querySelector('sl-dropdown');
|
||||
const dropdown = el.shadowRoot!.querySelector('sl-dropdown');
|
||||
|
||||
expect(dropdown).to.exist;
|
||||
});
|
||||
|
||||
it('should not render in a dropdown when inline is enabled', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker inline></sl-color-picker> `);
|
||||
const dropdown = el.shadowRoot.querySelector('sl-dropdown');
|
||||
const dropdown = el.shadowRoot!.querySelector('sl-dropdown');
|
||||
|
||||
expect(dropdown).to.not.exist;
|
||||
});
|
||||
|
||||
it('should show opacity slider when opacity is enabled', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const opacitySlider = el.shadowRoot.querySelector('[part*="opacity-slider"]') as HTMLElement;
|
||||
const opacitySlider = el.shadowRoot!.querySelector('[part*="opacity-slider"]')!;
|
||||
|
||||
expect(opacitySlider).to.exist;
|
||||
});
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import Color from 'color';
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlInput from '../input/input';
|
||||
import color from 'color';
|
||||
import styles from './color-picker.styles';
|
||||
|
||||
import '../button/button';
|
||||
import '../button-group/button-group';
|
||||
import '../dropdown/dropdown';
|
||||
import '../icon/icon';
|
||||
import '../input/input';
|
||||
import '~/components/button-group/button-group';
|
||||
import '~/components/button/button';
|
||||
import type SlDropdown from '~/components/dropdown/dropdown';
|
||||
import '~/components/dropdown/dropdown';
|
||||
import '~/components/icon/icon';
|
||||
import type SlInput from '~/components/input/input';
|
||||
import '~/components/input/input';
|
||||
import { drag } from '~/internal/drag';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController } from '~/internal/form-control';
|
||||
import { clamp } from '~/internal/math';
|
||||
import { watch } from '~/internal/watch';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
const hasEyeDropper = 'EyeDropper' in window;
|
||||
|
||||
interface EyeDropperConstructor {
|
||||
new (): EyeDropperInterface;
|
||||
}
|
||||
|
||||
interface EyeDropperInterface {
|
||||
open: () => Promise<{ sRGBHex: string }>;
|
||||
}
|
||||
|
||||
declare const EyeDropper: EyeDropperConstructor;
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
@@ -63,11 +73,11 @@ export default class SlColorPicker extends LitElement {
|
||||
@query('[part="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('.color-dropdown') dropdown: SlDropdown;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private isSafeValue = false;
|
||||
private lastValueEmitted: string;
|
||||
private localize = new LocalizeController(this);
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@state() private inputValue = '';
|
||||
@state() private hue = 0;
|
||||
@@ -151,7 +161,7 @@ export default class SlColorPicker extends LitElement {
|
||||
|
||||
this.inputValue = this.value;
|
||||
this.lastValueEmitted = this.value;
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
/** Returns the current value as a string in the specified format. */
|
||||
@@ -160,7 +170,7 @@ export default class SlColorPicker extends LitElement {
|
||||
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
);
|
||||
|
||||
if (!currentColor) {
|
||||
if (currentColor === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -195,11 +205,10 @@ export default class SlColorPicker extends LitElement {
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this.dropdown.show();
|
||||
void this.dropdown.show();
|
||||
});
|
||||
} else {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
@@ -215,9 +224,9 @@ export default class SlColorPicker extends LitElement {
|
||||
|
||||
// Show copied animation
|
||||
this.previewButton.classList.add('color-picker__preview-color--copied');
|
||||
this.previewButton.addEventListener('animationend', () =>
|
||||
this.previewButton.classList.remove('color-picker__preview-color--copied')
|
||||
);
|
||||
this.previewButton.addEventListener('animationend', () => {
|
||||
this.previewButton.classList.remove('color-picker__preview-color--copied');
|
||||
});
|
||||
}
|
||||
|
||||
handleFormatToggle() {
|
||||
@@ -226,106 +235,74 @@ export default class SlColorPicker extends LitElement {
|
||||
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
|
||||
}
|
||||
|
||||
handleAlphaDrag(event: any) {
|
||||
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__alpha') as HTMLElement;
|
||||
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement;
|
||||
handleAlphaDrag(event: Event) {
|
||||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
||||
this.handleDrag(event, container, x => {
|
||||
drag(container, x => {
|
||||
this.alpha = clamp((x / width) * 100, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
});
|
||||
}
|
||||
|
||||
handleHueDrag(event: any) {
|
||||
const container = this.shadowRoot!.querySelector('.color-picker__slider.color-picker__hue') as HTMLElement;
|
||||
const handle = container.querySelector('.color-picker__slider-handle') as HTMLElement;
|
||||
handleHueDrag(event: Event) {
|
||||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
||||
this.handleDrag(event, container, x => {
|
||||
drag(container, x => {
|
||||
this.hue = clamp((x / width) * 360, 0, 360);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
});
|
||||
}
|
||||
|
||||
handleGridDrag(event: any) {
|
||||
const grid = this.shadowRoot!.querySelector('.color-picker__grid') as HTMLElement;
|
||||
const handle = grid.querySelector('.color-picker__grid-handle') as HTMLElement;
|
||||
handleGridDrag(event: Event) {
|
||||
const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
|
||||
const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
|
||||
const { width, height } = grid.getBoundingClientRect();
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
|
||||
this.handleDrag(event, grid, (x, y) => {
|
||||
drag(grid, (x, y) => {
|
||||
this.saturation = clamp((x / width) * 100, 0, 100);
|
||||
this.lightness = clamp(100 - (y / height) * 100, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
});
|
||||
}
|
||||
|
||||
handleDrag(event: any, container: HTMLElement, onMove: (x: number, y: number) => void) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const move = (event: any) => {
|
||||
const dims = container.getBoundingClientRect();
|
||||
const defaultView = container.ownerDocument.defaultView!;
|
||||
const offsetX = dims.left + defaultView.pageXOffset;
|
||||
const offsetY = dims.top + defaultView.pageYOffset;
|
||||
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
|
||||
const y = (event.changedTouches ? event.changedTouches[0].pageY : event.pageY) - offsetY;
|
||||
|
||||
onMove(x, y);
|
||||
};
|
||||
|
||||
// Move on init
|
||||
move(event);
|
||||
|
||||
const stop = () => {
|
||||
document.removeEventListener('mousemove', move);
|
||||
document.removeEventListener('touchmove', move);
|
||||
document.removeEventListener('mouseup', stop);
|
||||
document.removeEventListener('touchend', stop);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', move);
|
||||
document.addEventListener('touchmove', move);
|
||||
document.addEventListener('mouseup', stop);
|
||||
document.addEventListener('touchend', stop);
|
||||
}
|
||||
|
||||
handleAlphaKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
this.alpha = clamp(this.alpha - increment, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.alpha = clamp(this.alpha + increment, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'Home') {
|
||||
event.preventDefault();
|
||||
this.alpha = 0;
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'End') {
|
||||
event.preventDefault();
|
||||
this.alpha = 100;
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,25 +312,25 @@ export default class SlColorPicker extends LitElement {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
this.hue = clamp(this.hue - increment, 0, 360);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.hue = clamp(this.hue + increment, 0, 360);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'Home') {
|
||||
event.preventDefault();
|
||||
this.hue = 0;
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'End') {
|
||||
event.preventDefault();
|
||||
this.hue = 360;
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,25 +340,25 @@ export default class SlColorPicker extends LitElement {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
this.saturation = clamp(this.saturation - increment, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.saturation = clamp(this.saturation + increment, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.lightness = clamp(this.lightness + increment, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.lightness = clamp(this.lightness - increment, 0, 100);
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +374,9 @@ export default class SlColorPicker extends LitElement {
|
||||
if (event.key === 'Enter') {
|
||||
this.setColor(this.input.value);
|
||||
this.input.value = this.value;
|
||||
setTimeout(() => this.input.select());
|
||||
setTimeout(() => {
|
||||
this.input.select();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +398,7 @@ export default class SlColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
if (rgba[3].indexOf('%') > -1) {
|
||||
rgba[3] = (Number(rgba[3].replace(/%/g, '')) / 100).toString();
|
||||
rgba[3] = (parseFloat(rgba[3].replace(/%/g, '')) / 100).toString();
|
||||
}
|
||||
|
||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
|
||||
@@ -437,7 +416,7 @@ export default class SlColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
if (hsla[3].indexOf('%') > -1) {
|
||||
hsla[3] = (Number(hsla[3].replace(/%/g, '')) / 100).toString();
|
||||
hsla[3] = (parseFloat(hsla[3].replace(/%/g, '')) / 100).toString();
|
||||
}
|
||||
|
||||
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
|
||||
@@ -451,41 +430,40 @@ export default class SlColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
parseColor(colorString: string) {
|
||||
function toHex(value: number) {
|
||||
const hex = Math.round(value).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
let parsed: Color;
|
||||
|
||||
// The color module has a weak parser, so we normalize certain things to make the user experience better
|
||||
colorString = this.normalizeColorString(colorString);
|
||||
|
||||
try {
|
||||
parsed = color(colorString);
|
||||
parsed = Color(colorString);
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const hslColor = parsed.hsl();
|
||||
|
||||
const hsl = {
|
||||
h: parsed.hsl().color[0],
|
||||
s: parsed.hsl().color[1],
|
||||
l: parsed.hsl().color[2],
|
||||
a: parsed.hsl().valpha
|
||||
h: hslColor.hue(),
|
||||
s: hslColor.saturationl(),
|
||||
l: hslColor.lightness(),
|
||||
a: hslColor.alpha()
|
||||
};
|
||||
|
||||
const rgbColor = parsed.rgb();
|
||||
|
||||
const rgb = {
|
||||
r: parsed.rgb().color[0],
|
||||
g: parsed.rgb().color[1],
|
||||
b: parsed.rgb().color[2],
|
||||
a: parsed.rgb().valpha
|
||||
r: rgbColor.red(),
|
||||
g: rgbColor.green(),
|
||||
b: rgbColor.blue(),
|
||||
a: rgbColor.alpha()
|
||||
};
|
||||
|
||||
const hex = {
|
||||
r: toHex(parsed.rgb().color[0]),
|
||||
g: toHex(parsed.rgb().color[1]),
|
||||
b: toHex(parsed.rgb().color[2]),
|
||||
a: toHex(parsed.rgb().valpha * 255)
|
||||
r: toHex(rgb.r),
|
||||
g: toHex(rgb.g),
|
||||
b: toHex(rgb.b),
|
||||
a: toHex(rgb.a * 255)
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -501,9 +479,7 @@ export default class SlColorPicker extends LitElement {
|
||||
l: hsl.l,
|
||||
a: hsl.a,
|
||||
string: this.setLetterCase(
|
||||
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${Number(
|
||||
hsl.a.toFixed(2).toString()
|
||||
)})`
|
||||
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${hsl.a.toFixed(2).toString()})`
|
||||
)
|
||||
},
|
||||
rgb: {
|
||||
@@ -518,9 +494,7 @@ export default class SlColorPicker extends LitElement {
|
||||
b: rgb.b,
|
||||
a: rgb.a,
|
||||
string: this.setLetterCase(
|
||||
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${Number(
|
||||
rgb.a.toFixed(2).toString()
|
||||
)})`
|
||||
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${rgb.a.toFixed(2).toString()})`
|
||||
)
|
||||
},
|
||||
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
|
||||
@@ -531,7 +505,7 @@ export default class SlColorPicker extends LitElement {
|
||||
setColor(colorString: string) {
|
||||
const newColor = this.parseColor(colorString);
|
||||
|
||||
if (!newColor) {
|
||||
if (newColor === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -540,13 +514,15 @@ export default class SlColorPicker extends LitElement {
|
||||
this.lightness = newColor.hsla.l;
|
||||
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
|
||||
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setLetterCase(string: string) {
|
||||
if (typeof string !== 'string') return '';
|
||||
if (typeof string !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return this.uppercase ? string.toUpperCase() : string.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -555,7 +531,7 @@ export default class SlColorPicker extends LitElement {
|
||||
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
);
|
||||
|
||||
if (!currentColor) {
|
||||
if (currentColor === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -586,12 +562,11 @@ export default class SlColorPicker extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const eyeDropper = new EyeDropper();
|
||||
|
||||
eyeDropper
|
||||
.open()
|
||||
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex))
|
||||
.then(colorSelectionResult => this.setColor(colorSelectionResult.sRGBHex))
|
||||
.catch(() => {
|
||||
// The user canceled, do nothing
|
||||
});
|
||||
@@ -599,7 +574,7 @@ export default class SlColorPicker extends LitElement {
|
||||
|
||||
@watch('format')
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
void this.syncValues();
|
||||
}
|
||||
|
||||
@watch('opacity')
|
||||
@@ -608,11 +583,11 @@ export default class SlColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
handleValueChange(oldValue: string, newValue: string) {
|
||||
if (!this.isSafeValue) {
|
||||
handleValueChange(oldValue: string | undefined, newValue: string) {
|
||||
if (!this.isSafeValue && oldValue !== undefined) {
|
||||
const newColor = this.parseColor(newValue);
|
||||
|
||||
if (newColor) {
|
||||
if (newColor !== null) {
|
||||
this.inputValue = this.value;
|
||||
this.hue = newColor.hsla.h;
|
||||
this.saturation = newColor.hsla.s;
|
||||
@@ -658,11 +633,8 @@ export default class SlColorPicker extends LitElement {
|
||||
left: `${x}%`,
|
||||
backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)`
|
||||
})}
|
||||
role="slider"
|
||||
role="application"
|
||||
aria-label="HSL"
|
||||
aria-valuetext=${`hsl(${Math.round(this.hue)}, ${Math.round(this.saturation)}%, ${Math.round(
|
||||
this.lightness
|
||||
)}%)`}
|
||||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||
@keydown=${this.handleGridKeyDown}
|
||||
></span>
|
||||
@@ -743,7 +715,7 @@ export default class SlColorPicker extends LitElement {
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="color-picker__user-input">
|
||||
<div class="color-picker__user-input" aria-live="polite">
|
||||
<sl-input
|
||||
part="input"
|
||||
type="text"
|
||||
@@ -762,7 +734,7 @@ export default class SlColorPicker extends LitElement {
|
||||
${!this.noFormatToggle
|
||||
? html`
|
||||
<sl-button
|
||||
aria-label=${this.localize.term('toggle_color_format')}
|
||||
aria-label=${this.localize.term('toggleColorFormat')}
|
||||
exportparts="base:format-button"
|
||||
@click=${this.handleFormatToggle}
|
||||
>
|
||||
@@ -776,7 +748,7 @@ export default class SlColorPicker extends LitElement {
|
||||
<sl-icon
|
||||
library="system"
|
||||
name="eyedropper"
|
||||
label=${this.localize.term('select_a_color_from_the_screen')}
|
||||
label=${this.localize.term('selectAColorFromTheScreen')}
|
||||
></sl-icon>
|
||||
</sl-button>
|
||||
`
|
||||
@@ -784,7 +756,7 @@ export default class SlColorPicker extends LitElement {
|
||||
</sl-button-group>
|
||||
</div>
|
||||
|
||||
${this.swatches
|
||||
${this.swatches.length > 0
|
||||
? html`
|
||||
<div part="swatches" class="color-picker__swatches">
|
||||
${this.swatches.map(swatch => {
|
||||
@@ -828,12 +800,14 @@ export default class SlColorPicker extends LitElement {
|
||||
part="trigger"
|
||||
slot="trigger"
|
||||
class=${classMap({
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
'color-dropdown__trigger': true,
|
||||
'color-dropdown__trigger--disabled': this.disabled,
|
||||
'color-dropdown__trigger--small': this.size === 'small',
|
||||
'color-dropdown__trigger--medium': this.size === 'medium',
|
||||
'color-dropdown__trigger--large': this.size === 'large',
|
||||
'color-picker__transparent-bg': true
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
})}
|
||||
style=${styleMap({
|
||||
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
@@ -846,6 +820,11 @@ export default class SlColorPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
function toHex(value: number) {
|
||||
const hex = Math.round(value).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-color-picker': SlColorPicker;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// cspell:dictionaries lorem-ipsum
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDetails from './details';
|
||||
|
||||
describe('<sl-details>', () => {
|
||||
@@ -14,7 +12,7 @@ describe('<sl-details>', () => {
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||
|
||||
expect(body.hidden).to.be.false;
|
||||
});
|
||||
@@ -27,7 +25,7 @@ describe('<sl-details>', () => {
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||
|
||||
expect(body.hidden).to.be.true;
|
||||
});
|
||||
@@ -40,13 +38,13 @@ describe('<sl-details>', () => {
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
@@ -64,13 +62,13 @@ describe('<sl-details>', () => {
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
void el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
@@ -88,7 +86,7 @@ describe('<sl-details>', () => {
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -112,7 +110,7 @@ describe('<sl-details>', () => {
|
||||
consequat.
|
||||
</sl-details>
|
||||
`);
|
||||
const body = el.shadowRoot?.querySelector('.details__body') as HTMLElement;
|
||||
const body = el.shadowRoot!.querySelector<HTMLElement>('.details__body')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import styles from './details.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '~/internal/animate';
|
||||
import { emit, waitForEvent } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '~/utilities/animation-registry';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -58,7 +56,7 @@ export default class SlDetails extends LitElement {
|
||||
/** Shows the details. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
@@ -68,16 +66,24 @@ export default class SlDetails extends LitElement {
|
||||
/** Hides the details */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
toggleOpen() {
|
||||
if (this.open) {
|
||||
void this.hide();
|
||||
} else {
|
||||
void this.show();
|
||||
}
|
||||
}
|
||||
|
||||
handleSummaryClick() {
|
||||
if (!this.disabled) {
|
||||
this.open ? this.hide() : this.show();
|
||||
this.toggleOpen();
|
||||
this.header.focus();
|
||||
}
|
||||
}
|
||||
@@ -85,17 +91,17 @@ export default class SlDetails extends LitElement {
|
||||
handleSummaryKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
this.open ? this.hide() : this.show();
|
||||
this.toggleOpen();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
void this.hide();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.show();
|
||||
void this.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// cspell:dictionaries lorem-ipsum
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDialog from './dialog';
|
||||
|
||||
describe('<sl-dialog>', () => {
|
||||
@@ -10,7 +8,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
@@ -19,7 +17,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(
|
||||
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
|
||||
);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
@@ -28,13 +26,13 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
@@ -48,13 +46,13 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
void el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
@@ -68,7 +66,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -88,7 +86,7 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -108,9 +106,11 @@ describe('<sl-dialog>', () => {
|
||||
const el = await fixture<SlDialog>(html`
|
||||
<sl-dialog open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
|
||||
`);
|
||||
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement;
|
||||
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
|
||||
|
||||
el.addEventListener('sl-request-close', event => event.preventDefault());
|
||||
el.addEventListener('sl-request-close', event => {
|
||||
event.preventDefault();
|
||||
});
|
||||
overlay.click();
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
@@ -118,14 +118,14 @@ describe('<sl-dialog>', () => {
|
||||
|
||||
it('should allow initial focus to be set', async () => {
|
||||
const el = await fixture<SlDialog>(html` <sl-dialog><input /></sl-dialog> `);
|
||||
const input = el.querySelector('input');
|
||||
const initialFocusHandler = sinon.spy(event => {
|
||||
const input = el.querySelector('input')!;
|
||||
const initialFocusHandler = sinon.spy((event: Event) => {
|
||||
event.preventDefault();
|
||||
input.focus();
|
||||
});
|
||||
|
||||
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => initialFocusHandler.calledOnce);
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@ import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
import Modal from '../../internal/modal';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import styles from './dialog.styles';
|
||||
|
||||
import '../icon-button/icon-button';
|
||||
import '~/components/icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||
import { emit, waitForEvent } from '~/internal/event';
|
||||
import Modal from '~/internal/modal';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '~/internal/scroll';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
import { isPreventScrollSupported } from '~/internal/support';
|
||||
import { watch } from '~/internal/watch';
|
||||
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
|
||||
|
||||
const hasPreventScroll = isPreventScrollSupported();
|
||||
|
||||
@@ -65,7 +63,7 @@ export default class SlDialog extends LitElement {
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
@query('.dialog__overlay') overlay: HTMLElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
|
||||
@@ -106,7 +104,7 @@ export default class SlDialog extends LitElement {
|
||||
/** Shows the dialog. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
@@ -116,7 +114,7 @@ export default class SlDialog extends LitElement {
|
||||
/** Hides the dialog */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
@@ -127,11 +125,11 @@ export default class SlDialog extends LitElement {
|
||||
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
|
||||
if (slRequestClose.defaultPrevented) {
|
||||
const animation = getAnimation(this, 'dialog.denyClose');
|
||||
animateTo(this.panel, animation.keyframes, animation.options);
|
||||
void animateTo(this.panel, animation.keyframes, animation.options);
|
||||
return;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
void this.hide();
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -197,8 +195,10 @@ export default class SlDialog extends LitElement {
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
if (typeof trigger?.focus === 'function') {
|
||||
setTimeout(() => {
|
||||
trigger.focus();
|
||||
});
|
||||
}
|
||||
|
||||
emit(this, 'sl-after-hide');
|
||||
@@ -216,7 +216,13 @@ export default class SlDialog extends LitElement {
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
>
|
||||
<div part="overlay" class="dialog__overlay" @click=${this.requestClose} tabindex="-1"></div>
|
||||
<div
|
||||
part="overlay"
|
||||
class="dialog__overlay"
|
||||
@click=${this.requestClose}
|
||||
@keydown=${this.handleKeyDown}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
|
||||
<div
|
||||
part="panel"
|
||||
@@ -232,7 +238,7 @@ export default class SlDialog extends LitElement {
|
||||
? html`
|
||||
<header part="header" class="dialog__header">
|
||||
<span part="title" class="dialog__title" id="title">
|
||||
<slot name="label"> ${this.label || String.fromCharCode(65279)} </slot>
|
||||
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||
</span>
|
||||
<sl-icon-button
|
||||
exportparts="base:close-button"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './divider.styles';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// cspell:dictionaries lorem-ipsum
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDrawer from './drawer';
|
||||
|
||||
describe('<sl-drawer>', () => {
|
||||
@@ -10,7 +8,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.false;
|
||||
});
|
||||
@@ -19,7 +17,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(
|
||||
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
|
||||
);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
|
||||
expect(base.hidden).to.be.true;
|
||||
});
|
||||
@@ -28,13 +26,13 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
@@ -48,13 +46,13 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
void el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
@@ -68,7 +66,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -88,7 +86,7 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const base = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
const base = el.shadowRoot!.querySelector<HTMLElement>('[part="base"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
@@ -108,9 +106,11 @@ describe('<sl-drawer>', () => {
|
||||
const el = await fixture<SlDrawer>(html`
|
||||
<sl-drawer open>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
|
||||
`);
|
||||
const overlay = el.shadowRoot?.querySelector('[part="overlay"]') as HTMLElement;
|
||||
const overlay = el.shadowRoot!.querySelector<HTMLElement>('[part="overlay"]')!;
|
||||
|
||||
el.addEventListener('sl-request-close', event => event.preventDefault());
|
||||
el.addEventListener('sl-request-close', event => {
|
||||
event.preventDefault();
|
||||
});
|
||||
overlay.click();
|
||||
|
||||
expect(el.open).to.be.true;
|
||||
@@ -118,14 +118,14 @@ describe('<sl-drawer>', () => {
|
||||
|
||||
it('should allow initial focus to be set', async () => {
|
||||
const el = await fixture<SlDrawer>(html` <sl-drawer><input /></sl-drawer> `);
|
||||
const input = el.querySelector('input');
|
||||
const initialFocusHandler = sinon.spy(event => {
|
||||
const input = el.querySelector<HTMLInputElement>('input')!;
|
||||
const initialFocusHandler = sinon.spy((event: InputEvent) => {
|
||||
event.preventDefault();
|
||||
input.focus();
|
||||
});
|
||||
|
||||
el.addEventListener('sl-initial-focus', initialFocusHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => initialFocusHandler.calledOnce);
|
||||
|
||||
|
||||
@@ -2,19 +2,17 @@ import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { uppercaseFirstLetter } from '../../internal/string';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
import Modal from '../../internal/modal';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import styles from './drawer.styles';
|
||||
|
||||
import '../icon-button/icon-button';
|
||||
import '~/components/icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||
import { emit, waitForEvent } from '~/internal/event';
|
||||
import Modal from '~/internal/modal';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '~/internal/scroll';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
import { uppercaseFirstLetter } from '~/internal/string';
|
||||
import { isPreventScrollSupported } from '~/internal/support';
|
||||
import { watch } from '~/internal/watch';
|
||||
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
|
||||
|
||||
const hasPreventScroll = isPreventScrollSupported();
|
||||
|
||||
@@ -73,7 +71,7 @@ export default class SlDrawer extends LitElement {
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
@query('.drawer__overlay') overlay: HTMLElement;
|
||||
|
||||
private hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
|
||||
@@ -123,7 +121,7 @@ export default class SlDrawer extends LitElement {
|
||||
/** Shows the drawer. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
@@ -133,7 +131,7 @@ export default class SlDrawer extends LitElement {
|
||||
/** Hides the drawer */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
@@ -144,11 +142,11 @@ export default class SlDrawer extends LitElement {
|
||||
const slRequestClose = emit(this, 'sl-request-close', { cancelable: true });
|
||||
if (slRequestClose.defaultPrevented) {
|
||||
const animation = getAnimation(this, 'drawer.denyClose');
|
||||
animateTo(this.panel, animation.keyframes, animation.options);
|
||||
void animateTo(this.panel, animation.keyframes, animation.options);
|
||||
return;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
void this.hide();
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -217,8 +215,10 @@ export default class SlDrawer extends LitElement {
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
if (typeof trigger?.focus === 'function') {
|
||||
setTimeout(() => {
|
||||
trigger.focus();
|
||||
});
|
||||
}
|
||||
|
||||
emit(this, 'sl-after-hide');
|
||||
@@ -242,7 +242,13 @@ export default class SlDrawer extends LitElement {
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
>
|
||||
<div part="overlay" class="drawer__overlay" @click=${this.requestClose} tabindex="-1"></div>
|
||||
<div
|
||||
part="overlay"
|
||||
class="drawer__overlay"
|
||||
@click=${this.requestClose}
|
||||
@keydown=${this.handleKeyDown}
|
||||
tabindex="-1"
|
||||
></div>
|
||||
|
||||
<div
|
||||
part="panel"
|
||||
@@ -259,7 +265,7 @@ export default class SlDrawer extends LitElement {
|
||||
<header part="header" class="drawer__header">
|
||||
<span part="title" class="drawer__title" id="title">
|
||||
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
|
||||
<slot name="label"> ${this.label || String.fromCharCode(65279)} </slot>
|
||||
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||
</span>
|
||||
<sl-icon-button
|
||||
exportparts="base:close-button"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlDropdown from './dropdown';
|
||||
|
||||
describe('<sl-dropdown>', () => {
|
||||
@@ -16,7 +14,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
|
||||
expect(panel.hidden).to.be.false;
|
||||
});
|
||||
@@ -32,7 +30,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
|
||||
expect(panel.hidden).to.be.true;
|
||||
});
|
||||
@@ -48,13 +46,13 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-show', showHandler);
|
||||
el.addEventListener('sl-after-show', afterShowHandler);
|
||||
el.show();
|
||||
void el.show();
|
||||
|
||||
await waitUntil(() => showHandler.calledOnce);
|
||||
await waitUntil(() => afterShowHandler.calledOnce);
|
||||
@@ -75,13 +73,13 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-hide', hideHandler);
|
||||
el.addEventListener('sl-after-hide', afterHideHandler);
|
||||
el.hide();
|
||||
void el.hide();
|
||||
|
||||
await waitUntil(() => hideHandler.calledOnce);
|
||||
await waitUntil(() => afterHideHandler.calledOnce);
|
||||
@@ -102,7 +100,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const showHandler = sinon.spy();
|
||||
const afterShowHandler = sinon.spy();
|
||||
|
||||
@@ -129,7 +127,7 @@ describe('<sl-dropdown>', () => {
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
`);
|
||||
const panel = el.shadowRoot?.querySelector('[part="panel"]') as HTMLElement;
|
||||
const panel = el.shadowRoot!.querySelector<HTMLElement>('[part="panel"]')!;
|
||||
const hideHandler = sinon.spy();
|
||||
const afterHideHandler = sinon.spy();
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Instance as PopperInstance } from '@popperjs/core/dist/esm';
|
||||
import { createPopper } from '@popperjs/core/dist/esm';
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { scrollIntoView } from '../../internal/scroll';
|
||||
import { getTabbableBoundary } from '../../internal/tabbable';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from './dropdown.styles';
|
||||
import type SlMenuItem from '~/components/menu-item/menu-item';
|
||||
import type SlMenu from '~/components/menu/menu';
|
||||
import { animateTo, stopAnimations } from '~/internal/animate';
|
||||
import { emit, waitForEvent } from '~/internal/event';
|
||||
import { scrollIntoView } from '~/internal/scroll';
|
||||
import { getTabbableBoundary } from '~/internal/tabbable';
|
||||
import { watch } from '~/internal/watch';
|
||||
import { setDefaultAnimation, getAnimation } from '~/utilities/animation-registry';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -40,7 +40,7 @@ export default class SlDropdown extends LitElement {
|
||||
@query('.dropdown__panel') panel: HTMLElement;
|
||||
@query('.dropdown__positioner') positioner: HTMLElement;
|
||||
|
||||
private popover: PopperInstance;
|
||||
private popover?: PopperInstance;
|
||||
|
||||
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
@@ -73,7 +73,7 @@ export default class SlDropdown extends LitElement {
|
||||
@property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false;
|
||||
|
||||
/** The dropdown will close when the user interacts outside of this element (e.g. clicking). */
|
||||
@property({ attribute: false }) containingElement: HTMLElement;
|
||||
@property({ attribute: false }) containingElement?: HTMLElement;
|
||||
|
||||
/** The distance in pixels from which to offset the panel away from its trigger. */
|
||||
@property({ type: Number }) distance = 0;
|
||||
@@ -94,12 +94,12 @@ export default class SlDropdown extends LitElement {
|
||||
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
||||
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
||||
|
||||
if (!this.containingElement) {
|
||||
if (typeof this.containingElement === 'undefined') {
|
||||
this.containingElement = this;
|
||||
}
|
||||
|
||||
// Create the popover after render
|
||||
this.updateComplete.then(() => {
|
||||
void this.updateComplete.then(() => {
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
@@ -127,30 +127,30 @@ export default class SlDropdown extends LitElement {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.hide();
|
||||
void void this.hide();
|
||||
|
||||
if (this.popover) {
|
||||
this.popover.destroy();
|
||||
}
|
||||
this.popover?.destroy();
|
||||
}
|
||||
|
||||
focusOnTrigger() {
|
||||
const slot = this.trigger.querySelector('slot')!;
|
||||
const trigger = slot.assignedElements({ flatten: true })[0] as any;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
const trigger = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
|
||||
if (typeof trigger?.focus === 'function') {
|
||||
trigger.focus();
|
||||
}
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
const slot = this.panel.querySelector('slot')!;
|
||||
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
|
||||
return slot.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'sl-menu') as
|
||||
| SlMenu
|
||||
| undefined;
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
// Close when escape is pressed
|
||||
if (event.key === 'Escape') {
|
||||
this.hide();
|
||||
void this.hide();
|
||||
this.focusOnTrigger();
|
||||
return;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ export default class SlDropdown extends LitElement {
|
||||
// Tabbing within an open menu should close the dropdown and refocus the trigger
|
||||
if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') {
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
void this.hide();
|
||||
this.focusOnTrigger();
|
||||
return;
|
||||
}
|
||||
@@ -171,13 +171,15 @@ export default class SlDropdown extends LitElement {
|
||||
// otherwise `document.activeElement` will only return the name of the parent shadow DOM element.
|
||||
setTimeout(() => {
|
||||
const activeElement =
|
||||
this.containingElement.getRootNode() instanceof ShadowRoot
|
||||
this.containingElement?.getRootNode() instanceof ShadowRoot
|
||||
? document.activeElement?.shadowRoot?.activeElement
|
||||
: document.activeElement;
|
||||
|
||||
if (activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement) {
|
||||
this.hide();
|
||||
return;
|
||||
if (
|
||||
typeof this.containingElement === 'undefined' ||
|
||||
activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
|
||||
) {
|
||||
void this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -185,10 +187,9 @@ export default class SlDropdown extends LitElement {
|
||||
|
||||
handleDocumentMouseDown(event: MouseEvent) {
|
||||
// Close when clicking outside of the containing element
|
||||
const path = event.composedPath() as Array<EventTarget>;
|
||||
if (!path.includes(this.containingElement)) {
|
||||
this.hide();
|
||||
return;
|
||||
const path = event.composedPath();
|
||||
if (typeof this.containingElement !== 'undefined' && !path.includes(this.containingElement)) {
|
||||
void this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +203,7 @@ export default class SlDropdown extends LitElement {
|
||||
|
||||
// Hide the dropdown when a menu item is selected
|
||||
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') {
|
||||
this.hide();
|
||||
void this.hide();
|
||||
this.focusOnTrigger();
|
||||
}
|
||||
}
|
||||
@@ -212,42 +213,44 @@ export default class SlDropdown extends LitElement {
|
||||
@watch('placement')
|
||||
@watch('skidding')
|
||||
handlePopoverOptionsChange() {
|
||||
if (this.popover) {
|
||||
this.popover.setOptions({
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
void this.popover?.setOptions({
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
handleTriggerClick() {
|
||||
this.open ? this.hide() : this.show();
|
||||
if (this.open) {
|
||||
void this.hide();
|
||||
} else {
|
||||
void this.show();
|
||||
}
|
||||
}
|
||||
|
||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
||||
const menuItems = typeof menu !== 'undefined' ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
// Close when escape or tab is pressed
|
||||
if (event.key === 'Escape') {
|
||||
this.focusOnTrigger();
|
||||
this.hide();
|
||||
void this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -255,7 +258,7 @@ export default class SlDropdown extends LitElement {
|
||||
// key again to hide the menu in case they don't want to make a selection.
|
||||
if ([' ', 'Enter'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
this.open ? this.hide() : this.show();
|
||||
this.handleTriggerClick();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,19 +270,18 @@ export default class SlDropdown extends LitElement {
|
||||
|
||||
// Show the menu if it's not already open
|
||||
if (!this.open) {
|
||||
this.show();
|
||||
void this.show();
|
||||
}
|
||||
|
||||
// Focus on a menu item
|
||||
if (event.key === 'ArrowDown' && firstMenuItem) {
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
if (event.key === 'ArrowDown' && typeof firstMenuItem !== 'undefined') {
|
||||
menu!.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && lastMenuItem) {
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
if (event.key === 'ArrowUp' && typeof lastMenuItem !== 'undefined') {
|
||||
menu!.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
@@ -287,9 +289,8 @@ export default class SlDropdown extends LitElement {
|
||||
|
||||
// Other keys bring focus to the menu and initiate type-to-select behavior
|
||||
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
||||
if (this.open && menu && !ignoredKeys.includes(event.key)) {
|
||||
menu.typeToSelect(event.key);
|
||||
return;
|
||||
if (this.open && !ignoredKeys.includes(event.key)) {
|
||||
menu?.typeToSelect(event.key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,22 +316,20 @@ export default class SlDropdown extends LitElement {
|
||||
// To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger."
|
||||
//
|
||||
updateAccessibleTrigger() {
|
||||
if (this.trigger) {
|
||||
const slot = this.trigger.querySelector('slot') as HTMLSlotElement;
|
||||
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
|
||||
const slot = this.trigger.querySelector('slot')!;
|
||||
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
|
||||
|
||||
if (accessibleTrigger) {
|
||||
accessibleTrigger.setAttribute('aria-haspopup', 'true');
|
||||
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
||||
}
|
||||
if (typeof accessibleTrigger !== 'undefined') {
|
||||
accessibleTrigger.setAttribute('aria-haspopup', 'true');
|
||||
accessibleTrigger.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the dropdown panel. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
@@ -340,7 +339,7 @@ export default class SlDropdown extends LitElement {
|
||||
/** Hides the dropdown panel */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
@@ -356,7 +355,7 @@ export default class SlDropdown extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.popover.update();
|
||||
void this.popover?.update();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
@@ -376,7 +375,7 @@ export default class SlDropdown extends LitElement {
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.popover.update();
|
||||
void this.popover?.update();
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { formatBytes } from '../../internal/number';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { formatBytes } from '~/internal/number';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -9,7 +9,7 @@ import { LocalizeController } from '../../utilities/localize';
|
||||
*/
|
||||
@customElement('sl-format-bytes')
|
||||
export default class SlFormatBytes extends LitElement {
|
||||
private localize = new LocalizeController(this);
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** The number to format in bytes. */
|
||||
@property({ type: Number }) value = 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
|
||||
*/
|
||||
@customElement('sl-format-date')
|
||||
export default class SlFormatDate extends LitElement {
|
||||
private localize = new LocalizeController(this);
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** The date/time to format. If not set, the current date and time will be used. */
|
||||
@property() date: Date | string = new Date();
|
||||
@@ -55,7 +55,7 @@ export default class SlFormatDate extends LitElement {
|
||||
|
||||
// Check for an invalid date
|
||||
if (isNaN(date.getMilliseconds())) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.localize.date(date, {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -8,7 +8,7 @@ import { LocalizeController } from '../../utilities/localize';
|
||||
*/
|
||||
@customElement('sl-format-number')
|
||||
export default class SlFormatNumber extends LitElement {
|
||||
private localize = new LocalizeController(this);
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** The number to format. */
|
||||
@property({ type: Number }) value = 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -3,8 +3,7 @@ import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import styles from './icon-button.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -21,22 +20,22 @@ export default class SlIconButton extends LitElement {
|
||||
@query('button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
/** The name of the icon to draw. */
|
||||
@property() name: string;
|
||||
@property() name?: string;
|
||||
|
||||
/** The name of a registered custom icon library. */
|
||||
@property() library: string;
|
||||
@property() library?: string;
|
||||
|
||||
/** An external URL of an SVG file. */
|
||||
@property() src: string;
|
||||
@property() src?: string;
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property() href: string;
|
||||
@property() href?: string;
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
@property() target?: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||
@property() download: string;
|
||||
@property() download?: string;
|
||||
|
||||
/**
|
||||
* A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should
|
||||
@@ -48,7 +47,7 @@ export default class SlIconButton extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
render() {
|
||||
const isLink = this.href ? true : false;
|
||||
const isLink = typeof this.href !== 'undefined';
|
||||
|
||||
const interior = html`
|
||||
<sl-icon
|
||||
@@ -67,7 +66,7 @@ export default class SlIconButton extends LitElement {
|
||||
href=${ifDefined(this.href)}
|
||||
target=${ifDefined(this.target)}
|
||||
download=${ifDefined(this.download)}
|
||||
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
|
||||
rel=${ifDefined(typeof this.target !== 'undefined' ? 'noreferrer noopener' : undefined)}
|
||||
role="button"
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
aria-label="${this.label}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './icon.styles';
|
||||
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
|
||||
import { requestIcon } from './request';
|
||||
import styles from './icon.styles';
|
||||
import { emit } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
@@ -26,13 +26,17 @@ export default class SlIcon extends LitElement {
|
||||
@state() private svg = '';
|
||||
|
||||
/** The name of the icon to draw. */
|
||||
@property() name: string;
|
||||
@property() name?: string;
|
||||
|
||||
/** An external URL of an SVG file. */
|
||||
@property() src: string;
|
||||
/**
|
||||
* An external URL of an SVG file.
|
||||
*
|
||||
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
|
||||
*/
|
||||
@property() src?: string;
|
||||
|
||||
/** An alternate description to use for accessibility. If omitted, the icon will be ignored by assistive devices. */
|
||||
@property() label: string;
|
||||
@property() label = '';
|
||||
|
||||
/** The name of a registered custom icon library. */
|
||||
@property() library = 'default';
|
||||
@@ -43,7 +47,7 @@ export default class SlIcon extends LitElement {
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.setIcon();
|
||||
void this.setIcon();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -51,18 +55,17 @@ export default class SlIcon extends LitElement {
|
||||
unwatchIcon(this);
|
||||
}
|
||||
|
||||
private getUrl(): string {
|
||||
private getUrl() {
|
||||
const library = getIconLibrary(this.library);
|
||||
if (this.name && library) {
|
||||
if (typeof this.name !== 'undefined' && typeof library !== 'undefined') {
|
||||
return library.resolver(this.name);
|
||||
} else {
|
||||
return this.src;
|
||||
}
|
||||
return this.src;
|
||||
}
|
||||
|
||||
/** @internal Fetches the icon and redraws it. Used to handle library registrations. */
|
||||
redraw() {
|
||||
this.setIcon();
|
||||
void this.setIcon();
|
||||
}
|
||||
|
||||
@watch('name')
|
||||
@@ -71,7 +74,7 @@ export default class SlIcon extends LitElement {
|
||||
async setIcon() {
|
||||
const library = getIconLibrary(this.library);
|
||||
const url = this.getUrl();
|
||||
if (url) {
|
||||
if (typeof url !== 'undefined' && url.length > 0) {
|
||||
try {
|
||||
const file = await requestIcon(url)!;
|
||||
if (url !== this.getUrl()) {
|
||||
@@ -81,10 +84,8 @@ export default class SlIcon extends LitElement {
|
||||
const doc = parser.parseFromString(file.svg, 'text/html');
|
||||
const svgEl = doc.body.querySelector('svg');
|
||||
|
||||
if (svgEl) {
|
||||
if (library && library.mutator) {
|
||||
library.mutator(svgEl);
|
||||
}
|
||||
if (svgEl !== null) {
|
||||
library?.mutator?.(svgEl);
|
||||
|
||||
this.svg = svgEl.outerHTML;
|
||||
emit(this, 'sl-load');
|
||||
@@ -99,14 +100,14 @@ export default class SlIcon extends LitElement {
|
||||
} catch {
|
||||
emit(this, 'sl-error', { detail: { status: -1 } });
|
||||
}
|
||||
} else if (this.svg) {
|
||||
} else if (this.svg.length > 0) {
|
||||
// If we can't resolve a URL and an icon was previously set, remove it
|
||||
this.svg = '';
|
||||
}
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.setIcon();
|
||||
void this.setIcon();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBasePath } from '../../utilities/base-path';
|
||||
import type { IconLibrary } from './library';
|
||||
import { getBasePath } from '~/utilities/base-path';
|
||||
|
||||
const library: IconLibrary = {
|
||||
name: 'default',
|
||||
|
||||
@@ -86,11 +86,10 @@ const icons = {
|
||||
const systemLibrary: IconLibrary = {
|
||||
name: 'system',
|
||||
resolver: (name: keyof typeof icons) => {
|
||||
if (icons[name]) {
|
||||
if (name in icons) {
|
||||
return `data:image/svg+xml,${encodeURIComponent(icons[name])}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import defaultLibrary from './library.default';
|
||||
import systemLibrary from './library.system';
|
||||
import type SlIcon from '../icon/icon';
|
||||
import type SlIcon from '~/components/icon/icon';
|
||||
|
||||
export type IconLibraryResolver = (name: string) => string;
|
||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||
@@ -22,7 +22,7 @@ export function unwatchIcon(icon: SlIcon) {
|
||||
}
|
||||
|
||||
export function getIconLibrary(name?: string) {
|
||||
return registry.filter(lib => lib.name === name)[0];
|
||||
return registry.find(lib => lib.name === name);
|
||||
}
|
||||
|
||||
export function registerIconLibrary(
|
||||
@@ -37,7 +37,7 @@ export function registerIconLibrary(
|
||||
});
|
||||
|
||||
// Redraw watched icons
|
||||
watchedIcons.map(icon => {
|
||||
watchedIcons.forEach(icon => {
|
||||
if (icon.library === name) {
|
||||
icon.redraw();
|
||||
}
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
interface IconFile {
|
||||
import { requestInclude } from '~/components/include/request';
|
||||
|
||||
type IconFile =
|
||||
| {
|
||||
ok: true;
|
||||
status: number;
|
||||
svg: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
status: number;
|
||||
svg: null;
|
||||
};
|
||||
|
||||
interface IconFileUnknown {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
svg: string;
|
||||
svg: string | null;
|
||||
}
|
||||
|
||||
const iconFiles = new Map<string, Promise<IconFile>>();
|
||||
const iconFiles = new Map<string, IconFile>();
|
||||
|
||||
export const requestIcon = (url: string) => {
|
||||
export async function requestIcon(url: string): Promise<IconFile> {
|
||||
if (iconFiles.has(url)) {
|
||||
return iconFiles.get(url);
|
||||
} else {
|
||||
const request = fetch(url).then(async response => {
|
||||
if (response.ok) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = await response.text();
|
||||
const svg = div.firstElementChild;
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
svg: svg && svg.tagName.toLowerCase() === 'svg' ? svg.outerHTML : ''
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
svg: null
|
||||
};
|
||||
}
|
||||
}) as Promise<IconFile>;
|
||||
|
||||
iconFiles.set(url, request);
|
||||
return request;
|
||||
return iconFiles.get(url)!;
|
||||
}
|
||||
};
|
||||
const fileData = await requestInclude(url);
|
||||
const iconFileData: IconFileUnknown = {
|
||||
ok: fileData.ok,
|
||||
status: fileData.status,
|
||||
svg: null
|
||||
};
|
||||
if (fileData.ok) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = fileData.html;
|
||||
const svg = div.firstElementChild;
|
||||
iconFileData.svg = svg?.tagName.toLowerCase() === 'svg' ? svg.outerHTML : '';
|
||||
}
|
||||
|
||||
iconFiles.set(url, iconFileData as IconFile);
|
||||
return iconFileData as IconFile;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './image-comparer.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
import { autoIncrement } from '~/internal/autoIncrement';
|
||||
import { drag } from '~/internal/drag';
|
||||
import { emit } from '~/internal/event';
|
||||
import { clamp } from '~/internal/math';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -36,42 +37,19 @@ export default class SlImageComparer extends LitElement {
|
||||
@query('.image-comparer') base: HTMLElement;
|
||||
@query('.image-comparer__handle') handle: HTMLElement;
|
||||
|
||||
private readonly attrId = autoIncrement();
|
||||
private readonly containerId = `comparer-container-${this.attrId}`;
|
||||
|
||||
/** The position of the divider as a percentage. */
|
||||
@property({ type: Number, reflect: true }) position = 50;
|
||||
|
||||
handleDrag(event: any) {
|
||||
handleDrag(event: Event) {
|
||||
const { width } = this.base.getBoundingClientRect();
|
||||
|
||||
function drag(event: any, container: HTMLElement, onMove: (x: number) => void) {
|
||||
const move = (event: any) => {
|
||||
const { left } = container.getBoundingClientRect();
|
||||
const { pageXOffset } = container.ownerDocument.defaultView!;
|
||||
const offsetX = left + pageXOffset;
|
||||
const x = (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - offsetX;
|
||||
|
||||
onMove(x);
|
||||
};
|
||||
|
||||
// Move on init
|
||||
move(event);
|
||||
|
||||
const stop = () => {
|
||||
document.removeEventListener('mousemove', move);
|
||||
document.removeEventListener('touchmove', move);
|
||||
document.removeEventListener('mouseup', stop);
|
||||
document.removeEventListener('touchend', stop);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', move);
|
||||
document.addEventListener('touchmove', move);
|
||||
document.addEventListener('mouseup', stop);
|
||||
document.addEventListener('touchend', stop);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
drag(event, this.base, x => {
|
||||
this.position = Number(clamp((x / width) * 100, 0, 100).toFixed(2));
|
||||
drag(this.base, x => {
|
||||
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,10 +60,18 @@ export default class SlImageComparer extends LitElement {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'ArrowLeft') newPosition = newPosition - incr;
|
||||
if (event.key === 'ArrowRight') newPosition = newPosition + incr;
|
||||
if (event.key === 'Home') newPosition = 0;
|
||||
if (event.key === 'End') newPosition = 100;
|
||||
if (event.key === 'ArrowLeft') {
|
||||
newPosition -= incr;
|
||||
}
|
||||
if (event.key === 'ArrowRight') {
|
||||
newPosition += incr;
|
||||
}
|
||||
if (event.key === 'Home') {
|
||||
newPosition = 0;
|
||||
}
|
||||
if (event.key === 'End') {
|
||||
newPosition = 100;
|
||||
}
|
||||
newPosition = clamp(newPosition, 0, 100);
|
||||
|
||||
this.position = newPosition;
|
||||
@@ -99,7 +85,7 @@ export default class SlImageComparer extends LitElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div part="base" class="image-comparer" @keydown=${this.handleKeyDown}>
|
||||
<div part="base" class="image-comparer" @keydown=${this.handleKeyDown} id=${this.containerId}>
|
||||
<div class="image-comparer__image">
|
||||
<div part="before" class="image-comparer__before">
|
||||
<slot name="before"></slot>
|
||||
@@ -117,7 +103,7 @@ export default class SlImageComparer extends LitElement {
|
||||
<div
|
||||
part="divider"
|
||||
class="image-comparer__divider"
|
||||
style=${styleMap({ left: this.position + '%' })}
|
||||
style=${styleMap({ left: `${this.position}%` })}
|
||||
@mousedown=${this.handleDrag}
|
||||
@touchstart=${this.handleDrag}
|
||||
>
|
||||
@@ -128,6 +114,7 @@ export default class SlImageComparer extends LitElement {
|
||||
aria-valuenow=${this.position}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-controls=${this.containerId}
|
||||
tabindex="0"
|
||||
>
|
||||
<slot name="handle-icon">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlInclude from './include';
|
||||
|
||||
const stubbedFetchResponse: Response = {
|
||||
@@ -22,6 +20,14 @@ const stubbedFetchResponse: Response = {
|
||||
clone: sinon.fake()
|
||||
};
|
||||
|
||||
function delayResolve(resolveValue: string) {
|
||||
return new Promise<string>(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(resolveValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('<sl-include>', () => {
|
||||
afterEach(() => {
|
||||
sinon.verifyAndRestore();
|
||||
@@ -32,7 +38,7 @@ describe('<sl-include>', () => {
|
||||
...stubbedFetchResponse,
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () => Promise.resolve('"id": 1')
|
||||
text: () => delayResolve('"id": 1')
|
||||
});
|
||||
const el = await fixture<SlInclude>(html` <sl-include src="/found"></sl-include> `);
|
||||
const loadHandler = sinon.spy();
|
||||
@@ -49,7 +55,7 @@ describe('<sl-include>', () => {
|
||||
...stubbedFetchResponse,
|
||||
ok: false,
|
||||
status: 404,
|
||||
text: () => Promise.resolve('{}')
|
||||
text: () => delayResolve('{}')
|
||||
});
|
||||
const el = await fixture<SlInclude>(html` <sl-include src="/not-found"></sl-include> `);
|
||||
const loadHandler = sinon.spy();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { requestInclude } from './request';
|
||||
import styles from './include.styles';
|
||||
import { requestInclude } from './request';
|
||||
import { emit } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -16,7 +16,11 @@ import styles from './include.styles';
|
||||
export default class SlInclude extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
/** The location of the HTML file to include. */
|
||||
/**
|
||||
* The location of the HTML file to include.
|
||||
*
|
||||
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
|
||||
*/
|
||||
@property() src: string;
|
||||
|
||||
/** The fetch mode to use. */
|
||||
@@ -31,7 +35,9 @@ export default class SlInclude extends LitElement {
|
||||
executeScript(script: HTMLScriptElement) {
|
||||
// Create a copy of the script and swap it out so the browser executes it
|
||||
const newScript = document.createElement('script');
|
||||
[...script.attributes].map(attr => newScript.setAttribute(attr.name, attr.value));
|
||||
[...script.attributes].forEach(attr => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
newScript.textContent = script.textContent;
|
||||
script.parentNode!.replaceChild(newScript, script);
|
||||
}
|
||||
@@ -47,7 +53,7 @@ export default class SlInclude extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
if (typeof file === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,7 +65,9 @@ export default class SlInclude extends LitElement {
|
||||
this.innerHTML = file.html;
|
||||
|
||||
if (this.allowScripts) {
|
||||
[...this.querySelectorAll('script')].map(script => this.executeScript(script));
|
||||
[...this.querySelectorAll('script')].forEach(script => {
|
||||
this.executeScript(script);
|
||||
});
|
||||
}
|
||||
|
||||
emit(this, 'sl-load');
|
||||
|
||||
@@ -6,18 +6,17 @@ interface IncludeFile {
|
||||
|
||||
const includeFiles = new Map<string, Promise<IncludeFile>>();
|
||||
|
||||
export const requestInclude = async (src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors') => {
|
||||
export function requestInclude(src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors'): Promise<IncludeFile> {
|
||||
if (includeFiles.has(src)) {
|
||||
return includeFiles.get(src);
|
||||
} else {
|
||||
const request = fetch(src, { mode: mode }).then(async response => {
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
html: await response.text()
|
||||
};
|
||||
});
|
||||
includeFiles.set(src, request);
|
||||
return request;
|
||||
return includeFiles.get(src)!;
|
||||
}
|
||||
};
|
||||
const fileDataPromise = fetch(src, { mode: mode }).then(async response => {
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
html: await response.text()
|
||||
};
|
||||
});
|
||||
includeFiles.set(src, fileDataPromise);
|
||||
return fileDataPromise;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import formControlStyles from '../../styles/form-control.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
import formControlStyles from '~/styles/form-control.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlInput from './input';
|
||||
|
||||
describe('<sl-input>', () => {
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `);
|
||||
const input = el.shadowRoot?.querySelector('[part="input"]') as HTMLInputElement;
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part="input"]')!;
|
||||
|
||||
expect(input.disabled).to.be.true;
|
||||
});
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { FormSubmitController, getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './input.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
|
||||
let id = 0;
|
||||
import '~/components/icon/icon';
|
||||
import { autoIncrement } from '~/internal/autoIncrement';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController, getLabelledBy, renderFormControl } from '~/internal/form-control';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -49,12 +47,13 @@ export default class SlInput extends LitElement {
|
||||
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `input-${++id}`;
|
||||
private helpTextId = `input-help-text-${id}`;
|
||||
private labelId = `input-label-${id}`;
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly attrId = autoIncrement();
|
||||
private readonly inputId = `input-${this.attrId}`;
|
||||
private readonly helpTextId = `input-help-text-${this.attrId}`;
|
||||
private readonly labelId = `input-label-${this.attrId}`;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() private isPasswordVisible = false;
|
||||
@@ -79,7 +78,7 @@ export default class SlInput extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
/** The input's label. Alternatively, you can use the label slot. */
|
||||
@property() label: string;
|
||||
@property() label = '';
|
||||
|
||||
/** The input's help text. Alternatively, you can use the help-text slot. */
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
@@ -146,7 +145,7 @@ export default class SlInput extends LitElement {
|
||||
|
||||
/** Gets or sets the current value as a `Date` object. Only valid when `type` is `date`. */
|
||||
get valueAsDate() {
|
||||
return this.input.valueAsDate as Date;
|
||||
return this.input.valueAsDate!;
|
||||
}
|
||||
|
||||
set valueAsDate(newValue: Date) {
|
||||
@@ -156,7 +155,7 @@ export default class SlInput extends LitElement {
|
||||
|
||||
/** Gets or sets the current value as a number. */
|
||||
get valueAsNumber() {
|
||||
return this.input.valueAsNumber as number;
|
||||
return this.input.valueAsNumber;
|
||||
}
|
||||
|
||||
set valueAsNumber(newValue: number) {
|
||||
@@ -180,7 +179,7 @@ export default class SlInput extends LitElement {
|
||||
|
||||
/** Selects all the text in the input. */
|
||||
select() {
|
||||
return this.input.select();
|
||||
this.input.select();
|
||||
}
|
||||
|
||||
/** Sets the start and end positions of the text selection (0-based). */
|
||||
@@ -189,7 +188,7 @@ export default class SlInput extends LitElement {
|
||||
selectionEnd: number,
|
||||
selectionDirection: 'forward' | 'backward' | 'none' = 'none'
|
||||
) {
|
||||
return this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
|
||||
this.input.setSelectionRange(selectionStart, selectionEnd, selectionDirection);
|
||||
}
|
||||
|
||||
/** Replaces a range of text with a new string. */
|
||||
@@ -239,13 +238,11 @@ export default class SlInput extends LitElement {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.input) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
@@ -266,11 +263,9 @@ export default class SlInput extends LitElement {
|
||||
this.isPasswordVisible = !this.isPasswordVisible;
|
||||
}
|
||||
|
||||
@watch('value')
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
if (this.input) {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -306,7 +301,7 @@ export default class SlInput extends LitElement {
|
||||
'input--filled': this.filled,
|
||||
'input--disabled': this.disabled,
|
||||
'input--focused': this.hasFocus,
|
||||
'input--empty': this.value?.length === 0,
|
||||
'input--empty': this.value.length === 0,
|
||||
'input--invalid': this.invalid
|
||||
})}
|
||||
>
|
||||
@@ -355,7 +350,7 @@ export default class SlInput extends LitElement {
|
||||
@blur=${this.handleBlur}
|
||||
/>
|
||||
|
||||
${this.clearable && this.value?.length > 0
|
||||
${this.clearable && this.value.length > 0
|
||||
? html`
|
||||
<button
|
||||
part="clear-button"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './menu-item.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -43,12 +42,12 @@ export default class SlMenuItem extends LitElement {
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.setAttribute('aria-checked', String(this.checked));
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', String(this.disabled));
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, query } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { hasFocusVisible } from '../../internal/focus-visible';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from './menu.styles';
|
||||
import type SlMenuItem from '~/components/menu-item/menu-item';
|
||||
import { emit } from '~/internal/event';
|
||||
import { hasFocusVisible } from '~/internal/focus-visible';
|
||||
import { getTextContent } from '~/internal/slot';
|
||||
|
||||
export interface MenuSelectEventDetail {
|
||||
item: SlMenuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -24,7 +28,7 @@ export default class SlMenu extends LitElement {
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
|
||||
private typeToSelectString = '';
|
||||
private typeToSelectTimeout: any;
|
||||
private typeToSelectTimeout: NodeJS.Timeout;
|
||||
|
||||
firstUpdated() {
|
||||
this.setAttribute('role', 'menu');
|
||||
@@ -36,7 +40,7 @@ export default class SlMenu extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options?.includeDisabled && (el as SlMenuItem).disabled) {
|
||||
if (!options.includeDisabled && (el as SlMenuItem).disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -58,10 +62,12 @@ export default class SlMenu extends LitElement {
|
||||
*/
|
||||
setCurrentItem(item: SlMenuItem) {
|
||||
const items = this.getAllItems({ includeDisabled: false });
|
||||
let activeItem = item.disabled ? items[0] : item;
|
||||
const activeItem = item.disabled ? items[0] : item;
|
||||
|
||||
// Update tab indexes
|
||||
items.map(i => i.setAttribute('tabindex', i === activeItem ? '0' : '-1'));
|
||||
items.forEach(i => {
|
||||
i.setAttribute('tabindex', i === activeItem ? '0' : '-1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,13 +84,15 @@ export default class SlMenu extends LitElement {
|
||||
|
||||
// Restore focus in browsers that don't support :focus-visible when using the keyboard
|
||||
if (!hasFocusVisible) {
|
||||
items.map(item => item.classList.remove('sl-focus-invisible'));
|
||||
items.forEach(item => {
|
||||
item.classList.remove('sl-focus-invisible');
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const slot = item.shadowRoot!.querySelector('slot:not([name])') as HTMLSlotElement;
|
||||
const slot = item.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');
|
||||
const label = getTextContent(slot).toLowerCase().trim();
|
||||
if (label.substring(0, this.typeToSelectString.length) === this.typeToSelectString) {
|
||||
if (label.startsWith(this.typeToSelectString)) {
|
||||
this.setCurrentItem(item);
|
||||
|
||||
// Set focus here to force the browser to show :focus-visible styles
|
||||
@@ -96,9 +104,9 @@ export default class SlMenu extends LitElement {
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const item = target.closest('sl-menu-item') as SlMenuItem;
|
||||
const item = target.closest('sl-menu-item');
|
||||
|
||||
if (item && !item.disabled) {
|
||||
if (item?.disabled === false) {
|
||||
emit(this, 'sl-select', { detail: { item } });
|
||||
}
|
||||
}
|
||||
@@ -107,7 +115,9 @@ export default class SlMenu extends LitElement {
|
||||
// Restore focus in browsers that don't support :focus-visible when using the keyboard
|
||||
if (!hasFocusVisible) {
|
||||
const items = this.getAllItems();
|
||||
items.map(item => item.classList.remove('sl-focus-invisible'));
|
||||
items.forEach(item => {
|
||||
item.classList.remove('sl-focus-invisible');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +127,8 @@ export default class SlMenu extends LitElement {
|
||||
const item = this.getCurrentItem();
|
||||
event.preventDefault();
|
||||
|
||||
if (item) {
|
||||
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
||||
item.click();
|
||||
}
|
||||
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
||||
item?.click();
|
||||
}
|
||||
|
||||
// Prevent scrolling when space is pressed
|
||||
@@ -132,9 +140,9 @@ export default class SlMenu extends LitElement {
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||
const items = this.getAllItems({ includeDisabled: false });
|
||||
const activeItem = this.getCurrentItem();
|
||||
let index = activeItem ? items.indexOf(activeItem) : 0;
|
||||
let index = typeof activeItem !== 'undefined' ? items.indexOf(activeItem) : 0;
|
||||
|
||||
if (items.length) {
|
||||
if (items.length > 0) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
@@ -147,8 +155,12 @@ export default class SlMenu extends LitElement {
|
||||
index = items.length - 1;
|
||||
}
|
||||
|
||||
if (index < 0) index = 0;
|
||||
if (index > items.length - 1) index = items.length - 1;
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
if (index > items.length - 1) {
|
||||
index = items.length - 1;
|
||||
}
|
||||
|
||||
this.setCurrentItem(items[index]);
|
||||
items[index].focus();
|
||||
@@ -177,7 +189,7 @@ export default class SlMenu extends LitElement {
|
||||
const items = this.getAllItems({ includeDisabled: false });
|
||||
|
||||
// Reset the roving tab index when the slotted items change
|
||||
if (items.length) {
|
||||
if (items.length > 0) {
|
||||
this.setCurrentItem(items[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlMutationObserver from './mutation-observer';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
describe('<sl-mutation-observer>', () => {
|
||||
it('should render a component', async () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './mutation-observer.styles';
|
||||
import { emit } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlProgressBar from './progress-bar';
|
||||
|
||||
describe('<sl-progress-bar>', () => {
|
||||
let el: SlProgressBar;
|
||||
|
||||
describe('when provided just a value parameter', async () => {
|
||||
describe('when provided just a value parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
|
||||
});
|
||||
@@ -16,7 +14,7 @@ describe('<sl-progress-bar>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a title, and value parameter', async () => {
|
||||
describe('when provided a title, and value parameter', () => {
|
||||
let base: HTMLDivElement;
|
||||
let indicator: HTMLDivElement;
|
||||
|
||||
@@ -24,43 +22,43 @@ describe('<sl-progress-bar>', () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement;
|
||||
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
indicator = el.shadowRoot!.querySelector('[part="indicator"]')!;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
||||
it('uses the value parameter on the base, as aria-valuenow', () => {
|
||||
expect(base).attribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', async () => {
|
||||
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', () => {
|
||||
expect(indicator).attribute('style', 'width:25%;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an indeterminate parameter', async () => {
|
||||
describe('when provided an indeterminate parameter', () => {
|
||||
let base: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should append a progress-bar--indeterminate class to the "base" part.', async () => {
|
||||
it('should append a progress-bar--indeterminate class to the "base" part.', () => {
|
||||
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
||||
describe('when provided a ariaLabel, and value parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
|
||||
@@ -72,7 +70,7 @@ describe('<sl-progress-bar>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
||||
describe('when provided a ariaLabelledBy, and value parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './progress-bar.styles';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -24,7 +24,7 @@ import styles from './progress-bar.styles';
|
||||
@customElement('sl-progress-bar')
|
||||
export default class SlProgressBar extends LitElement {
|
||||
static styles = styles;
|
||||
private localize = new LocalizeController(this);
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** The current progress, 0 to 100. */
|
||||
@property({ type: Number, reflect: true }) value = 0;
|
||||
@@ -33,7 +33,7 @@ export default class SlProgressBar extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||
|
||||
/** A custom label for the progress bar's aria label. */
|
||||
@property() label: string;
|
||||
@property() label = '';
|
||||
|
||||
/** The locale to render the component in. */
|
||||
@property() lang: string;
|
||||
@@ -48,12 +48,12 @@ export default class SlProgressBar extends LitElement {
|
||||
})}
|
||||
role="progressbar"
|
||||
title=${ifDefined(this.title)}
|
||||
aria-label=${this.label || this.localize.term('progress')}
|
||||
aria-label=${this.label.length > 0 ? this.label : this.localize.term('progress')}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow=${this.indeterminate ? 0 : this.value}
|
||||
>
|
||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.value + '%' })}>
|
||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: `${this.value}%` })}>
|
||||
${!this.indeterminate
|
||||
? html`
|
||||
<span part="label" class="progress-bar__label">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlProgressRing from './progress-ring';
|
||||
|
||||
describe('<sl-progress-ring>', () => {
|
||||
let el: SlProgressRing;
|
||||
|
||||
describe('when provided just a value parameter', async () => {
|
||||
describe('when provided just a value parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
|
||||
});
|
||||
@@ -16,30 +14,30 @@ describe('<sl-progress-ring>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a title, and value parameter', async () => {
|
||||
describe('when provided a title, and value parameter', () => {
|
||||
let base: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
base = el.shadowRoot!.querySelector('[part="base"]')!;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
||||
it('uses the value parameter on the base, as aria-valuenow', () => {
|
||||
expect(base).attribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', async () => {
|
||||
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', () => {
|
||||
expect(base).attribute('style', '--percentage: 0.25');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
||||
describe('when provided a ariaLabel, and value parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
|
||||
@@ -51,7 +49,7 @@ describe('<sl-progress-ring>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
||||
describe('when provided a ariaLabelledBy, and value parameter', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './progress-ring.styles';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -20,7 +20,7 @@ import styles from './progress-ring.styles';
|
||||
@customElement('sl-progress-ring')
|
||||
export default class SlProgressRing extends LitElement {
|
||||
static styles = styles;
|
||||
private localize = new LocalizeController(this);
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.progress-ring__indicator') indicator: SVGCircleElement;
|
||||
|
||||
@@ -30,12 +30,12 @@ export default class SlProgressRing extends LitElement {
|
||||
@property({ type: Number, reflect: true }) value = 0;
|
||||
|
||||
/** A custom label for the progress ring's aria label. */
|
||||
@property() label: string;
|
||||
@property() label = '';
|
||||
|
||||
/** The locale to render the component in. */
|
||||
@property() lang: string;
|
||||
|
||||
updated(changedProps: Map<string, any>) {
|
||||
updated(changedProps: Map<string, unknown>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
//
|
||||
@@ -48,7 +48,7 @@ export default class SlProgressRing extends LitElement {
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (this.value / 100) * circumference;
|
||||
|
||||
this.indicatorOffset = String(offset) + 'px';
|
||||
this.indicatorOffset = `${offset}px`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export default class SlProgressRing extends LitElement {
|
||||
part="base"
|
||||
class="progress-ring"
|
||||
role="progressbar"
|
||||
aria-label=${this.label || this.localize.term('progress')}
|
||||
aria-label=${this.label.length > 0 ? this.label : this.localize.term('progress')}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.value}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import QrCreator from 'qr-creator';
|
||||
import styles from './qr-code.styles';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -77,7 +77,7 @@ export default class SlQrCode extends LitElement {
|
||||
height: `${this.size}px`
|
||||
})}
|
||||
>
|
||||
<canvas role="img" aria-label=${this.label || this.value}></canvas>
|
||||
<canvas role="img" aria-label=${this.label.length > 0 ? this.label : this.value}></canvas>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import type SlRadio from '../radio/radio';
|
||||
import styles from './radio-group.styles';
|
||||
import type SlRadio from '~/components/radio/radio';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -29,13 +29,11 @@ export default class SlRadioGroup extends LitElement {
|
||||
handleFocusIn() {
|
||||
// When tabbing into the fieldset, make sure it lands on the checked radio
|
||||
requestAnimationFrame(() => {
|
||||
const checkedRadio = [...this.defaultSlot.assignedElements({ flatten: true })].find(
|
||||
el => el.tagName.toLowerCase() === 'sl-radio' && (el as SlRadio).checked
|
||||
) as SlRadio;
|
||||
const checkedRadio = [...(this.defaultSlot.assignedElements({ flatten: true }) as SlRadio[])].find(
|
||||
el => el.tagName.toLowerCase() === 'sl-radio' && el.checked
|
||||
);
|
||||
|
||||
if (checkedRadio) {
|
||||
checkedRadio.focus();
|
||||
}
|
||||
checkedRadio?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlRadio from './radio';
|
||||
import type SlRadioGroup from '../radio-group/radio-group';
|
||||
import type SlRadioGroup from '~/components/radio-group/radio-group';
|
||||
|
||||
describe('<sl-radio>', () => {
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlRadio>(html` <sl-radio disabled></sl-radio> `);
|
||||
const radio = el.shadowRoot?.querySelector('input');
|
||||
const radio = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
|
||||
|
||||
expect(radio.disabled).to.be.true;
|
||||
});
|
||||
@@ -21,7 +19,7 @@ describe('<sl-radio>', () => {
|
||||
|
||||
it('should fire sl-change when clicked', async () => {
|
||||
const el = await fixture<SlRadio>(html` <sl-radio></sl-radio> `);
|
||||
setTimeout(() => el.shadowRoot?.querySelector('input').click());
|
||||
setTimeout(() => el.shadowRoot?.querySelector('input')?.click());
|
||||
const event = await oneEvent(el, 'sl-change');
|
||||
expect(event.target).to.equal(el);
|
||||
expect(el.checked).to.be.true;
|
||||
@@ -29,9 +27,11 @@ describe('<sl-radio>', () => {
|
||||
|
||||
it('should fire sl-change when toggled via keyboard - space', async () => {
|
||||
const el = await fixture<SlRadio>(html` <sl-radio></sl-radio> `);
|
||||
const input = el.shadowRoot?.querySelector('input');
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
|
||||
input.focus();
|
||||
setTimeout(() => sendKeys({ press: ' ' }));
|
||||
setTimeout(() => {
|
||||
void sendKeys({ press: ' ' });
|
||||
});
|
||||
const event = await oneEvent(el, 'sl-change');
|
||||
expect(event.target).to.equal(el);
|
||||
expect(el.checked).to.be.true;
|
||||
@@ -44,11 +44,13 @@ describe('<sl-radio>', () => {
|
||||
<sl-radio id="radio-2"></sl-radio>
|
||||
</sl-radio-group>
|
||||
`);
|
||||
const radio1: SlRadio = radioGroup.querySelector('sl-radio#radio-1');
|
||||
const radio2: SlRadio = radioGroup.querySelector('sl-radio#radio-2');
|
||||
const input1 = radio1.shadowRoot?.querySelector('input');
|
||||
const radio1 = radioGroup.querySelector<SlRadio>('sl-radio#radio-1')!;
|
||||
const radio2 = radioGroup.querySelector<SlRadio>('sl-radio#radio-2')!;
|
||||
const input1 = radio1.shadowRoot!.querySelector<HTMLInputElement>('input')!;
|
||||
input1.focus();
|
||||
setTimeout(() => sendKeys({ press: 'ArrowRight' }));
|
||||
setTimeout(() => {
|
||||
void sendKeys({ press: 'ArrowRight' });
|
||||
});
|
||||
const event = await oneEvent(radio2, 'sl-change');
|
||||
expect(event.target).to.equal(radio2);
|
||||
expect(radio2.checked).to.be.true;
|
||||
|
||||
@@ -3,10 +3,10 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import styles from './radio.styles';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController } from '~/internal/form-control';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -29,8 +29,8 @@ export default class SlRadio extends LitElement {
|
||||
|
||||
@query('input[type="radio"]') input: HTMLInputElement;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this, {
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
value: (control: SlRadio) => (control.checked ? control.value : undefined)
|
||||
});
|
||||
|
||||
@@ -58,17 +58,17 @@ export default class SlRadio extends LitElement {
|
||||
const radios = this.getAllRadios();
|
||||
const checkedRadio = radios.find(radio => radio.checked);
|
||||
|
||||
radios.map(radio => {
|
||||
if (radio.input) {
|
||||
setTimeout(() => {
|
||||
radios.forEach(radio => {
|
||||
radio.input.tabIndex = -1;
|
||||
});
|
||||
|
||||
if (typeof checkedRadio !== 'undefined') {
|
||||
checkedRadio.input.tabIndex = 0;
|
||||
} else if (radios.length > 0) {
|
||||
radios[0].input.tabIndex = 0;
|
||||
}
|
||||
});
|
||||
|
||||
if (checkedRadio) {
|
||||
checkedRadio.input.tabIndex = 0;
|
||||
} else if (radios.length) {
|
||||
radios[0].input.tabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** Simulates a click on the radio. */
|
||||
@@ -101,7 +101,7 @@ export default class SlRadio extends LitElement {
|
||||
const radioGroup = this.closest('sl-radio-group');
|
||||
|
||||
// Radios must be part of a radio group
|
||||
if (!radioGroup) {
|
||||
if (radioGroup === null) {
|
||||
return [this];
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export default class SlRadio extends LitElement {
|
||||
}
|
||||
|
||||
getSiblingRadios() {
|
||||
return this.getAllRadios().filter(radio => radio !== this) as this[];
|
||||
return this.getAllRadios().filter(radio => radio !== this);
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
@@ -122,7 +122,7 @@ export default class SlRadio extends LitElement {
|
||||
if (this.checked) {
|
||||
this.input.tabIndex = 0;
|
||||
|
||||
this.getSiblingRadios().map(radio => {
|
||||
this.getSiblingRadios().forEach(radio => {
|
||||
radio.input.tabIndex = -1;
|
||||
radio.checked = false;
|
||||
});
|
||||
@@ -134,13 +134,11 @@ export default class SlRadio extends LitElement {
|
||||
emit(this, 'sl-change');
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.input) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
@@ -153,10 +151,14 @@ export default class SlRadio extends LitElement {
|
||||
const radios = this.getAllRadios().filter(radio => !radio.disabled);
|
||||
const incr = ['ArrowUp', 'ArrowLeft'].includes(event.key) ? -1 : 1;
|
||||
let index = radios.indexOf(this) + incr;
|
||||
if (index < 0) index = radios.length - 1;
|
||||
if (index > radios.length - 1) index = 0;
|
||||
if (index < 0) {
|
||||
index = radios.length - 1;
|
||||
}
|
||||
if (index > radios.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
this.getAllRadios().map(radio => {
|
||||
this.getAllRadios().forEach(radio => {
|
||||
radio.checked = false;
|
||||
radio.input.tabIndex = -1;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import formControlStyles from '../../styles/form-control.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
import formControlStyles from '~/styles/form-control.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -2,15 +2,13 @@ import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './range.styles';
|
||||
|
||||
let id = 0;
|
||||
import { autoIncrement } from '~/internal/autoIncrement';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController, getLabelledBy, renderFormControl } from '~/internal/form-control';
|
||||
import { HasSlotController } from '~/internal/slot';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -38,14 +36,15 @@ export default class SlRange extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@query('.range__control') input: HTMLInputElement;
|
||||
@query('.range__tooltip') output: HTMLOutputElement;
|
||||
@query('.range__tooltip') output: HTMLOutputElement | null;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `input-${++id}`;
|
||||
private helpTextId = `input-help-text-${id}`;
|
||||
private labelId = `input-label-${id}`;
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly attrId = autoIncrement();
|
||||
private readonly inputId = `input-${this.attrId}`;
|
||||
private readonly helpTextId = `input-help-text-${this.attrId}`;
|
||||
private readonly labelId = `input-label-${this.attrId}`;
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@@ -89,13 +88,21 @@ export default class SlRange extends LitElement {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.resizeObserver = new ResizeObserver(() => this.syncRange());
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.syncRange();
|
||||
});
|
||||
|
||||
if (this.value === undefined || this.value === null) this.value = this.min;
|
||||
if (this.value < this.min) this.value = this.min;
|
||||
if (this.value > this.max) this.value = this.max;
|
||||
if (typeof this.value === 'undefined') {
|
||||
this.value = this.min;
|
||||
}
|
||||
if (this.value < this.min) {
|
||||
this.value = this.min;
|
||||
}
|
||||
if (this.value > this.max) {
|
||||
this.value = this.max;
|
||||
}
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
void this.updateComplete.then(() => {
|
||||
this.syncRange();
|
||||
this.resizeObserver.observe(this.input);
|
||||
});
|
||||
@@ -123,7 +130,7 @@ export default class SlRange extends LitElement {
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
this.value = Number(this.input.value);
|
||||
this.value = parseFloat(this.input.value);
|
||||
emit(this, 'sl-change');
|
||||
|
||||
this.syncRange();
|
||||
@@ -137,22 +144,16 @@ export default class SlRange extends LitElement {
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.value = Number(this.value);
|
||||
|
||||
if (this.input) {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.invalid = !this.input.checkValidity();
|
||||
|
||||
this.syncRange();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.input) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
@@ -186,7 +187,7 @@ export default class SlRange extends LitElement {
|
||||
}
|
||||
|
||||
syncTooltip(percent: number) {
|
||||
if (this.output) {
|
||||
if (this.output !== null) {
|
||||
const inputWidth = this.input.offsetWidth;
|
||||
const tooltipWidth = this.output.offsetWidth;
|
||||
const thumbSize = getComputedStyle(this.input).getPropertyValue('--thumb-size');
|
||||
@@ -238,7 +239,7 @@ export default class SlRange extends LitElement {
|
||||
min=${ifDefined(this.min)}
|
||||
max=${ifDefined(this.max)}
|
||||
step=${ifDefined(this.step)}
|
||||
.value=${live(String(this.value))}
|
||||
.value=${live(this.value.toString())}
|
||||
aria-labelledby=${ifDefined(
|
||||
getLabelledBy({
|
||||
label: this.label,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import { focusVisibleSelector } from '../../internal/focus-visible';
|
||||
import { focusVisibleSelector } from '~/internal/focus-visible';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -3,12 +3,11 @@ import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { clamp } from '../../internal/math';
|
||||
import styles from './rating.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
import '~/components/icon/icon';
|
||||
import { emit } from '~/internal/event';
|
||||
import { clamp } from '~/internal/math';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -50,9 +49,7 @@ export default class SlRating extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The name of the icon to display as the symbol. */
|
||||
// @ts-ignore
|
||||
@property() getSymbol: (value: number) => string = (value: number) =>
|
||||
'<sl-icon name="star-fill" library="system"></sl-icon>';
|
||||
@property() getSymbol: (value: number) => string = () => '<sl-icon name="star-fill" library="system"></sl-icon>';
|
||||
|
||||
/** Sets focus on the rating. */
|
||||
focus(options?: FocusOptions) {
|
||||
@@ -185,7 +182,7 @@ export default class SlRating extends LitElement {
|
||||
})}
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
aria-readonly=${this.readonly ? 'true' : 'false'}
|
||||
aria-value=${this.value}
|
||||
aria-valuenow=${this.value}
|
||||
aria-valuemin=${0}
|
||||
aria-valuemax=${this.max}
|
||||
tabindex=${this.disabled ? '-1' : '0'}
|
||||
@@ -206,7 +203,9 @@ export default class SlRating extends LitElement {
|
||||
return html`
|
||||
<span
|
||||
class=${classMap({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
rating__symbol: true,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
|
||||
})}
|
||||
role="presentation"
|
||||
@@ -223,7 +222,9 @@ export default class SlRating extends LitElement {
|
||||
return html`
|
||||
<span
|
||||
class=${classMap({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
rating__symbol: true,
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
|
||||
})}
|
||||
style=${styleMap({
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { LocalizeController } from '~/utilities/localize';
|
||||
|
||||
interface UnitConfig {
|
||||
max: number;
|
||||
value: number;
|
||||
unit: Intl.RelativeTimeFormatUnit;
|
||||
}
|
||||
|
||||
const availableUnits: UnitConfig[] = [
|
||||
{ max: 2760000, value: 60000, unit: 'minute' }, // max 46 minutes
|
||||
{ max: 72000000, value: 3600000, unit: 'hour' }, // max 20 hours
|
||||
{ max: 518400000, value: 86400000, unit: 'day' }, // max 6 days
|
||||
{ max: 2419200000, value: 604800000, unit: 'week' }, // max 28 days
|
||||
{ max: 28512000000, value: 2592000000, unit: 'month' }, // max 11 months
|
||||
{ max: Infinity, value: 31536000000, unit: 'year' }
|
||||
];
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -8,8 +23,8 @@ import { LocalizeController } from '../../utilities/localize';
|
||||
*/
|
||||
@customElement('sl-relative-time')
|
||||
export default class SlRelativeTime extends LitElement {
|
||||
private localize = new LocalizeController(this);
|
||||
private updateTimeout: any;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private updateTimeout: NodeJS.Timeout;
|
||||
|
||||
@state() private isoTime = '';
|
||||
@state() private relativeTime = '';
|
||||
@@ -49,16 +64,8 @@ export default class SlRelativeTime extends LitElement {
|
||||
return '';
|
||||
}
|
||||
|
||||
const diff = +then - +now;
|
||||
const availableUnits = [
|
||||
{ max: 2760000, value: 60000, unit: 'minute' }, // max 46 minutes
|
||||
{ max: 72000000, value: 3600000, unit: 'hour' }, // max 20 hours
|
||||
{ max: 518400000, value: 86400000, unit: 'day' }, // max 6 days
|
||||
{ max: 2419200000, value: 604800000, unit: 'week' }, // max 28 days
|
||||
{ max: 28512000000, value: 2592000000, unit: 'month' }, // max 11 months
|
||||
{ max: Infinity, value: 31536000000, unit: 'year' }
|
||||
];
|
||||
const { unit, value } = availableUnits.find(unit => Math.abs(diff) < unit.max) as any;
|
||||
const diff = then.getTime() - now.getTime();
|
||||
const { unit, value } = availableUnits.find(singleUnit => Math.abs(diff) < singleUnit.max)!;
|
||||
|
||||
this.isoTime = then.toISOString();
|
||||
this.titleTime = this.localize.date(then, {
|
||||
@@ -79,13 +86,6 @@ export default class SlRelativeTime extends LitElement {
|
||||
clearTimeout(this.updateTimeout);
|
||||
|
||||
if (this.sync) {
|
||||
// Calculates the number of milliseconds until the next respective unit changes. This ensures that all components
|
||||
// update at the same time which is less distracting than updating independently.
|
||||
const getTimeUntilNextUnit = (unit: 'second' | 'minute' | 'hour' | 'day') => {
|
||||
const units = { second: 1000, minute: 60000, hour: 3600000, day: 86400000 };
|
||||
const value = units[unit];
|
||||
return value - (now.getTime() % value);
|
||||
};
|
||||
let nextInterval: number;
|
||||
|
||||
// NOTE: this could be optimized to determine when the next update should actually occur, but the size and cost of
|
||||
@@ -102,13 +102,23 @@ export default class SlRelativeTime extends LitElement {
|
||||
nextInterval = getTimeUntilNextUnit('day'); // next day
|
||||
}
|
||||
|
||||
this.updateTimeout = setTimeout(() => this.requestUpdate(), nextInterval);
|
||||
this.updateTimeout = setTimeout(() => {
|
||||
this.requestUpdate();
|
||||
}, nextInterval);
|
||||
}
|
||||
|
||||
return html` <time datetime=${this.isoTime} title=${this.titleTime}>${this.relativeTime}</time> `;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the number of milliseconds until the next respective unit changes. This ensures that all components
|
||||
// update at the same time which is less distracting than updating independently.
|
||||
function getTimeUntilNextUnit(unit: 'second' | 'minute' | 'hour' | 'day') {
|
||||
const units = { second: 1000, minute: 60000, hour: 3600000, day: 86400000 };
|
||||
const value = units[unit];
|
||||
return value - (Date.now() % value);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-relative-time': SlRelativeTime;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './resize-observer.styles';
|
||||
import { emit } from '~/internal/event';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -45,17 +45,19 @@ export default class SlResizeObserver extends LitElement {
|
||||
}
|
||||
|
||||
startObserver() {
|
||||
const slot = this.shadowRoot!.querySelector('slot')!;
|
||||
const slot = this.shadowRoot!.querySelector('slot');
|
||||
|
||||
if (slot) {
|
||||
if (slot !== null) {
|
||||
const elements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
|
||||
// Unwatch previous elements
|
||||
this.observedElements.map(el => this.resizeObserver.unobserve(el));
|
||||
this.observedElements.forEach(el => {
|
||||
this.resizeObserver.unobserve(el);
|
||||
});
|
||||
this.observedElements = [];
|
||||
|
||||
// Watch new elements
|
||||
elements.map(el => {
|
||||
elements.forEach(el => {
|
||||
this.resizeObserver.observe(el);
|
||||
this.observedElements.push(el);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default class SlResponsiveMedia extends LitElement {
|
||||
const split = this.aspectRatio.split(':');
|
||||
const x = parseFloat(split[0]);
|
||||
const y = parseFloat(split[1]);
|
||||
const paddingBottom = x && y ? `${(y / x) * 100}%` : '0';
|
||||
const paddingBottom = !isNaN(x) && !isNaN(y) && x > 0 && y > 0 ? `${(y / x) * 100}%` : '0';
|
||||
|
||||
return html`
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import formControlStyles from '../../styles/form-control.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
import formControlStyles from '~/styles/form-control.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { expect, fixture, html, waitUntil, aTimeout } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlSelect from './select';
|
||||
|
||||
describe('<sl-select>', () => {
|
||||
@@ -23,14 +21,14 @@ describe('<sl-select>', () => {
|
||||
});
|
||||
|
||||
it('should open the menu when any letter key is pressed with sl-select is on focus', async () => {
|
||||
const el = (await fixture(html`
|
||||
const el = await fixture(html`
|
||||
<sl-select>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`)) as SlSelect;
|
||||
const control = el.shadowRoot.querySelector('.select__control') as HTMLSelectElement;
|
||||
`);
|
||||
const control = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__control')!;
|
||||
control.focus();
|
||||
const rKeyEvent = new KeyboardEvent('keydown', { key: 'r' });
|
||||
control.dispatchEvent(rKeyEvent);
|
||||
@@ -39,14 +37,14 @@ describe('<sl-select>', () => {
|
||||
});
|
||||
|
||||
it('should not open the menu when ctrl + R is pressed with sl-select is on focus', async () => {
|
||||
const el = (await fixture(html`
|
||||
const el = await fixture(html`
|
||||
<sl-select>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
`)) as SlSelect;
|
||||
const control = el.shadowRoot.querySelector('.select__control') as HTMLSelectElement;
|
||||
`);
|
||||
const control = el.shadowRoot!.querySelector<HTMLSelectElement>('.select__control')!;
|
||||
control.focus();
|
||||
const rKeyEvent = new KeyboardEvent('keydown', { key: 'r', ctrlKey: true });
|
||||
control.dispatchEvent(rKeyEvent);
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { LitElement, TemplateResult, html } from 'lit';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { FormSubmitController } from '../../internal/form-control';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from './select.styles';
|
||||
|
||||
import '../dropdown/dropdown';
|
||||
import '../icon/icon';
|
||||
import '../icon-button/icon-button';
|
||||
import '../menu/menu';
|
||||
import '../tag/tag';
|
||||
|
||||
let id = 0;
|
||||
import type SlDropdown from '~/components/dropdown/dropdown';
|
||||
import '~/components/dropdown/dropdown';
|
||||
import type SlIconButton from '~/components/icon-button/icon-button';
|
||||
import '~/components/icon-button/icon-button';
|
||||
import '~/components/icon/icon';
|
||||
import type SlMenuItem from '~/components/menu-item/menu-item';
|
||||
import type SlMenu from '~/components/menu/menu';
|
||||
import type { MenuSelectEventDetail } from '~/components/menu/menu';
|
||||
import '~/components/tag/tag';
|
||||
import { autoIncrement } from '~/internal/autoIncrement';
|
||||
import { emit } from '~/internal/event';
|
||||
import { FormSubmitController, getLabelledBy, renderFormControl } from '~/internal/form-control';
|
||||
import { getTextContent, HasSlotController } from '~/internal/slot';
|
||||
import { watch } from '~/internal/watch';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
@@ -66,12 +63,14 @@ export default class SlSelect extends LitElement {
|
||||
@query('.select__hidden-select') input: HTMLInputElement;
|
||||
@query('.select__menu') menu: SlMenu;
|
||||
|
||||
// @ts-ignore
|
||||
private formSubmitController = new FormSubmitController(this);
|
||||
private hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private inputId = `select-${++id}`;
|
||||
private helpTextId = `select-help-text-${id}`;
|
||||
private labelId = `select-label-${id}`;
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly attrId = autoIncrement();
|
||||
private readonly inputId = `select-${this.attrId}`;
|
||||
private readonly helpTextId = `select-help-text-${this.attrId}`;
|
||||
private readonly labelId = `select-label-${this.attrId}`;
|
||||
private readonly menuId = `select-menu-${this.attrId}`;
|
||||
private resizeObserver: ResizeObserver;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@@ -107,7 +106,7 @@ export default class SlSelect extends LitElement {
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
/** The value of the control. This will be a string or an array depending on `multiple`. */
|
||||
@property() value: string | Array<string> = '';
|
||||
@property() value: string | string[] = '';
|
||||
|
||||
/** Draws a filled select. */
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
@@ -116,10 +115,10 @@ export default class SlSelect extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
/** The select's label. Alternatively, you can use the label slot. */
|
||||
@property() label: string;
|
||||
@property() label = '';
|
||||
|
||||
/** The select's help text. Alternatively, you can use the help-text slot. */
|
||||
@property({ attribute: 'help-text' }) helpText: string;
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
|
||||
/** The select's required attribute. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
@@ -133,9 +132,11 @@ export default class SlSelect extends LitElement {
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleMenuSlotChange = this.handleMenuSlotChange.bind(this);
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeMenu());
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.resizeMenu();
|
||||
});
|
||||
|
||||
this.updateComplete.then(() => {
|
||||
void this.updateComplete.then(() => {
|
||||
this.resizeObserver.observe(this);
|
||||
this.syncItemsFromValue();
|
||||
});
|
||||
@@ -162,7 +163,7 @@ export default class SlSelect extends LitElement {
|
||||
}
|
||||
|
||||
getItemLabel(item: SlMenuItem) {
|
||||
const slot = item.shadowRoot!.querySelector('slot:not([name])') as HTMLSlotElement;
|
||||
const slot = item.shadowRoot?.querySelector<HTMLSlotElement>('slot:not([name])');
|
||||
return getTextContent(slot);
|
||||
}
|
||||
|
||||
@@ -204,17 +205,15 @@ export default class SlSelect extends LitElement {
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
if (this.disabled && this.isOpen) {
|
||||
this.dropdown.hide();
|
||||
void this.dropdown.hide();
|
||||
}
|
||||
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.input) {
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
@@ -238,7 +237,7 @@ export default class SlSelect extends LitElement {
|
||||
// Tabbing out of the control closes it
|
||||
if (event.key === 'Tab') {
|
||||
if (this.isOpen) {
|
||||
this.dropdown.hide();
|
||||
void this.dropdown.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -249,17 +248,17 @@ export default class SlSelect extends LitElement {
|
||||
|
||||
// Show the menu if it's not already open
|
||||
if (!this.isOpen) {
|
||||
this.dropdown.show();
|
||||
void this.dropdown.show();
|
||||
}
|
||||
|
||||
// Focus on a menu item
|
||||
if (event.key === 'ArrowDown' && firstItem) {
|
||||
if (event.key === 'ArrowDown' && typeof firstItem !== 'undefined') {
|
||||
this.menu.setCurrentItem(firstItem);
|
||||
firstItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && lastItem) {
|
||||
if (event.key === 'ArrowUp' && typeof lastItem !== 'undefined') {
|
||||
this.menu.setCurrentItem(lastItem);
|
||||
lastItem.focus();
|
||||
return;
|
||||
@@ -275,7 +274,7 @@ export default class SlSelect extends LitElement {
|
||||
if (!this.isOpen && event.key.length === 1) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.dropdown.show();
|
||||
void this.dropdown.show();
|
||||
this.menu.typeToSelect(event.key);
|
||||
}
|
||||
}
|
||||
@@ -284,11 +283,11 @@ export default class SlSelect extends LitElement {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
handleMenuSelect(event: CustomEvent) {
|
||||
handleMenuSelect(event: CustomEvent<MenuSelectEventDetail>) {
|
||||
const item = event.detail.item;
|
||||
|
||||
if (this.multiple) {
|
||||
this.value = this.value?.includes(item.value)
|
||||
this.value = this.value.includes(item.value)
|
||||
? (this.value as []).filter(v => v !== item.value)
|
||||
: [...this.value, item.value];
|
||||
} else {
|
||||
@@ -314,7 +313,7 @@ export default class SlSelect extends LitElement {
|
||||
handleMultipleChange() {
|
||||
// Cast to array | string based on `this.multiple`
|
||||
const value = this.getValueAsArray();
|
||||
this.value = this.multiple ? value : value[0] || '';
|
||||
this.value = this.multiple ? value : value[0] ?? '';
|
||||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
@@ -324,7 +323,7 @@ export default class SlSelect extends LitElement {
|
||||
|
||||
// Check for duplicate values in menu items
|
||||
const values: string[] = [];
|
||||
items.map(item => {
|
||||
items.forEach(item => {
|
||||
if (values.includes(item.value)) {
|
||||
console.error(`Duplicate value found in <sl-select> menu item: '${item.value}'`, item);
|
||||
}
|
||||
@@ -332,12 +331,14 @@ export default class SlSelect extends LitElement {
|
||||
values.push(item.value);
|
||||
});
|
||||
|
||||
await Promise.all(items.map(item => item.render)).then(() => this.syncItemsFromValue());
|
||||
await Promise.all(items.map(item => item.render)).then(() => {
|
||||
this.syncItemsFromValue();
|
||||
});
|
||||
}
|
||||
|
||||
handleTagInteraction(event: KeyboardEvent | MouseEvent) {
|
||||
// Don't toggle the menu when a tag's clear button is activated
|
||||
const path = event.composedPath() as Array<EventTarget>;
|
||||
const path = event.composedPath();
|
||||
const clearButton = path.find((el: SlIconButton) => {
|
||||
if (el instanceof HTMLElement) {
|
||||
const element = el as HTMLElement;
|
||||
@@ -346,7 +347,7 @@ export default class SlSelect extends LitElement {
|
||||
return false;
|
||||
});
|
||||
|
||||
if (clearButton) {
|
||||
if (typeof clearButton !== 'undefined') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
@@ -360,12 +361,9 @@ export default class SlSelect extends LitElement {
|
||||
}
|
||||
|
||||
resizeMenu() {
|
||||
const box = this.shadowRoot?.querySelector('.select__control') as HTMLElement;
|
||||
this.menu.style.width = `${box.clientWidth}px`;
|
||||
this.menu.style.width = `${this.control.clientWidth}px`;
|
||||
|
||||
if (this.dropdown) {
|
||||
this.dropdown.reposition();
|
||||
}
|
||||
this.dropdown.reposition();
|
||||
}
|
||||
|
||||
syncItemsFromValue() {
|
||||
@@ -377,9 +375,9 @@ export default class SlSelect extends LitElement {
|
||||
|
||||
// Sync display label and tags
|
||||
if (this.multiple) {
|
||||
const checkedItems = items.filter(item => value.includes(item.value)) as SlMenuItem[];
|
||||
const checkedItems = items.filter(item => value.includes(item.value));
|
||||
|
||||
this.displayLabel = checkedItems[0] ? this.getItemLabel(checkedItems[0]) : '';
|
||||
this.displayLabel = checkedItems.length > 0 ? this.getItemLabel(checkedItems[0]) : '';
|
||||
this.displayTags = checkedItems.map((item: SlMenuItem) => {
|
||||
return html`
|
||||
<sl-tag
|
||||
@@ -412,9 +410,9 @@ export default class SlSelect extends LitElement {
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
const checkedItem = items.filter(item => item.value === value[0])[0];
|
||||
const checkedItem = items.find(item => item.value === value[0]);
|
||||
|
||||
this.displayLabel = checkedItem ? this.getItemLabel(checkedItem) : '';
|
||||
this.displayLabel = typeof checkedItem !== 'undefined' ? this.getItemLabel(checkedItem) : '';
|
||||
this.displayTags = [];
|
||||
}
|
||||
}
|
||||
@@ -434,7 +432,7 @@ export default class SlSelect extends LitElement {
|
||||
render() {
|
||||
const hasLabelSlot = this.hasSlotController.test('label');
|
||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||
const hasSelection = this.multiple ? this.value?.length > 0 : this.value !== '';
|
||||
const hasSelection = this.multiple ? this.value.length > 0 : this.value !== '';
|
||||
|
||||
return renderFormControl(
|
||||
{
|
||||
@@ -446,7 +444,9 @@ export default class SlSelect extends LitElement {
|
||||
helpText: this.helpText,
|
||||
hasHelpTextSlot,
|
||||
size: this.size,
|
||||
onLabelClick: () => this.handleLabelClick()
|
||||
onLabelClick: () => {
|
||||
this.handleLabelClick();
|
||||
}
|
||||
},
|
||||
html`
|
||||
<sl-dropdown
|
||||
@@ -458,7 +458,7 @@ export default class SlSelect extends LitElement {
|
||||
class=${classMap({
|
||||
select: true,
|
||||
'select--open': this.isOpen,
|
||||
'select--empty': this.value?.length === 0,
|
||||
'select--empty': this.value.length === 0,
|
||||
'select--focused': this.hasFocus,
|
||||
'select--clearable': this.clearable,
|
||||
'select--disabled': this.disabled,
|
||||
@@ -494,6 +494,7 @@ export default class SlSelect extends LitElement {
|
||||
)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded=${this.isOpen ? 'true' : 'false'}
|
||||
aria-controls=${this.menuId}
|
||||
tabindex=${this.disabled ? '-1' : '0'}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
@@ -504,9 +505,11 @@ export default class SlSelect extends LitElement {
|
||||
</span>
|
||||
|
||||
<div class="select__label">
|
||||
${this.displayTags.length
|
||||
${this.displayTags.length > 0
|
||||
? html` <span part="tags" class="select__tags"> ${this.displayTags} </span> `
|
||||
: this.displayLabel || this.placeholder}
|
||||
: this.displayLabel.length > 0
|
||||
? this.displayLabel
|
||||
: this.placeholder}
|
||||
</div>
|
||||
|
||||
${this.clearable && hasSelection
|
||||
@@ -544,7 +547,7 @@ export default class SlSelect extends LitElement {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<sl-menu part="menu" class="select__menu" @sl-select=${this.handleMenuSelect}>
|
||||
<sl-menu part="menu" class="select__menu" @sl-select=${this.handleMenuSelect} id=${this.menuId}>
|
||||
<slot @slotchange=${this.handleMenuSlotChange}></slot>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
import componentStyles from '~/styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user