Fix touch, cursor and accessibility in slider

This commit improves the horizontal slider between the generated code
area and the script list. It enhances interaction, accessibility and
performance. It provides missing touch responsiveness, improves
accessibility by using better HTML semantics, introduces throttling and
refactors cursor handling during drag operations with added tests.

These changes provides smoother user experience, better support for
touch devices, reduce load during interactions and ensure the
component's behavior is intuitive and accessible across different
devices and interactions.

- Fix horizontal slider not responding to touch events.
- Improve slider handle to be a `<button>` for improved accessibility
  and native browser support, improving user interaction and keyboard
  support.
- Add throttling in the slider for performance optimization, reducing
  processing load during actions.
- Fix losing dragging state cursor on hover over page elements such as
  input boxes and buttons during dragging.
- Separate dragging logic into its own compositional hook for clearer
  separation of concerns.
- Refactor global cursor mutation process.
- Increase robustness in global cursor changes by preserving and
  restoring previous cursor style to prevent potential side-effects.
- Use Vue 3.2 feature for defining cursor CSS style in `<style>`
  section.
- Expand unit test coverage for horizontal slider, use MouseEvent and
  type cast it to PointerEvent as MouseEvent is not yet supported by
  `jsdom` (see jsdom/jsdom#2527).
This commit is contained in:
undergroundwires
2024-01-08 23:08:10 +01:00
parent 3b1a89ce86
commit 728584240c
8 changed files with 587 additions and 36 deletions

View File

@@ -0,0 +1,118 @@
import { it, describe, expect } from 'vitest';
import {
Ref, ref, defineComponent, nextTick,
} from 'vue';
import { shallowMount } from '@vue/test-utils';
import { CursorStyleDomModifier, useGlobalCursor } from '@/presentation/components/Scripts/Slider/UseGlobalCursor';
describe('useGlobalCursor', () => {
it('adds cursor style to head on activation', async () => {
// arrange
const expectedCursorCssStyleValue = 'pointer';
const isActive = ref(false);
// act
const { cursorStyleDomModifierMock } = createHookWithStubs({ isActive });
isActive.value = true;
await nextTick();
// assert
const { elementsAppendedToHead: appendedElements } = cursorStyleDomModifierMock;
expect(appendedElements.length).to.equal(1);
expect(appendedElements[0].innerHTML).toContain(expectedCursorCssStyleValue);
});
it('removes cursor style from head on deactivation', async () => {
// arrange
const isActive = ref(true);
// act
const { cursorStyleDomModifierMock } = createHookWithStubs({ isActive });
await nextTick();
isActive.value = false;
await nextTick();
// assert
expect(cursorStyleDomModifierMock.elementsRemovedFromHead.length).to.equal(1);
});
it('cleans up cursor style on unmount', async () => {
// arrange
const isActive = ref(true);
// act
const { wrapper, returnObject } = mountWrapperComponent({ isActive });
wrapper.unmount();
await nextTick();
// assert
const { cursorStyleDomModifierMock } = returnObject;
expect(cursorStyleDomModifierMock.elementsRemovedFromHead.length).to.equal(1);
});
it('does not append style to head when initially inactive', async () => {
// arrange
const isActive = ref(false);
// act
const { cursorStyleDomModifierMock } = createHookWithStubs({ isActive });
await nextTick();
// assert
expect(cursorStyleDomModifierMock.elementsAppendedToHead.length).toBe(0);
});
});
function mountWrapperComponent(...hookOptions: Parameters<typeof createHookWithStubs>) {
let returnObject: ReturnType<typeof createHookWithStubs> | undefined;
const wrapper = shallowMount(
defineComponent({
setup() {
returnObject = createHookWithStubs(...hookOptions);
},
template: '<div></div>',
}),
);
if (!returnObject) {
throw new Error('missing hook result');
}
return {
wrapper,
returnObject,
};
}
function createHookWithStubs(options?: {
readonly isActive?: Ref<boolean>;
readonly expectedCursorCssStyleValue?: string;
}) {
const cursorStyleDomModifierMock = new CursorStyleDomModifierStub();
const hookResult = useGlobalCursor(
options?.isActive ?? ref(true),
options?.expectedCursorCssStyleValue ?? 'pointer',
cursorStyleDomModifierMock,
);
return {
cursorStyleDomModifierMock,
hookResult,
};
}
class CursorStyleDomModifierStub implements CursorStyleDomModifier {
public elementsAppendedToHead: HTMLStyleElement[] = [];
public elementsRemovedFromHead: HTMLStyleElement[] = [];
public appendStyleToHead(element: HTMLStyleElement): void {
this.elementsAppendedToHead.push(element);
}
public removeElement(element: HTMLStyleElement): void {
this.elementsRemovedFromHead.push(element);
}
public createStyleElement(): HTMLStyleElement {
const element = document.createElement('style');
return element;
}
}