-
-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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).
- Loading branch information
1 parent
3b1a89c
commit 7285842
Showing
8 changed files
with
587 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
90 changes: 90 additions & 0 deletions
90
src/presentation/components/Scripts/Slider/UseDragHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { | ||
onUnmounted, ref, shallowReadonly, watch, | ||
} from 'vue'; | ||
import { throttle } from '@/application/Common/Timing/Throttle'; | ||
import type { Ref } from 'vue'; | ||
|
||
const ThrottleInMs = 15; | ||
|
||
export function useDragHandler( | ||
draggableElementRef: Readonly<Ref<HTMLElement | undefined>>, | ||
dragDomModifier: DragDomModifier = new GlobalDocumentDragDomModifier(), | ||
throttler = throttle, | ||
) { | ||
const displacementX = ref(0); | ||
const isDragging = ref(false); | ||
|
||
let initialPointerX: number | undefined; | ||
|
||
const onDrag = throttler((event: PointerEvent) => { | ||
if (initialPointerX === undefined) { | ||
throw new Error('Resize action started without an initial X coordinate.'); | ||
} | ||
displacementX.value = event.clientX - initialPointerX; | ||
initialPointerX = event.clientX; | ||
}, ThrottleInMs); | ||
|
||
const stopDrag = () => { | ||
isDragging.value = false; | ||
dragDomModifier.removeEventListenerFromDocument('pointermove', onDrag); | ||
dragDomModifier.removeEventListenerFromDocument('pointerup', stopDrag); | ||
}; | ||
|
||
const startDrag = (event: PointerEvent) => { | ||
isDragging.value = true; | ||
initialPointerX = event.clientX; | ||
dragDomModifier.addEventListenerToDocument('pointermove', onDrag); | ||
dragDomModifier.addEventListenerToDocument('pointerup', stopDrag); | ||
event.stopPropagation(); | ||
event.preventDefault(); | ||
}; | ||
|
||
watch(draggableElementRef, (element) => { | ||
if (!element) { | ||
initialPointerX = undefined; | ||
return; | ||
} | ||
initializeElement(element); | ||
}, { immediate: true }); | ||
|
||
function initializeElement(element: HTMLElement) { | ||
element.style.touchAction = 'none'; // Disable default touch behavior, necessary for resizing functionality to work correctly on touch-enabled devices | ||
element.addEventListener('pointerdown', startDrag); | ||
} | ||
|
||
onUnmounted(() => { | ||
stopDrag(); | ||
}); | ||
|
||
return { | ||
displacementX: shallowReadonly(displacementX), | ||
isDragging: shallowReadonly(isDragging), | ||
}; | ||
} | ||
|
||
export interface DragDomModifier { | ||
addEventListenerToDocument( | ||
type: keyof DocumentEventMap, | ||
handler: EventListener, | ||
): void; | ||
removeEventListenerFromDocument( | ||
type: keyof DocumentEventMap, | ||
handler: EventListener, | ||
): void; | ||
} | ||
|
||
class GlobalDocumentDragDomModifier implements DragDomModifier { | ||
public addEventListenerToDocument( | ||
type: keyof DocumentEventMap, | ||
listener: EventListener, | ||
): void { | ||
document.addEventListener(type, listener); | ||
} | ||
|
||
public removeEventListenerFromDocument( | ||
type: keyof DocumentEventMap, | ||
listener: EventListener, | ||
): void { | ||
document.removeEventListener(type, listener); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
src/presentation/components/Scripts/Slider/UseGlobalCursor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { watch, type Ref, onUnmounted } from 'vue'; | ||
|
||
export function useGlobalCursor( | ||
isActive: Readonly<Ref<boolean>>, | ||
cursorCssValue: string, | ||
documentAccessor: CursorStyleDomModifier = new GlobalDocumentCursorStyleDomModifier(), | ||
) { | ||
const cursorStyle = createCursorStyle(cursorCssValue, documentAccessor); | ||
|
||
watch(isActive, (isCursorVisible) => { | ||
if (isCursorVisible) { | ||
documentAccessor.appendStyleToHead(cursorStyle); | ||
} else { | ||
documentAccessor.removeElement(cursorStyle); | ||
} | ||
}); | ||
|
||
onUnmounted(() => { | ||
documentAccessor.removeElement(cursorStyle); | ||
}); | ||
} | ||
|
||
function createCursorStyle( | ||
cursorCssValue: string, | ||
documentAccessor: CursorStyleDomModifier, | ||
): HTMLStyleElement { | ||
// Using `document.body.style.cursor` does not override cursor when hovered on input boxes, | ||
// buttons etc. so we create a custom style that will do that | ||
const cursorStyle = documentAccessor.createStyleElement(); | ||
cursorStyle.innerHTML = `*{cursor: ${cursorCssValue}!important;}`; | ||
return cursorStyle; | ||
} | ||
|
||
export interface CursorStyleDomModifier { | ||
appendStyleToHead(element: HTMLStyleElement): void; | ||
removeElement(element: HTMLStyleElement): void; | ||
createStyleElement(): HTMLStyleElement; | ||
} | ||
|
||
class GlobalDocumentCursorStyleDomModifier implements CursorStyleDomModifier { | ||
public appendStyleToHead(element: HTMLStyleElement): void { | ||
document.head.appendChild(element); | ||
} | ||
|
||
public removeElement(element: HTMLStyleElement): void { | ||
element.remove(); | ||
} | ||
|
||
public createStyleElement(): HTMLStyleElement { | ||
return document.createElement('style'); | ||
} | ||
} |
Oops, something went wrong.