Skip to content

Commit

Permalink
fix(Slider): enhance Safari (desktop) UX (#1539)
Browse files Browse the repository at this point in the history
- Enhance "jump to" implementation
- Ensure pointer cursor is shown during dragging
  • Loading branch information
tujoworker authored Aug 31, 2022
1 parent 885d2d1 commit 6ca785f
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 57 deletions.
13 changes: 10 additions & 3 deletions packages/dnb-eufemia/src/components/slider/SliderInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import FormStatus from '../form-status/FormStatus'

import {
SliderMainTrack,
SliderTrackAfter,
SliderTrackBefore,
SliderTrackAfter,
} from './SliderTrack'
import { SliderThumb } from './SliderThumb'
import { useSliderProps } from './hooks/useSliderProps'
Expand All @@ -31,8 +31,14 @@ import { clamp, getHumanNumber } from './SliderHelpers'
export function SliderInstance() {
const context = React.useContext(Context)

const { isReverse, isVertical, showButtons, showStatus, allProps } =
useSliderProps()
const {
isReverse,
isVertical,
showButtons,
showStatus,
shouldAnimate,
allProps,
} = useSliderProps()

const {
id,
Expand All @@ -55,6 +61,7 @@ export function SliderInstance() {
'dnb-slider',
isVertical && 'dnb-slider--vertical',
disabled && 'dnb-slider__state--disabled',
shouldAnimate && 'dnb-slider__state--animate',
!showButtons && 'dnb-slider--no-buttons',
isTrue(stretch) && 'dnb-slider--stretch',
label && labelDirection && `dnb-slider__label--${labelDirection}`,
Expand Down
23 changes: 16 additions & 7 deletions packages/dnb-eufemia/src/components/slider/SliderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ export function SliderProvider(localProps: SliderProps) {
const [thumbState, setThumbState] =
React.useState<ThumbStateEnums>('initial')
const thumbIndex = React.useRef<number>(-1)
const [shouldAnimate, updateAnimateState] =
React.useState<boolean>(false)
const [isVertical] = React.useState(isTrue(_vertical))
const [isReverse] = React.useState(
isVertical ? !isTrue(_reverse) : isTrue(_reverse)
Expand Down Expand Up @@ -211,6 +213,7 @@ export function SliderProvider(localProps: SliderProps) {
}

updateValue(multiValues)
setShouldAnimate(false)
}
}

Expand All @@ -234,11 +237,16 @@ export function SliderProvider(localProps: SliderProps) {

const trackRef = React.useRef<HTMLElement>()

const jumpedTimeout = React.useRef<NodeJS.Timeout>()
const setJumpedState = () => {
setThumbState('jumped')
clearTimeout(jumpedTimeout.current)
jumpedTimeout.current = setTimeout(() => setThumbState('normal'), 100)
const animationTimeout = React.useRef<NodeJS.Timeout>()
const setShouldAnimate = (state: boolean) => {
updateAnimateState(state)
clearTimeout(animationTimeout.current)
if (state) {
animationTimeout.current = setTimeout(
() => updateAnimateState(false),
250
)
}
}

const showStatus = getStatusState(status)
Expand All @@ -251,6 +259,7 @@ export function SliderProvider(localProps: SliderProps) {
isMulti,
isReverse,
isVertical,
shouldAnimate,
value,
values,
setValue,
Expand All @@ -264,8 +273,8 @@ export function SliderProvider(localProps: SliderProps) {
emitChange,
allProps,
trackRef,
setJumpedState,
jumpedTimeout,
setShouldAnimate,
animationTimeout,
}}
>
{localProps.children}
Expand Down
3 changes: 1 addition & 2 deletions packages/dnb-eufemia/src/components/slider/SliderThumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ export function SliderThumb() {
)
}

validateDOMAttributes(null, helperParams)
validateDOMAttributes(allProps, thumbParams)
validateDOMAttributes(allProps, thumbParams) // because we send along rest attributes

return (
<>
Expand Down
23 changes: 9 additions & 14 deletions packages/dnb-eufemia/src/components/slider/SliderTrack.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import classnames from 'classnames'
import React from 'react'
import {
dispatchCustomElementEvent,
validateDOMAttributes,
} from '../../shared/component-helper'
import { dispatchCustomElementEvent } from '../../shared/component-helper'
import { useSliderEvents } from './hooks/useSliderEvents'
import { useSliderProps } from './hooks/useSliderProps'
import { clamp, formatNumber } from './SliderHelpers'
Expand All @@ -14,7 +10,7 @@ export function SliderMainTrack({
}: {
children: React.ReactNode | React.ReactNode[]
}) {
const { isMulti, value, allProps, trackRef, jumpedTimeout, thumbState } =
const { isMulti, value, allProps, trackRef, animationTimeout } =
useSliderProps()
const { id, numberFormat, onInit } = allProps
const { onTrackClickHandler, onThumbMouseDownHandler, removeEvents } =
Expand All @@ -35,27 +31,26 @@ export function SliderMainTrack({

return () => {
removeEvents()
clearTimeout(jumpedTimeout.current)
clearTimeout(animationTimeout.current)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const trackParams = {
className: classnames(
'dnb-slider__track',
thumbState && `dnb-slider__state--${thumbState}`
),
onTouchStart: onTrackClickHandler,
onTouchStartCapture: onThumbMouseDownHandler,
onMouseDown: onTrackClickHandler,
onMouseDownCapture: onThumbMouseDownHandler,
}

validateDOMAttributes(null, trackParams)

return (
// @ts-ignore
<span id={id} ref={trackRef} {...trackParams}>
<span
id={id}
ref={trackRef}
className="dnb-slider__track"
{...trackParams}
>
{children}
</span>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ const props: SliderProps = {
labelDirection: 'horizontal',
}

const resetMouseSimulation = () => {
fireEvent.mouseUp(document.querySelector('.dnb-slider__track'))
}

describe('Slider component', () => {
afterEach(() => {
resetMouseSimulation()
})

it('supports snake_case props', () => {
const props: SliderProps = {
id: 'slider',
Expand Down Expand Up @@ -229,8 +229,6 @@ describe('Slider component', () => {

expect(onChange).toBeCalledTimes(1)
expect(onChange.mock.calls[0][0].value).toBe(80)

resetMouseSimulation()
})

describe('multi thumb', () => {
Expand Down Expand Up @@ -354,8 +352,6 @@ describe('Slider component', () => {
simulateMouseMove({ pageX: 40, width: 100, height: 10 })

expect(onChange.mock.calls[2][0].value).toEqual([10, 40, 80])

resetMouseSimulation()
})

it('updates thumb index and returns correct event value', () => {
Expand All @@ -382,8 +378,6 @@ describe('Slider component', () => {
simulateMouseMove({ pageX: 20, width: 100, height: 10 })

expect(onChange.mock.calls[1][0].value).toEqual([10, 20, 40])

resetMouseSimulation()
})

it('will not swap thumb positions when multiThumbBehavior="omit"', () => {
Expand Down Expand Up @@ -436,8 +430,6 @@ describe('Slider component', () => {
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 4; left: 50%;'
)

resetMouseSimulation()
})

it('will push thumb positions when multiThumbBehavior="push"', () => {
Expand Down Expand Up @@ -495,8 +487,6 @@ describe('Slider component', () => {
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 4; left: 20%;'
)

resetMouseSimulation()
})

it('sets correct inline styles', () => {
Expand Down Expand Up @@ -536,8 +526,6 @@ describe('Slider component', () => {
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 3; left: 80%;'
)

resetMouseSimulation()
})
})

Expand All @@ -558,6 +546,13 @@ const getButtonHelper = (): HTMLInputElement => {
return document.querySelector('.dnb-slider__button-helper')
}

const resetMouseSimulation = () => {
const elem = document.querySelector('.dnb-slider__track')
if (elem) {
fireEvent.mouseUp(elem)
}
}

const simulateMouseMove = (props) => {
fireEvent.mouseUp(document.querySelector('.dnb-slider__track'))
fireEvent.mouseDown(document.querySelector('.dnb-slider__track'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,7 @@ legend.dnb-form-label {
.dnb-slider {
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
display: flex;
font-size: var(--font-size-small);
line-height: var(--slider-thumb-size); }
Expand Down Expand Up @@ -826,8 +827,8 @@ legend.dnb-form-label {
z-index: 2; }
.dnb-slider--vertical .dnb-slider__button.dnb-button--size-small {
transform: translateX(0.25rem); }
.dnb-slider__state--jumped .dnb-slider__thumb,
.dnb-slider__state--jumped .dnb-slider__line {
.dnb-slider__state--animate .dnb-slider__thumb,
.dnb-slider__state--animate .dnb-slider__line {
transition: left 250ms ease, top 250ms ease, bottom 250ms ease, right 250ms ease, box-shadow 250ms ease; }
.dnb-slider__state--disabled .dnb-slider__track,
.dnb-slider__state--disabled .dnb-slider__thumb,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function useSliderEvents() {
emitChange,
trackRef,
isVertical,
setJumpedState,
setShouldAnimate,
setThumbState,
setThumbIndex,
allProps,
Expand All @@ -28,14 +28,15 @@ export function useSliderEvents() {
const percent = calculatePercent(trackRef.current, event, isVertical)

emitChange(event, percentToValue(percent, min, max, isReverse))
setJumpedState()
setShouldAnimate(true)
}

const onThumbMouseDownHandler = (event: React.SyntheticEvent) => {
const target = event.target as HTMLButtonElement

setThumbIndex(parseFloat(target.dataset.index))
setThumbState('released')
setThumbState('activated')
setShouldAnimate(false)

if (typeof onDragStart === 'function') {
dispatchCustomElementEvent(allProps, 'onDragStart', {
Expand All @@ -62,7 +63,7 @@ export function useSliderEvents() {
}

const onThumbMouseUpHandler = () => {
setThumbState('activated')
setThumbState('released')
}

const onTrackTouchEndHandler = (event: TouchEvent) =>
Expand Down Expand Up @@ -106,13 +107,12 @@ export function useSliderEvents() {
const onTrackTouchMoveHandler = (event: MouseEvent) =>
onTrackMouseMoveHandler(event)
const onTrackMouseMoveHandler = (event: MouseEvent) => {
event.preventDefault() // ensures correct cursor in Safari (dekstop)

let elem = trackRef.current

// we have to mock this for jsdom.
if (
// @ts-ignore
typeof event?.detail?.height !== 'undefined'
) {
if (process.env.NODE_ENV === 'test') {
// @ts-ignore
elem = createMockDiv(event.detail)
// @ts-ignore
Expand Down
5 changes: 3 additions & 2 deletions packages/dnb-eufemia/src/components/slider/style/_slider.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
.dnb-slider {
user-select: none;
-webkit-user-select: none; // Safari / Touch fix
-webkit-touch-callout: none; // Safari / Touch fix

// use flex to make sure we have the whole space available
display: flex;
Expand Down Expand Up @@ -180,8 +181,8 @@
transform: translateX(0.25rem);
}

&__state--jumped &__thumb,
&__state--jumped &__line {
&__state--animate &__thumb,
&__state--animate &__line {
transition: left 250ms ease, top 250ms ease, bottom 250ms ease,
right 250ms ease, box-shadow 250ms ease;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/dnb-eufemia/src/components/slider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ export type ThumbStateEnums =
| 'activated'
| 'released'
| 'focused'
| 'jumped'

export type SliderContextTypes = {
isMulti: boolean
isReverse: boolean
isVertical: boolean
shouldAnimate: boolean
thumbState: ThumbStateEnums
thumbIndex: React.RefObject<number>
showStatus: boolean
Expand All @@ -133,6 +133,6 @@ export type SliderContextTypes = {
setThumbIndex: (thumbIndex: number) => void
emitChange: (emitEvent: MouseEvent | TouchEvent, value: number) => void
trackRef: React.RefObject<HTMLElement>
setJumpedState: () => void
jumpedTimeout: React.RefObject<NodeJS.Timeout>
setShouldAnimate: (state: boolean) => void
animationTimeout: React.RefObject<NodeJS.Timeout>
}

0 comments on commit 6ca785f

Please sign in to comment.