Compare commits

..

2 Commits

Author SHA1 Message Date
Lea Verou
9328feed19 [image-comparer] Remove base part
Had to move the keydown listener to the divider, but that seems like a good change *anyway* since if the content is interactive (such as our own theme previews) we don't really want to be listening to key presses on it, we only really want to be listening to key presses on the divider, which is the actual focusable part.
2025-04-23 12:44:33 -04:00
Lea Verou
2542354d5c [breadcrumb-item] Drop base part, move styling to host 2025-04-23 12:33:51 -04:00
15 changed files with 189 additions and 175 deletions

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,16 +1,14 @@
---
title: Comparer
description: Compare visual differences between similar content with a sliding panel.
title: Image Comparer
description: Compare visual differences between similar photos with a sliding panel.
tags: [imagery, niche]
icon: comparer
icon: image-comparer
---
This is especially useful for comparing images, but can be used for comparing any type of content (for an example of using it to compare entire UIs, check out our [theme pages](/docs/themes/default/)).
For best results, use content that shares the same dimensions.
The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.)
For best results, use images that share the same dimensions. The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.)
```html {.example}
<wa-comparer>
<wa-image-comparer>
<img
slot="before"
src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80&sat=-100&bri=-5"
@@ -21,7 +19,7 @@ The slider can be controlled by dragging or pressing the left and right arrow ke
src="https://images.unsplash.com/photo-1517331156700-3c241d2b4d83?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=80"
alt="Color version of kittens in a basket looking around."
/>
</wa-comparer>
</wa-image-comparer>
```
## Examples
@@ -31,7 +29,7 @@ The slider can be controlled by dragging or pressing the left and right arrow ke
Use the `position` attribute to set the initial position of the slider. This is a percentage from `0` to `100`.
```html {.example}
<wa-comparer position="25">
<wa-image-comparer position="25">
<img
slot="before"
src="https://images.unsplash.com/photo-1520903074185-8eca362b3dce?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1200&q=80"
@@ -42,5 +40,5 @@ Use the `position` attribute to set the initial position of the slider. This is
src="https://images.unsplash.com/photo-1520640023173-50a135e35804?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=80"
alt="A person sitting on a yellow curb tying shoelaces on a boot."
/>
</wa-comparer>
</wa-image-comparer>
```

View File

@@ -129,7 +129,7 @@ Set a matching width and height to make a circle, square, or rounded avatar skel
<style>
.skeleton-avatars wa-skeleton {
display: inline-flex;
display: inline-block;
width: 3rem;
height: 3rem;
margin-right: 0.5rem;

View File

@@ -12,10 +12,6 @@ Components with the <wa-badge variant="warning" pill>Experimental</wa-badge> bad
During the alpha period, things might break! We take breaking changes very seriously, but sometimes they're necessary to make the final product that much better. We appreciate your patience!
:::
## Next
- 🚨 BREAKING: Renamed `<image-comparer>` to `<wa-comparer>` and improved compatibility for non-image content.
## 3.0.0-alpha.12
### Enhancements

View File

@@ -26,14 +26,14 @@ eleventyComputed:
{% include 'theme-showcase.njk' %}
{% endset %}
<wa-comparer style="width: 100%" position="90">
<wa-image-comparer style="width: 100%" position="90">
<div slot="after" class="theme-showcase wa-gap-xl">
{{ content | safe }}
</div>
<div slot="before" class="theme-showcase wa-gap-xl wa-invert">
{{ content | safe }}
</div>
</wa-comparer>
</wa-image-comparer>
<script type="module">
import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";

View File

@@ -1,14 +1,6 @@
:host {
color: var(--wa-color-text-link);
display: inline-flex;
}
:host(:last-of-type) {
color: var(--wa-color-text-quiet);
}
.breadcrumb-item {
display: inline-flex;
align-items: center;
font: inherit;
font-weight: var(--wa-font-weight-action);
@@ -16,6 +8,10 @@
white-space: nowrap;
}
:host(:last-of-type) {
color: var(--wa-color-text-quiet);
}
.label {
display: inline-block;
font: inherit;

View File

@@ -17,7 +17,6 @@ import styles from './breadcrumb-item.css';
* @slot separator - The separator to use for the breadcrumb item. This will only change the separator for this item. If
* you want to change it for all items in the group, set the separator on `<wa-breadcrumb>` instead.
*
* @csspart base - The component's base wrapper.
* @csspart label - The breadcrumb item's label.
* @csspart prefix - The container that wraps the prefix.
* @csspart suffix - The container that wraps the suffix.
@@ -72,47 +71,45 @@ export default class WaBreadcrumbItem extends WebAwesomeElement {
render() {
return html`
<div part="base" class="breadcrumb-item">
<span part="prefix" class="prefix">
<slot name="prefix"></slot>
</span>
<span part="prefix" class="prefix">
<slot name="prefix"></slot>
</span>
${this.renderType === 'link'
? html`
<a
part="label"
class="label label--link"
href="${this.href!}"
target="${ifDefined(this.target ? this.target : undefined)}"
rel=${ifDefined(this.target ? this.rel : undefined)}
>
<slot></slot>
</a>
`
: ''}
${this.renderType === 'button'
? html`
<button part="label" type="button" class="label label--button">
<slot @slotchange=${this.handleSlotChange}></slot>
</button>
`
: ''}
${this.renderType === 'dropdown'
? html`
<div part="label" class="label label--dropdown">
<slot @slotchange=${this.handleSlotChange}></slot>
</div>
`
: ''}
${this.renderType === 'link'
? html`
<a
part="label"
class="label label--link"
href="${this.href!}"
target="${ifDefined(this.target ? this.target : undefined)}"
rel=${ifDefined(this.target ? this.rel : undefined)}
>
<slot></slot>
</a>
`
: ''}
${this.renderType === 'button'
? html`
<button part="label" type="button" class="label label--button">
<slot @slotchange=${this.handleSlotChange}></slot>
</button>
`
: ''}
${this.renderType === 'dropdown'
? html`
<div part="label" class="label label--dropdown">
<slot @slotchange=${this.handleSlotChange}></slot>
</div>
`
: ''}
<span part="suffix" class="suffix">
<slot name="suffix"></slot>
</span>
<span part="suffix" class="suffix">
<slot name="suffix"></slot>
</span>
<span part="separator" class="separator" aria-hidden="true">
<slot name="separator"></slot>
</span>
</div>
<span part="separator" class="separator" aria-hidden="true">
<slot name="separator"></slot>
</span>
`;
}
}

View File

@@ -21,10 +21,6 @@
max-width: 100% !important;
height: auto;
}
&::slotted(:not(img, svg)) {
isolation: isolate;
}
}
.after {

View File

@@ -1,19 +1,18 @@
import { expect } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import { html } from 'lit';
import sinon from 'sinon';
import { fixtures } from '../../internal/test/fixture.js';
import type WaComparer from './comparer.js';
import type WaImageComparer from './image-comparer.js';
describe('<wa-comparer>', () => {
describe('<wa-image-comparer>', () => {
for (const fixture of fixtures) {
describe(`with "${fixture.type}" rendering`, () => {
it('should render a basic before/after', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const afterPart = el.shadowRoot!.querySelector<HTMLElement>('[part~="after"]')!;
@@ -30,11 +29,11 @@ describe('<wa-comparer>', () => {
});
it('should emit change event when position changed manually', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handler = sinon.spy();
@@ -47,166 +46,194 @@ describe('<wa-comparer>', () => {
});
it('should increment position on arrow right', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
handle.focus();
await sendKeys({ press: 'ArrowRight' });
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowRight',
}),
);
await el.updateComplete;
expect(el.position).to.equal(51);
});
it('should decrement position on arrow left', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'ArrowLeft' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowLeft',
}),
);
await el.updateComplete;
expect(el.position).to.equal(49);
});
it('should set position to 0 on home key', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'Home' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Home',
}),
);
await el.updateComplete;
expect(el.position).to.equal(0);
});
it('should set position to 100 on end key', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'End' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'End',
}),
);
await el.updateComplete;
expect(el.position).to.equal(100);
});
it('should clamp to 100 on arrow right', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
el.position = 0;
await el.updateComplete;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'ArrowLeft' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowLeft',
}),
);
await el.updateComplete;
expect(el.position).to.equal(0);
});
it('should clamp to 0 on arrow left', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
el.position = 100;
await el.updateComplete;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'ArrowRight' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowRight',
}),
);
await el.updateComplete;
expect(el.position).to.equal(100);
});
it('should increment position by 10 on arrow right + shift', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'Shift+ArrowRight' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowRight',
shiftKey: true,
}),
);
await el.updateComplete;
expect(el.position).to.equal(60);
});
it('should decrement position by 10 on arrow left + shift', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
handle.focus();
await sendKeys({ press: 'Shift+ArrowLeft' });
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowLeft',
shiftKey: true,
}),
);
await el.updateComplete;
expect(el.position).to.equal(40);
});
it('should set position by attribute', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer position="10">
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer position="10">
<div slot="before"></div>
<div slot="after"></div>
</wa-comparer>
</wa-image-comparer>
`);
expect(el.position).to.equal(10);
});
it('should move position on drag', async () => {
const el = await fixture<WaComparer>(html`
<wa-comparer>
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
<div slot="before" style="width: 50px"></div>
<div slot="after" style="width: 50px"></div>
</wa-comparer>
</wa-image-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const rect = handle.getBoundingClientRect();
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const rect = base.getBoundingClientRect();
const offsetX = rect.left + window.pageXOffset;
const offsetY = rect.top + window.pageYOffset;
@@ -214,7 +241,7 @@ describe('<wa-comparer>', () => {
document.dispatchEvent(
new PointerEvent('pointermove', {
clientX: offsetX + 15,
clientX: offsetX + 20,
clientY: offsetY,
}),
);

View File

@@ -7,34 +7,34 @@ import { watch } from '../../internal/watch.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import { LocalizeController } from '../../utilities/localize.js';
import '../icon/icon.js';
import styles from './comparer.css';
import styles from './image-comparer.css';
/**
* @summary Compare visual differences between similar content with a sliding panel.
* @documentation https://backers.webawesome.com/docs/components/comparer
* @summary Compare visual differences between similar photos with a sliding panel.
* @documentation https://backers.webawesome.com/docs/components/image-comparer
* @status stable
* @since 2.0
*
* @dependency wa-icon
*
* @slot before - The before content, often an `<img>` or `<svg>` element.
* @slot after - The after content, often an `<img>` or `<svg>` element.
* @slot before - The before image, an `<img>` or `<svg>` element.
* @slot after - The after image, an `<img>` or `<svg>` element.
* @slot handle - The icon used inside the handle.
*
* @event change - Emitted when the position changes.
*
* @csspart before - The container that wraps the before content.
* @csspart after - The container that wraps the after content.
* @csspart divider - The divider that separates the before and after content.
* @csspart handle - The handle that the user drags to expose the after content.
* @csspart before - The container that wraps the before image.
* @csspart after - The container that wraps the after image.
* @csspart divider - The divider that separates the images.
* @csspart handle - The handle that the user drags to expose the after image.
*
* @cssproperty --divider-color - The color of the divider.
* @cssproperty --divider-width - The width of the dividing line.
* @cssproperty --handle-color - The color of the icon used inside the handle.
* @cssproperty --handle-size - The size of the compare handle.
*/
@customElement('wa-comparer')
export default class WaComparer extends WebAwesomeElement {
@customElement('wa-image-comparer')
export default class WaImageComparer extends WebAwesomeElement {
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
@@ -129,7 +129,7 @@ export default class WaComparer extends WebAwesomeElement {
aria-valuenow=${this.position}
aria-valuemin="0"
aria-valuemax="100"
aria-controls="comparer"
aria-controls="image-comparer"
tabindex="0"
>
<slot name="handle">
@@ -143,6 +143,6 @@ export default class WaComparer extends WebAwesomeElement {
declare global {
interface HTMLElementTagNameMap {
'wa-comparer': WaComparer;
'wa-image-comparer': WaImageComparer;
}
}

View File

@@ -173,12 +173,11 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
this._value = val;
let newValue = this.value;
if (newValue !== oldValue) {
if (newValue != oldValue) {
this.requestUpdate('value', oldValue);
}
}
}
get value() {
let value = this._value ?? this.defaultValue;
value = Array.isArray(value) ? value : [value];
@@ -679,7 +678,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Toggle values present in the DOM from this.value, while preserving options NOT present in the DOM (for lazy loading)
// Note that options NOT present in the DOM will be moved to the end after this
if (selectedValues.size > 0 || this._value) {
const oldValue = this._value;
if (!this._value) {
// First time it's set
let value = this.defaultValue ?? [];
@@ -689,7 +687,6 @@ export default class WaSelect extends WebAwesomeFormAssociatedElement {
// Filter out values that are in the DOM
this._value = this._value.filter(value => !this.optionValues?.has(value));
this._value.unshift(...selectedValues);
this.requestUpdate('value', oldValue);
}
// Update the value and display label

View File

@@ -1,10 +1,14 @@
:host {
--border-radius: var(--wa-border-radius-pill);
--color: var(--wa-color-neutral-fill-normal);
--sheen-color: color-mix(in oklab, var(--color), var(--wa-color-surface-raised));
--sheen-color: color-mix(in oklab, var(--wa-color-neutral-fill-normal), var(--wa-color-surface-raised));
display: flex;
display: block;
position: relative;
}
.skeleton {
display: flex;
width: 100%;
height: 100%;
min-height: 1rem;
@@ -16,13 +20,13 @@
border-radius: var(--border-radius);
}
:host([effect='sheen']) .indicator {
.skeleton--sheen .indicator {
background: linear-gradient(270deg, var(--sheen-color), var(--color), var(--color), var(--sheen-color));
background-size: 400% 100%;
animation: sheen 8s ease-in-out infinite;
}
:host([effect='pulse']) .indicator {
.skeleton--pulse .indicator {
animation: pulse 2s ease-in-out 0.5s infinite;
}

View File

@@ -11,37 +11,27 @@ describe('<wa-skeleton>', () => {
await expect(el).to.be.accessible();
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const indicator = el.shadowRoot!.querySelector<HTMLElement>('[part~="indicator"]')!;
expect(el.getAttribute('effect')).to.equal(null);
expect(base.getAttribute('class')).to.equal(' skeleton ');
expect(indicator.getAttribute('class')).to.equal('indicator');
});
it('should set pulse effect by attribute', async () => {
const el = await fixture<WaSkeleton>(html` <wa-skeleton effect="none"></wa-skeleton> `);
const indicator = el.shadowRoot!.querySelector<HTMLElement>('[part~="indicator"]')!;
const cs = getComputedStyle(indicator);
expect(el.getAttribute('effect')).to.equal(null);
expect(cs.animationName).to.equal('none');
});
it('should set pulse effect by attribute', async () => {
const el = await fixture<WaSkeleton>(html` <wa-skeleton effect="pulse"></wa-skeleton> `);
const indicator = el.shadowRoot!.querySelector<HTMLElement>('[part~="indicator"]')!;
const cs = getComputedStyle(indicator);
expect(el.getAttribute('effect')).to.equal('pulse');
expect(cs.animationName).to.equal('pulse');
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.getAttribute('class')).to.equal(' skeleton skeleton--pulse ');
});
it('should set sheen effect by attribute', async () => {
const el = await fixture<WaSkeleton>(html` <wa-skeleton effect="sheen"></wa-skeleton> `);
const indicator = el.shadowRoot!.querySelector<HTMLElement>('[part~="indicator"]')!;
const cs = getComputedStyle(indicator);
expect(el.getAttribute('effect')).to.equal('sheen');
expect(cs.animationName).to.equal('sheen');
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.getAttribute('class')).to.equal(' skeleton skeleton--sheen ');
});
});
}

View File

@@ -1,5 +1,6 @@
import { html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import WebAwesomeElement from '../../internal/webawesome-element.js';
import styles from './skeleton.css';
@@ -9,6 +10,7 @@ import styles from './skeleton.css';
* @status stable
* @since 2.0
*
* @csspart base - The component's base wrapper.
* @csspart indicator - The skeleton's indicator which is responsible for its color and animation.
*
* @cssproperty --border-radius - The skeleton's border radius.
@@ -20,10 +22,21 @@ export default class WaSkeleton extends WebAwesomeElement {
static shadowStyle = styles;
/** Determines which effect the skeleton will use. */
@property({ reflect: true, default: 'none' }) effect: 'pulse' | 'sheen' | 'none' = 'none';
@property() effect: 'pulse' | 'sheen' | 'none' = 'none';
render() {
return html` <div part="indicator" class="indicator"></div> `;
return html`
<div
part="base"
class=${classMap({
skeleton: true,
'skeleton--pulse': this.effect === 'pulse',
'skeleton--sheen': this.effect === 'sheen',
})}
>
<div part="indicator" class="indicator"></div>
</div>
`;
}
}

View File

@@ -44,7 +44,7 @@
}
wa-carousel::part(pagination-item),
wa-comparer::part(handle),
wa-image-comparer::part(handle),
wa-progress-bar::part(base),
wa-slider::part(base),
input[type='range'],