Compare commits

..

4 Commits

Author SHA1 Message Date
Konnor Rogers
a0a137e3bf prettier 2025-04-29 01:23:24 -04:00
Konnor Rogers
02802bbc03 fix select 2025-04-29 01:17:05 -04:00
Lea Verou
c571573063 [Skeleton] Remove base part, rel #207 (#885) 2025-04-28 17:04:06 -04:00
Lea Verou
e813440315 [Image-comparer] Several fixes + rename to comparer (#883)
Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2025-04-28 16:58:24 -04:00
15 changed files with 175 additions and 189 deletions

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,14 +1,16 @@
---
title: Image Comparer
description: Compare visual differences between similar photos with a sliding panel.
title: Comparer
description: Compare visual differences between similar content with a sliding panel.
tags: [imagery, niche]
icon: image-comparer
icon: comparer
---
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.)
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.)
```html {.example}
<wa-image-comparer>
<wa-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"
@@ -19,7 +21,7 @@ For best results, use images that share the same dimensions. The slider can be c
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-image-comparer>
</wa-comparer>
```
## Examples
@@ -29,7 +31,7 @@ For best results, use images that share the same dimensions. The slider can be c
Use the `position` attribute to set the initial position of the slider. This is a percentage from `0` to `100`.
```html {.example}
<wa-image-comparer position="25">
<wa-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"
@@ -40,5 +42,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-image-comparer>
</wa-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-block;
display: inline-flex;
width: 3rem;
height: 3rem;
margin-right: 0.5rem;

View File

@@ -12,6 +12,10 @@ 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-image-comparer style="width: 100%" position="90">
<wa-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-image-comparer>
</wa-comparer>
<script type="module">
import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";

View File

@@ -1,6 +1,14 @@
: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);
@@ -8,10 +16,6 @@
white-space: nowrap;
}
:host(:last-of-type) {
color: var(--wa-color-text-quiet);
}
.label {
display: inline-block;
font: inherit;

View File

@@ -17,6 +17,7 @@ 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.
@@ -71,45 +72,47 @@ export default class WaBreadcrumbItem extends WebAwesomeElement {
render() {
return html`
<span part="prefix" class="prefix">
<slot name="prefix"></slot>
</span>
<div part="base" class="breadcrumb-item">
<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>
<span part="separator" class="separator" aria-hidden="true">
<slot name="separator"></slot>
</span>
</div>
`;
}
}

View File

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

View File

@@ -1,18 +1,19 @@
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 WaImageComparer from './image-comparer.js';
import type WaComparer from './comparer.js';
describe('<wa-image-comparer>', () => {
describe('<wa-comparer>', () => {
for (const fixture of fixtures) {
describe(`with "${fixture.type}" rendering`, () => {
it('should render a basic before/after', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const afterPart = el.shadowRoot!.querySelector<HTMLElement>('[part~="after"]')!;
@@ -29,11 +30,11 @@ describe('<wa-image-comparer>', () => {
});
it('should emit change event when position changed manually', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const handler = sinon.spy();
@@ -46,194 +47,166 @@ describe('<wa-image-comparer>', () => {
});
it('should increment position on arrow right', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowRight',
}),
);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
handle.focus();
await sendKeys({ press: 'ArrowRight' });
await el.updateComplete;
expect(el.position).to.equal(51);
});
it('should decrement position on arrow left', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowLeft',
}),
);
handle.focus();
await sendKeys({ press: 'ArrowLeft' });
await el.updateComplete;
expect(el.position).to.equal(49);
});
it('should set position to 0 on home key', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'Home',
}),
);
handle.focus();
await sendKeys({ press: 'Home' });
await el.updateComplete;
expect(el.position).to.equal(0);
});
it('should set position to 100 on end key', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'End',
}),
);
handle.focus();
await sendKeys({ press: 'End' });
await el.updateComplete;
expect(el.position).to.equal(100);
});
it('should clamp to 100 on arrow right', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
el.position = 0;
await el.updateComplete;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowLeft',
}),
);
handle.focus();
await sendKeys({ press: 'ArrowLeft' });
await el.updateComplete;
expect(el.position).to.equal(0);
});
it('should clamp to 0 on arrow left', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
el.position = 100;
await el.updateComplete;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowRight',
}),
);
handle.focus();
await sendKeys({ press: '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<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowRight',
shiftKey: true,
}),
);
handle.focus();
await sendKeys({ press: 'Shift+ArrowRight' });
await el.updateComplete;
expect(el.position).to.equal(60);
});
it('should decrement position by 10 on arrow left + shift', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
base.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowLeft',
shiftKey: true,
}),
);
handle.focus();
await sendKeys({ press: 'Shift+ArrowLeft' });
await el.updateComplete;
expect(el.position).to.equal(40);
});
it('should set position by attribute', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer position="10">
const el = await fixture<WaComparer>(html`
<wa-comparer position="10">
<div slot="before"></div>
<div slot="after"></div>
</wa-image-comparer>
</wa-comparer>
`);
expect(el.position).to.equal(10);
});
it('should move position on drag', async () => {
const el = await fixture<WaImageComparer>(html`
<wa-image-comparer>
const el = await fixture<WaComparer>(html`
<wa-comparer>
<div slot="before" style="width: 50px"></div>
<div slot="after" style="width: 50px"></div>
</wa-image-comparer>
</wa-comparer>
`);
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const rect = base.getBoundingClientRect();
const rect = handle.getBoundingClientRect();
const offsetX = rect.left + window.pageXOffset;
const offsetY = rect.top + window.pageYOffset;
@@ -241,7 +214,7 @@ describe('<wa-image-comparer>', () => {
document.dispatchEvent(
new PointerEvent('pointermove', {
clientX: offsetX + 20,
clientX: offsetX + 15,
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 './image-comparer.css';
import styles from './comparer.css';
/**
* @summary Compare visual differences between similar photos with a sliding panel.
* @documentation https://backers.webawesome.com/docs/components/image-comparer
* @summary Compare visual differences between similar content with a sliding panel.
* @documentation https://backers.webawesome.com/docs/components/comparer
* @status stable
* @since 2.0
*
* @dependency wa-icon
*
* @slot before - The before image, an `<img>` or `<svg>` element.
* @slot after - The after image, an `<img>` or `<svg>` element.
* @slot before - The before content, often an `<img>` or `<svg>` element.
* @slot after - The after content, often 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 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.
* @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.
*
* @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-image-comparer')
export default class WaImageComparer extends WebAwesomeElement {
@customElement('wa-comparer')
export default class WaComparer extends WebAwesomeElement {
static shadowStyle = styles;
private readonly localize = new LocalizeController(this);
@@ -129,7 +129,7 @@ export default class WaImageComparer extends WebAwesomeElement {
aria-valuenow=${this.position}
aria-valuemin="0"
aria-valuemax="100"
aria-controls="image-comparer"
aria-controls="comparer"
tabindex="0"
>
<slot name="handle">
@@ -143,6 +143,6 @@ export default class WaImageComparer extends WebAwesomeElement {
declare global {
interface HTMLElementTagNameMap {
'wa-image-comparer': WaImageComparer;
'wa-comparer': WaComparer;
}
}

View File

@@ -173,11 +173,12 @@ 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];
@@ -678,6 +679,7 @@ 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 ?? [];
@@ -687,6 +689,7 @@ 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,14 +1,10 @@
:host {
--border-radius: var(--wa-border-radius-pill);
--color: var(--wa-color-neutral-fill-normal);
--sheen-color: color-mix(in oklab, var(--wa-color-neutral-fill-normal), var(--wa-color-surface-raised));
--sheen-color: color-mix(in oklab, var(--color), var(--wa-color-surface-raised));
display: block;
position: relative;
}
.skeleton {
display: flex;
position: relative;
width: 100%;
height: 100%;
min-height: 1rem;
@@ -20,13 +16,13 @@
border-radius: var(--border-radius);
}
.skeleton--sheen .indicator {
:host([effect='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;
}
.skeleton--pulse .indicator {
:host([effect='pulse']) .indicator {
animation: pulse 2s ease-in-out 0.5s infinite;
}

View File

@@ -11,27 +11,37 @@ 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(base.getAttribute('class')).to.equal(' skeleton ');
expect(el.getAttribute('effect')).to.equal(null);
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);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.getAttribute('class')).to.equal(' skeleton skeleton--pulse ');
expect(el.getAttribute('effect')).to.equal('pulse');
expect(cs.animationName).to.equal('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);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.getAttribute('class')).to.equal(' skeleton skeleton--sheen ');
expect(el.getAttribute('effect')).to.equal('sheen');
expect(cs.animationName).to.equal('sheen');
});
});
}

View File

@@ -1,6 +1,5 @@
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';
@@ -10,7 +9,6 @@ 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.
@@ -22,21 +20,10 @@ export default class WaSkeleton extends WebAwesomeElement {
static shadowStyle = styles;
/** Determines which effect the skeleton will use. */
@property() effect: 'pulse' | 'sheen' | 'none' = 'none';
@property({ reflect: true, default: 'none' }) effect: 'pulse' | 'sheen' | 'none' = 'none';
render() {
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>
`;
return html` <div part="indicator" class="indicator"></div> `;
}
}

View File

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