[Image-comparer] Several fixes + rename to comparer (#883)

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
This commit is contained in:
Lea Verou
2025-04-28 16:58:24 -04:00
committed by GitHub
parent cfc3f181a3
commit e813440315
8 changed files with 133 additions and 156 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

@@ -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

@@ -6,9 +6,6 @@
display: inline-block;
position: relative;
}
.image-comparer {
max-width: 100%;
max-height: 100%;
overflow: hidden;
@@ -24,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,52 +7,50 @@ 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 base - The component's base wrapper.
* @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);
@query('.image-comparer') base: HTMLElement;
@query('.handle') handle: HTMLElement;
/** The position of the divider as a percentage. */
@property({ type: Number, reflect: true }) position = 50;
private handleDrag(event: PointerEvent) {
const { width } = this.base.getBoundingClientRect();
const { width } = this.getBoundingClientRect();
const isRtl = this.localize.dir() === 'rtl';
event.preventDefault();
drag(this.base, {
drag(this, {
onMove: x => {
this.position = parseFloat(clamp((x / width) * 100, 0, 100).toFixed(2));
if (isRtl) this.position = 100 - this.position;
@@ -98,46 +96,45 @@ export default class WaImageComparer extends WebAwesomeElement {
const isRtl = this.hasUpdated ? this.localize.dir() === 'rtl' : this.dir === 'rtl';
return html`
<div part="base" id="image-comparer" class="image-comparer" @keydown=${this.handleKeyDown}>
<div class="image">
<div part="before" class="before">
<slot name="before"></slot>
</div>
<div
part="after"
class="after"
style=${styleMap({
clipPath: isRtl ? `inset(0 0 0 ${100 - this.position}%)` : `inset(0 ${100 - this.position}% 0 0)`,
})}
>
<slot name="after"></slot>
</div>
<div class="image">
<div part="before" class="before">
<slot name="before"></slot>
</div>
<div
part="divider"
class="divider"
part="after"
class="after"
style=${styleMap({
left: isRtl ? `${100 - this.position}%` : `${this.position}%`,
clipPath: isRtl ? `inset(0 0 0 ${100 - this.position}%)` : `inset(0 ${100 - this.position}% 0 0)`,
})}
@mousedown=${this.handleDrag}
@touchstart=${this.handleDrag}
>
<div
part="handle"
class="handle"
role="scrollbar"
aria-valuenow=${this.position}
aria-valuemin="0"
aria-valuemax="100"
aria-controls="image-comparer"
tabindex="0"
>
<slot name="handle">
<wa-icon library="system" name="grip-vertical" variant="solid"></wa-icon>
</slot>
</div>
<slot name="after"></slot>
</div>
</div>
<div
part="divider"
class="divider"
style=${styleMap({
left: isRtl ? `${100 - this.position}%` : `${this.position}%`,
})}
@keydown=${this.handleKeyDown}
@mousedown=${this.handleDrag}
@touchstart=${this.handleDrag}
>
<div
part="handle"
class="handle"
role="scrollbar"
aria-valuenow=${this.position}
aria-valuemin="0"
aria-valuemax="100"
aria-controls="comparer"
tabindex="0"
>
<slot name="handle">
<wa-icon library="system" name="grip-vertical" variant="solid"></wa-icon>
</slot>
</div>
</div>
`;
@@ -146,6 +143,6 @@ export default class WaImageComparer extends WebAwesomeElement {
declare global {
interface HTMLElementTagNameMap {
'wa-image-comparer': WaImageComparer;
'wa-comparer': WaComparer;
}
}

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'],