Skip to content

Commit

Permalink
feat(Slider): add multiThumbBehavior property (#1526)
Browse files Browse the repository at this point in the history
  • Loading branch information
tujoworker authored Aug 25, 2022
1 parent b7f532e commit f835651
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ export const SliderExampleMultiButtons = () => (
</ComponentBox>
)

export const SliderExampleMultiButtonsThumbBehavior = () => (
<ComponentBox>
{
/* jsx */ `
<FormRow vertical>
<Slider
multiThumbBehavior="omit"
value={[30, 70]}
label="Omit behavior:"
onChange={({ value }) => console.log('onChange:', value)}
bottom
/>
<Slider
multiThumbBehavior="push"
value={[10, 50, 70]}
step={1}
label="Push behavior:"
numberFormat={{ decimals: 2, currency: true }}
onChange={({ value, number }) => console.log('onChange:', value, number)}
/>
</FormRow>
`
}
</ComponentBox>
)

export const SliderExampleHorizontalSync = () => (
<ComponentBox useRender>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ SliderExampleHorizontalSync,
SliderExampleSuffix,
SliderExampleRange,
SliderExampleMultiButtons,
SliderExampleMultiButtonsThumbBehavior,
} from 'Docs/uilib/components/slider/Examples'

## Demos
Expand All @@ -24,6 +25,10 @@ Provide the `value` property as an array with numbers. The `onChange` event will

<SliderExampleMultiButtons />

By default, the thumbs can swap positions. You can change that behavior with `multiThumbBehavior`.

<SliderExampleMultiButtonsThumbBehavior />

### Vertical slider with steps of 10

<SliderVerticalWithSteps />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ showTabs: true
| `reverse` | _(optional)_ show the slider reversed. Defaults to `false`. |
| `stretch` | _(optional)_ if set to `true`, then the slider will be 100% in `width`. |
| `hideButtons` | _(optional)_ removes the helper buttons. Defaults to `false`. |
| `multiThumbBehavior` | _(optional)_ use either `omit`, `push` or `swap`. This property only works for two (range) or more thumb buttons, while `omit` will stop the thumb from swapping, `push` will push its nearest thumb along. Defaults to `swap`. |
| `thumbTitle` | _(optional)_ give the slider thumb button a title for accessibility reasons. Defaults to `null`. |
| `subtractTitle` | _(optional)_ give the subtract button a title for accessibility reasons. Defaults to ``. |
| `addTitle` | _(optional)_ give the add button a title for accessibility reasons. Defaults to `+`. |
Expand Down
5 changes: 3 additions & 2 deletions packages/dnb-eufemia/src/components/slider/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import React from 'react'
import { SliderProvider } from './SliderProvider'
import { SliderInstance } from './SliderInstance'

import type { SliderProps, ValueTypes } from './types'
import { ISpacingProps } from '../../shared/interfaces'

export type { SliderProps, ValueTypes }
import type { SliderProps } from './types'

export * from './types'

function Slider(localProps: SliderProps & ISpacingProps) {
return (
Expand Down
57 changes: 45 additions & 12 deletions packages/dnb-eufemia/src/components/slider/SliderProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const defaultProps = {
min: 0,
max: 100,
value: -1,
multiThumbBehavior: 'swap',
}

export const SliderContext = React.createContext<SliderContextTypes>(null)
Expand Down Expand Up @@ -80,6 +81,7 @@ export function SliderProvider(localProps: SliderProps) {
subtractTitle, // eslint-disable-line
addTitle, // eslint-disable-line
hideButtons, // eslint-disable-line
multiThumbBehavior,
numberFormat,
skeleton,
max, // eslint-disable-line
Expand All @@ -99,7 +101,8 @@ export function SliderProvider(localProps: SliderProps) {
...attributes // Find a DOM element to forwards props too when multi buttons are supported
} = allProps

const [value, setValue] = React.useState(_value)
const [value, setValue] = React.useState<ValueTypes>(_value)
const realtimeValue = React.useRef<ValueTypes>(_value)
const [thumbState, setThumbState] =
React.useState<ThumbStateEnums>('initial')
const thumbIndex = React.useRef<number>(-1)
Expand Down Expand Up @@ -134,6 +137,11 @@ export function SliderProvider(localProps: SliderProps) {
return currentIndex
}

const updateValue = (value: ValueTypes) => {
setValue(value)
realtimeValue.current = value
}

const emitChange = (
event: MouseEvent | TouchEvent,
rawValue: number
Expand All @@ -142,34 +150,59 @@ export function SliderProvider(localProps: SliderProps) {
return
}

const currentValue = roundValue(rawValue, step)

if (currentValue > -1 && rawValue !== value) {
let newValue: ValueTypes = currentValue
let numberValue = roundValue(rawValue, step)
let multiValues: ValueTypes = numberValue

if (numberValue > -1) {
if (isMulti) {
const currentIndex = getAndUpdateCurrentIndex(currentValue)
const currentIndex = getAndUpdateCurrentIndex(numberValue)
const lower = realtimeValue.current[currentIndex - 1]
const upper = realtimeValue.current[currentIndex + 1]

newValue = getUpdatedValues(value, currentIndex, currentValue)
if (multiThumbBehavior === 'omit') {
if (numberValue < lower) {
numberValue = lower
}
if (numberValue > upper) {
numberValue = upper
}
}

multiValues = getUpdatedValues(
multiThumbBehavior === 'push'
? (realtimeValue.current as Array<number>)
: value,
currentIndex,
numberValue
)

if (multiThumbBehavior === 'push') {
if (typeof lower !== 'undefined' && numberValue < lower) {
multiValues[currentIndex - 1] = numberValue
}
if (typeof upper !== 'undefined' && numberValue >= upper) {
multiValues[currentIndex + 1] = numberValue
}
}
}

if (typeof onChange === 'function') {
const obj: onChangeEventProps = {
value: newValue,
value: multiValues,
rawValue,
raw_value: rawValue, // deprecated
event,
number: null,
}

if (numberFormat) {
obj.number = formatNumber(currentValue, numberFormat)
obj.number = formatNumber(numberValue, numberFormat)
}

dispatchCustomElementEvent(allProps, 'onChange', obj)
}

setValue(newValue)
updateValue(multiValues)
}
}

Expand All @@ -180,10 +213,10 @@ export function SliderProvider(localProps: SliderProps) {
})

if (hasChanged) {
setValue(_value)
updateValue(_value)
}
} else {
setValue(_value)
updateValue(_value)
}

// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
157 changes: 153 additions & 4 deletions packages/dnb-eufemia/src/components/slider/__tests__/Slider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { axeComponent, loadScss } from '../../../core/jest/jestSetup'
import { fireEvent, render } from '@testing-library/react'
import Slider from '../Slider'

import type { SliderProps } from '../Slider'
import type { SliderProps, onChangeEventProps } from '../Slider'

const props: SliderProps = {
id: 'slider',
Expand Down Expand Up @@ -62,6 +62,26 @@ describe('Slider component', () => {
)
})

it('should include className', () => {
render(<Slider {...props} className="custom-class" />)

const element = document.querySelector('.dnb-slider')

expect(Array.from(element.classList)).toEqual(
expect.arrayContaining(['custom-class'])
)
})

it('should apply custom attributes to thumb button', () => {
render(<Slider {...props} data-extra="property-value" />)

const element = document.querySelector(
'.dnb-slider__thumb .dnb-button'
)

expect(element.getAttribute('data-extra')).toBe('property-value')
})

it('should support stretch', () => {
render(<Slider {...props} stretch />)

Expand Down Expand Up @@ -196,6 +216,17 @@ describe('Slider component', () => {
})

describe('multi thumb', () => {
const SliderWithStateUpdate = (props: SliderProps) => {
const [value, setValue] = React.useState(props.value)
const onChangehandler = (event: onChangeEventProps) => {
setValue(event.value)
if (props.onChange) {
props.onChange(event)
}
}
return <Slider {...props} value={value} onChange={onChangehandler} />
}

const resetMouseSimulation = () => {
fireEvent.mouseUp(document.querySelector('.dnb-slider__track'))
}
Expand All @@ -205,7 +236,7 @@ describe('Slider component', () => {

props.value = [20, 30, 90]
render(
<Slider
<SliderWithStateUpdate
{...props}
numberFormat={{ currency: true, decimals: 1 }}
onChange={onChange}
Expand Down Expand Up @@ -266,7 +297,7 @@ describe('Slider component', () => {
const onChange = jest.fn()

props.value = [10, 30, 40]
render(<Slider {...props} onChange={onChange} />)
render(<SliderWithStateUpdate {...props} onChange={onChange} />)

const secondThumb = document.querySelectorAll(
'.dnb-slider__button-helper'
Expand All @@ -290,10 +321,128 @@ describe('Slider component', () => {
resetMouseSimulation()
})

it('will not swap thumb positions when multiThumbBehavior="omit"', () => {
const onChange = jest.fn()

props.value = [10, 30, 60]

render(
<SliderWithStateUpdate
{...props}
step={1}
multiThumbBehavior="omit"
onChange={onChange}
/>
)

const getThumbElements = (index: number) =>
document.querySelectorAll('.dnb-slider__thumb')[
index
] as HTMLElement

const secondThumb = document.querySelectorAll(
'.dnb-slider__button-helper'
)[1]
const thirdThumb = document.querySelectorAll(
'.dnb-slider__button-helper'
)[2]

fireEvent.focus(secondThumb)
simulateMouseMove({ pageX: 50, width: 100, height: 10 })

expect(onChange.mock.calls[0][0].value).toEqual([10, 50, 60])
expect(getThumbElements(0).getAttribute('style')).toBe(
'z-index: 3; left: 10%;'
)
expect(getThumbElements(1).getAttribute('style')).toBe(
'z-index: 4; left: 50%;'
)
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 3; left: 60%;'
)

resetMouseSimulation()

fireEvent.focus(thirdThumb)
simulateMouseMove({ pageX: 20, width: 100, height: 10 })

expect(onChange.mock.calls[1][0].value).toEqual([10, 50, 50])
expect(getThumbElements(0).getAttribute('style')).toBe(
'z-index: 3; left: 10%;'
)
expect(getThumbElements(1).getAttribute('style')).toBe(
'z-index: 3; left: 50%;'
)
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 4; left: 50%;'
)

resetMouseSimulation()
})

it('will push thumb positions when multiThumbBehavior="push"', () => {
const onChange = jest.fn()

props.value = [10, 30, 60]

render(
<SliderWithStateUpdate
{...props}
step={1}
onChange={onChange}
multiThumbBehavior="push"
/>
)

const getThumbElements = (index: number) =>
document.querySelectorAll('.dnb-slider__thumb')[
index
] as HTMLElement

const secondThumb = document.querySelectorAll(
'.dnb-slider__button-helper'
)[1]
const thirdThumb = document.querySelectorAll(
'.dnb-slider__button-helper'
)[2]

fireEvent.focus(secondThumb)
simulateMouseMove({ pageX: 50, width: 100, height: 10 })

expect(onChange.mock.calls[0][0].value).toEqual([10, 50, 60])
expect(getThumbElements(0).getAttribute('style')).toBe(
'z-index: 3; left: 10%;'
)
expect(getThumbElements(1).getAttribute('style')).toBe(
'z-index: 4; left: 50%;'
)
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 3; left: 60%;'
)

resetMouseSimulation()

fireEvent.focus(thirdThumb)
simulateMouseMove({ pageX: 20, width: 100, height: 10 })

expect(onChange.mock.calls[1][0].value).toEqual([10, 20, 20])
expect(getThumbElements(0).getAttribute('style')).toBe(
'z-index: 3; left: 10%;'
)
expect(getThumbElements(1).getAttribute('style')).toBe(
'z-index: 3; left: 20%;'
)
expect(getThumbElements(2).getAttribute('style')).toBe(
'z-index: 4; left: 20%;'
)

resetMouseSimulation()
})

it('sets correct inline styles', () => {
props.value = [20, 30, 90]
render(
<Slider
<SliderWithStateUpdate
{...props}
numberFormat={{ currency: true, decimals: 1 }}
/>
Expand Down
Loading

0 comments on commit f835651

Please sign in to comment.