mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat: add slider preview thumbnails (#935)
This commit is contained in:
@@ -155,7 +155,11 @@ export function createSlider(options: SliderOptions): SliderApi {
|
||||
options.onValueChange?.(percent);
|
||||
|
||||
// Focus the thumb for keyboard follow-up and screen reader tracking.
|
||||
options.getThumbElement?.()?.focus();
|
||||
options.getThumbElement?.()?.focus({
|
||||
preventScroll: true,
|
||||
// @ts-expect-error -- focusVisible is not yet in TypeScript's lib.dom typings.
|
||||
focusVisible: false,
|
||||
});
|
||||
},
|
||||
|
||||
onPointerMove(event) {
|
||||
|
||||
@@ -308,6 +308,104 @@ describe('createThumbnail', () => {
|
||||
|
||||
handle.destroy();
|
||||
});
|
||||
|
||||
it('React lifecycle: updateSrc before img available, then connect after mount', () => {
|
||||
let img: HTMLImageElement | null = null;
|
||||
const onStateChange = vi.fn();
|
||||
|
||||
const handle = createThumbnail(
|
||||
createOptions({
|
||||
getImg: () => img,
|
||||
onStateChange,
|
||||
})
|
||||
);
|
||||
|
||||
// 1. Render phase: updateSrc called but img ref is null (not mounted yet).
|
||||
handle.updateSrc('sprite.jpg');
|
||||
expect(handle.loading).toBe(true);
|
||||
|
||||
// 2. Commit phase: img becomes available (React sets ref) but image is still loading.
|
||||
img = document.createElement('img');
|
||||
// Simulate a loading image: in a real browser, an img with src set is !complete
|
||||
// while the network request is in flight.
|
||||
Object.defineProperty(img, 'complete', { value: false, configurable: true });
|
||||
|
||||
// 3. useEffect: connect() binds events and checks img.complete.
|
||||
handle.connect();
|
||||
|
||||
// Image is not complete, so loading should remain true.
|
||||
expect(handle.loading).toBe(true);
|
||||
|
||||
// 4. Image loads — event listener should catch it.
|
||||
Object.defineProperty(img, 'naturalWidth', { value: 2560, configurable: true });
|
||||
Object.defineProperty(img, 'naturalHeight', { value: 1600, configurable: true });
|
||||
img.dispatchEvent(new Event('load'));
|
||||
|
||||
expect(handle.loading).toBe(false);
|
||||
expect(handle.naturalWidth).toBe(2560);
|
||||
expect(onStateChange).toHaveBeenCalled();
|
||||
|
||||
handle.destroy();
|
||||
});
|
||||
|
||||
it('React lifecycle: handles already-loaded img when ref was null during updateSrc', () => {
|
||||
let img: HTMLImageElement | null = null;
|
||||
const onStateChange = vi.fn();
|
||||
|
||||
const handle = createThumbnail(
|
||||
createOptions({
|
||||
getImg: () => img,
|
||||
onStateChange,
|
||||
})
|
||||
);
|
||||
|
||||
// 1. Render phase: updateSrc called but img ref is null.
|
||||
handle.updateSrc('sprite.jpg');
|
||||
expect(handle.loading).toBe(true);
|
||||
|
||||
// 2. Commit phase: img becomes available and is already cached/complete.
|
||||
img = createMockImg();
|
||||
Object.defineProperty(img, 'complete', { value: true, configurable: true });
|
||||
|
||||
// 3. useEffect: connect() should detect the already-loaded image.
|
||||
handle.connect();
|
||||
|
||||
expect(handle.loading).toBe(false);
|
||||
expect(handle.naturalWidth).toBe(2560);
|
||||
expect(onStateChange).toHaveBeenCalled();
|
||||
|
||||
handle.destroy();
|
||||
});
|
||||
|
||||
it('React lifecycle: handles errored img when ref was null during updateSrc', () => {
|
||||
let img: HTMLImageElement | null = null;
|
||||
const onStateChange = vi.fn();
|
||||
|
||||
const handle = createThumbnail(
|
||||
createOptions({
|
||||
getImg: () => img,
|
||||
onStateChange,
|
||||
})
|
||||
);
|
||||
|
||||
// 1. Render phase: updateSrc called but img ref is null.
|
||||
handle.updateSrc('bad.jpg');
|
||||
expect(handle.loading).toBe(true);
|
||||
|
||||
// 2. Commit phase: img becomes available but image errored (complete but no dimensions).
|
||||
img = document.createElement('img');
|
||||
Object.defineProperty(img, 'complete', { value: true, configurable: true });
|
||||
// naturalWidth defaults to 0 in jsdom — simulates an errored image.
|
||||
|
||||
// 3. useEffect: connect() should detect the errored image.
|
||||
handle.connect();
|
||||
|
||||
expect(handle.loading).toBe(false);
|
||||
expect(handle.error).toBe(true);
|
||||
expect(onStateChange).toHaveBeenCalled();
|
||||
|
||||
handle.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
|
||||
@@ -108,15 +108,21 @@ export function createThumbnail(options: CreateThumbnailOptions): ThumbnailApi {
|
||||
function connect(): void {
|
||||
ensureBindings();
|
||||
|
||||
// Handle the case where the img already loaded before listeners were bound
|
||||
// (e.g., cached image in React where mount happens before useEffect).
|
||||
// Handle the case where the img already loaded or errored before listeners
|
||||
// were bound (e.g., cached image in React where mount happens before useEffect).
|
||||
const img = getImg();
|
||||
|
||||
if (img?.complete && img.naturalWidth > 0 && lastSrc) {
|
||||
naturalWidth = img.naturalWidth;
|
||||
naturalHeight = img.naturalHeight;
|
||||
loading = false;
|
||||
error = false;
|
||||
if (img?.complete && lastSrc) {
|
||||
if (img.naturalWidth > 0) {
|
||||
naturalWidth = img.naturalWidth;
|
||||
naturalHeight = img.naturalHeight;
|
||||
loading = false;
|
||||
error = false;
|
||||
} else {
|
||||
loading = false;
|
||||
error = true;
|
||||
}
|
||||
|
||||
onStateChange();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user