diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/slider/Examples.tsx b/packages/dnb-design-system-portal/src/docs/uilib/components/slider/Examples.tsx index e5f8fb4040f..4dd199d2389 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/slider/Examples.tsx +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/slider/Examples.tsx @@ -5,6 +5,7 @@ import React from 'react' import ComponentBox from 'dnb-design-system-portal/src/shared/tags/ComponentBox' +import { format } from '@dnb/eufemia/src/components/number-format/NumberUtils' export const SliderExampleDefault = () => ( @@ -16,6 +17,7 @@ export const SliderExampleDefault = () => ( value={70} label="Default Slider:" numberFormat={{ currency: 'EUR' }} + tooltip={true} onChange={({ value }) => console.log('onChange:', value)} /> ` @@ -24,7 +26,7 @@ export const SliderExampleDefault = () => ( ) export const SliderExampleMultiButtons = () => ( - + { /* jsx */ ` @@ -34,16 +36,18 @@ export const SliderExampleMultiButtons = () => ( value={[30, 70]} step={5} label="Range with steps:" - numberFormat={{ currency: 'EUR' }} + numberFormat={{ currency: 'USD' }} + tooltip onChange={({ value }) => console.log('onChange:', value)} bottom - /> - + format(value, { percent: true })} + tooltip onChange={({ value, number }) => console.log('onChange:', value, number)} /> @@ -62,6 +66,7 @@ export const SliderExampleMultiButtonsThumbBehavior = () => ( value={[30, 70]} label="Omit behavior:" numberFormat={{ currency: 'EUR' }} + tooltip={true} onChange={({ value }) => console.log('onChange:', value)} bottom /> @@ -71,6 +76,7 @@ export const SliderExampleMultiButtonsThumbBehavior = () => ( step={1} label="Push behavior:" numberFormat={{ currency: true }} + tooltip={true} onChange={({ value, number }) => console.log('onChange:', value, number)} /> @@ -80,7 +86,7 @@ export const SliderExampleMultiButtonsThumbBehavior = () => ( ) export const SliderExampleHorizontalSync = () => ( - + { /* jsx */ ` const Component = () => { @@ -88,10 +94,11 @@ const Component = () => { return (<> setValue(value)} /> @@ -99,10 +106,12 @@ const Component = () => { value={value} vertical={true} hideButtons={true} - step={1} + step={10} label="Slider B:" labelDirection="vertical" - numberFormat={{ currency: 'NOK' }} + numberFormat={(value) => format(value, { currency: 'NOK' })} + tooltip + alwaysShowTooltip onChange={({ value }) => setValue(value)} /> ( ) -export const SliderExampleRange = () => ( - - { - /* jsx */ ` - - - Native Range Slider - - console.log(event.currentTarget.value)} - /> - - ` - } - -) - export const SliderVerticalWithSteps = () => ( { diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/slider/info.md b/packages/dnb-design-system-portal/src/docs/uilib/components/slider/info.md index f51baa76119..029fa08e741 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/slider/info.md +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/slider/info.md @@ -9,7 +9,3 @@ The Slider component provides a visual indication of adjustable value. A value c ### Define a `min` and `max` value Keep in mind, you should most probably define your `min` and `max` value, because they are tied closely to your given value property. - -### Tooltip - -When a `numberFormat` is given, a Tooltip will be shown above the thumb button. diff --git a/packages/dnb-design-system-portal/src/docs/uilib/components/slider/properties.md b/packages/dnb-design-system-portal/src/docs/uilib/components/slider/properties.md index 70a7bdcae9e..cda424d80c2 100644 --- a/packages/dnb-design-system-portal/src/docs/uilib/components/slider/properties.md +++ b/packages/dnb-design-system-portal/src/docs/uilib/components/slider/properties.md @@ -18,7 +18,9 @@ showTabs: true | `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 `+`. | -| `numberFormat` | _(optional)_ Will extend the return object with a `number` property (from `onChange` event). You can use all the options from the [NumberFormat](/uilib/components/number-format/properties) component. It also will use that formatted number in the increase/decrease buttons. If it has to represent a currency, then use e.g. `numberFormat={{ currency: true, decimals: 0 }}` | +| `numberFormat` | _(optional)_ will extend the return object with a `number` property (from `onChange` event). You can use all the options from the [NumberFormat](/uilib/components/number-format/properties) component. It also will use that formatted number in the increase/decrease buttons. If it has to represent a currency, then use e.g. `numberFormat={{ currency: true, decimals: 0 }}` | +| `tooltip` | _(optional)_ use `true` to show a tooltip on `mouseOver`, `touchStart` and `focus`, showing the current number (if `numberFormat` is given) or the raw value. | +| `alwaysShowTooltip` | _(optional)_ use `true` to always show the tooltip, in addition to the `tooltip` property. | | `label` | _(optional)_ prepends the Form Label component. If no ID is provided, a random ID is created. | | `labelDirection` | _(optional)_ use `labelDirection="vertical"` to change the label layout direction. Defaults to `horizontal`. | | `labelSrOnly` | _(optional)_ use `true` to make the label only readable by screen readers. | diff --git a/packages/dnb-eufemia/src/components/slider/SliderHelpers.tsx b/packages/dnb-eufemia/src/components/slider/SliderHelpers.tsx index 3f106fc62cb..409e3c9d0db 100644 --- a/packages/dnb-eufemia/src/components/slider/SliderHelpers.tsx +++ b/packages/dnb-eufemia/src/components/slider/SliderHelpers.tsx @@ -1,13 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { - format, - formatOptionParams, - formatValue, - formatReturnType, - formatReturnValue, -} from '../number-format/NumberUtils' +import { format, formatReturnValue } from '../number-format/NumberUtils' -import type { ValueTypes } from './types' +import type { NumberFormatTypes, ValueTypes } from './types' export const percentToValue = ( percent: number, @@ -124,26 +118,21 @@ export const closestIndex = (goal: number, array: Array) => { return array.findIndex((num) => num === res) } -export const formatNumber = ( - value: formatValue, - opts: formatOptionParams = null -): formatReturnType => { - if (opts) { - return format(value, opts) - } - return value -} - export const getFormattedNumber = ( value: number, - numberFormat: formatOptionParams + numberFormat: NumberFormatTypes ) => { - return ( - numberFormat - ? formatNumber(value as number, { - ...(numberFormat || {}), - returnAria: true, - }) - : { aria: null, number: null } - ) as formatReturnValue + if (numberFormat) { + if (typeof numberFormat === 'function') { + const number = numberFormat(value as number) as string + return { number, aria: number } + } + + return format(value as number, { + ...(numberFormat || {}), + returnAria: true, + }) as formatReturnValue + } + + return { aria: null, number: null } as formatReturnValue } diff --git a/packages/dnb-eufemia/src/components/slider/SliderProvider.tsx b/packages/dnb-eufemia/src/components/slider/SliderProvider.tsx index 3e40f6646d3..539e9aa5a91 100644 --- a/packages/dnb-eufemia/src/components/slider/SliderProvider.tsx +++ b/packages/dnb-eufemia/src/components/slider/SliderProvider.tsx @@ -12,7 +12,7 @@ import { import Context from '../../shared/Context' import { closestIndex, - formatNumber, + getFormattedNumber, getUpdatedValues, roundValue, } from './SliderHelpers' @@ -83,6 +83,8 @@ export function SliderProvider(localProps: SliderProps) { hideButtons, // eslint-disable-line multiThumbBehavior, numberFormat, + tooltip, // eslint-disable-line + alwaysShowTooltip, // eslint-disable-line skeleton, max, // eslint-disable-line min, // eslint-disable-line @@ -206,14 +208,13 @@ export function SliderProvider(localProps: SliderProps) { } if (numberFormat) { - obj.number = formatNumber(numberValue, numberFormat) + obj.number = getFormattedNumber(numberValue, numberFormat).number } dispatchCustomElementEvent(allProps, 'onChange', obj) } updateValue(multiValues) - setShouldAnimate(false) } } diff --git a/packages/dnb-eufemia/src/components/slider/SliderThumb.tsx b/packages/dnb-eufemia/src/components/slider/SliderThumb.tsx index 2ca7411411b..ae0e1ebf3c3 100644 --- a/packages/dnb-eufemia/src/components/slider/SliderThumb.tsx +++ b/packages/dnb-eufemia/src/components/slider/SliderThumb.tsx @@ -47,6 +47,8 @@ function Thumb({ value, currentIndex }: ThumbProps) { disabled, suffix, numberFormat, + tooltip, + alwaysShowTooltip, } = allProps const index = thumbIndex.current @@ -63,12 +65,12 @@ function Thumb({ value, currentIndex }: ThumbProps) { const { number, aria } = getFormattedNumber(value, numberFormat) - const [hover, setHover] = React.useState(false) + const [showTooltip, setShowTooltip] = React.useState(false) const onMouseEnterHandler = () => { - setHover(true) + setShowTooltip(true) } const onMouseLeaveHandler = () => { - setHover(false) + setShowTooltip(false) } const { @@ -105,16 +107,11 @@ function Thumb({ value, currentIndex }: ThumbProps) { validateDOMAttributes(allProps, thumbParams) // because we send along rest attributes + const elemRef = React.useRef() + return ( <> - + { + onHelperFocusHandler(event) + onMouseEnterHandler() + }} + onBlur={onMouseLeaveHandler} + onMouseDown={onThumbMouseDownHandler} + onMouseUp={onThumbMouseUpHandler} aria-valuemin={min} aria-valuemax={max} aria-valuenow={value} @@ -143,13 +144,21 @@ function Thumb({ value, currentIndex }: ThumbProps) { disabled={disabled} skeleton={skeleton} data-index={currentIndex} - onMouseDown={onThumbMouseDownHandler} - onMouseUp={onThumbMouseUpHandler} + onMouseEnter={onMouseEnterHandler} + onMouseLeave={onMouseLeaveHandler} + onTouchStart={onMouseEnterHandler} + onTouchEnd={onMouseLeaveHandler} {...thumbParams} /> - {numberFormat && ( - + {tooltip && ( + {number || value} )} diff --git a/packages/dnb-eufemia/src/components/slider/SliderTrack.tsx b/packages/dnb-eufemia/src/components/slider/SliderTrack.tsx index 4d4b7d9acd2..fe69a42a754 100644 --- a/packages/dnb-eufemia/src/components/slider/SliderTrack.tsx +++ b/packages/dnb-eufemia/src/components/slider/SliderTrack.tsx @@ -3,7 +3,7 @@ import React from 'react' import { dispatchCustomElementEvent } from '../../shared/component-helper' import { useSliderEvents } from './hooks/useSliderEvents' import { useSliderProps } from './hooks/useSliderProps' -import { clamp, formatNumber } from './SliderHelpers' +import { clamp, getFormattedNumber } from './SliderHelpers' export function SliderMainTrack({ children, @@ -13,7 +13,7 @@ export function SliderMainTrack({ const { isMulti, value, allProps, trackRef, animationTimeout } = useSliderProps() const { id, numberFormat, onInit } = allProps - const { onTrackClickHandler, onThumbMouseDownHandler, removeEvents } = + const { onTrackMouseUpHandler, onThumbMouseDownHandler, removeEvents } = useSliderEvents() React.useEffect(() => { @@ -24,7 +24,7 @@ export function SliderMainTrack({ number: null, } if (numberFormat) { - obj.number = formatNumber(value as number, numberFormat) + obj.number = getFormattedNumber(value as number, numberFormat) } dispatchCustomElementEvent(allProps, 'onInit', obj) } @@ -37,9 +37,9 @@ export function SliderMainTrack({ }, []) const trackParams = { - onTouchStart: onTrackClickHandler, + onTouchStart: onTrackMouseUpHandler, onTouchStartCapture: onThumbMouseDownHandler, - onMouseDown: onTrackClickHandler, + onMouseDown: onTrackMouseUpHandler, onMouseDownCapture: onThumbMouseDownHandler, } diff --git a/packages/dnb-eufemia/src/components/slider/__tests__/Slider.test.tsx b/packages/dnb-eufemia/src/components/slider/__tests__/Slider.test.tsx index eac96b92a26..e532d64d40a 100644 --- a/packages/dnb-eufemia/src/components/slider/__tests__/Slider.test.tsx +++ b/packages/dnb-eufemia/src/components/slider/__tests__/Slider.test.tsx @@ -9,6 +9,7 @@ import { fireEvent, render } from '@testing-library/react' import Slider from '../Slider' import type { SliderProps, onChangeEventProps } from '../Slider' +import { format } from '../../number-format/NumberUtils' const props: SliderProps = { id: 'slider', @@ -153,33 +154,86 @@ describe('Slider component', () => { ) }) - it('shows Tooltip on hover when numberFormat is given', () => { - render() + describe('Tooltip', () => { + it('shows always a Tooltip when alwaysShowTooltip is true', () => { + render() - const mainElem = document.querySelector('.dnb-slider') - const thumbElem = mainElem.querySelector('.dnb-slider__thumb') - const tooltipElem = thumbElem.querySelector('.dnb-tooltip') + const mainElem = document.querySelector('.dnb-slider') + const thumbElem = mainElem.querySelector('.dnb-slider__thumb') + const tooltipElem = thumbElem.querySelector('.dnb-tooltip') - expect(tooltipElem.textContent).toBe('70,00 €') - expect(Array.from(tooltipElem.classList)).toEqual( - expect.arrayContaining(['dnb-tooltip']) - ) + expect(tooltipElem.textContent).toBe('70') + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--active']) + ) + }) - fireEvent.mouseOver(thumbElem) + it('shows Tooltip on hover with numberFormat', () => { + render( + + ) - simulateMouseMove({ pageX: 80, width: 100, height: 10 }) + const mainElem = document.querySelector('.dnb-slider') + const thumbElem = mainElem.querySelector('.dnb-slider__thumb') + const tooltipElem = thumbElem.querySelector('.dnb-tooltip') - expect(Array.from(tooltipElem.classList)).toEqual( - expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--active']) - ) + expect(tooltipElem.textContent).toBe('70,00 €') + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip']) + ) - expect(tooltipElem.textContent).toBe('80,00 €') + fireEvent.mouseOver(thumbElem) - fireEvent.mouseOut(thumbElem) + simulateMouseMove({ pageX: 80, width: 100, height: 10 }) - expect(Array.from(tooltipElem.classList)).toEqual( - expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--hide']) - ) + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--active']) + ) + + expect(tooltipElem.textContent).toBe('80,00 €') + + fireEvent.mouseOut(thumbElem) + + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--hide']) + ) + }) + + it('shows Tooltip on hover with custom formatting', () => { + render( + format(value, { percent: true })} + tooltip + step={null} + /> + ) + + const mainElem = document.querySelector('.dnb-slider') + const thumbElem = mainElem.querySelector('.dnb-slider__thumb') + const tooltipElem = thumbElem.querySelector('.dnb-tooltip') + + expect(tooltipElem.textContent).toBe('70 %') + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip']) + ) + + fireEvent.mouseOver(thumbElem) + + simulateMouseMove({ pageX: 80.5, width: 100, height: 10 }) + + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--active']) + ) + + expect(tooltipElem.textContent).toBe('80,5 %') + + fireEvent.mouseOut(thumbElem) + + expect(Array.from(tooltipElem.classList)).toEqual( + expect.arrayContaining(['dnb-tooltip', 'dnb-tooltip--hide']) + ) + }) }) it('has events that return a correct value', () => { diff --git a/packages/dnb-eufemia/src/components/slider/hooks/useSliderEvents.tsx b/packages/dnb-eufemia/src/components/slider/hooks/useSliderEvents.tsx index 981f4792dbe..af3cf7a4ce7 100644 --- a/packages/dnb-eufemia/src/components/slider/hooks/useSliderEvents.tsx +++ b/packages/dnb-eufemia/src/components/slider/hooks/useSliderEvents.tsx @@ -24,7 +24,7 @@ export function useSliderEvents() { } = React.useContext(SliderContext) const { min, max, onDragStart, onDragEnd } = allProps - const onTrackClickHandler = (event: MouseEvent | TouchEvent) => { + const onTrackMouseUpHandler = (event: MouseEvent | TouchEvent) => { const percent = calculatePercent(trackRef.current, event, isVertical) emitChange(event, percentToValue(percent, min, max, isReverse)) @@ -36,7 +36,6 @@ export function useSliderEvents() { setThumbIndex(parseFloat(target.dataset.index)) setThumbState('activated') - setShouldAnimate(false) if (typeof onDragStart === 'function') { dispatchCustomElementEvent(allProps, 'onDragStart', { @@ -46,16 +45,10 @@ export function useSliderEvents() { if (typeof document !== 'undefined') { try { - document.body.addEventListener( - 'touchmove', - onTrackTouchMoveHandler - ) - document.body.addEventListener('touchend', onTrackTouchEndHandler) - document.body.addEventListener( - 'mousemove', - onTrackMouseMoveHandler - ) - document.body.addEventListener('mouseup', onTrackMouseUpHandler) + document.body.addEventListener('touchmove', onBodyMouseMoveHandler) + document.body.addEventListener('touchend', onBodyMouseUpHandler) + document.body.addEventListener('mousemove', onBodyMouseMoveHandler) + document.body.addEventListener('mouseup', onBodyMouseUpHandler) } catch (e) { warn(e) } @@ -66,32 +59,26 @@ export function useSliderEvents() { setThumbState('released') } - const onTrackTouchEndHandler = (event: TouchEvent) => - onTrackMouseUpHandler(event) - const removeEvents = () => { if (typeof document !== 'undefined') { try { document.body.removeEventListener( 'touchmove', - onTrackTouchMoveHandler - ) - document.body.removeEventListener( - 'touchend', - onTrackTouchEndHandler + onBodyMouseMoveHandler ) + document.body.removeEventListener('touchend', onBodyMouseUpHandler) document.body.removeEventListener( 'mousemove', - onTrackMouseMoveHandler + onBodyMouseMoveHandler ) - document.body.removeEventListener('mouseup', onTrackMouseUpHandler) + document.body.removeEventListener('mouseup', onBodyMouseUpHandler) } catch (e) { warn(e) } } } - const onTrackMouseUpHandler = (event: MouseEvent | TouchEvent) => { + const onBodyMouseUpHandler = (event: MouseEvent | TouchEvent) => { removeEvents() setThumbIndex(-1) @@ -104,9 +91,7 @@ export function useSliderEvents() { } } - const onTrackTouchMoveHandler = (event: MouseEvent) => - onTrackMouseMoveHandler(event) - const onTrackMouseMoveHandler = (event: MouseEvent) => { + const onBodyMouseMoveHandler = (event: MouseEvent) => { event.preventDefault() // ensures correct cursor in Safari (dekstop) let elem = trackRef.current @@ -123,6 +108,8 @@ export function useSliderEvents() { const percent = calculatePercent(elem, event, isVertical) emitChange(event, percentToValue(percent, min, max, isReverse)) } + + setShouldAnimate(false) } const onThumbFocusHandler = () => { @@ -152,7 +139,7 @@ export function useSliderEvents() { return { onThumbMouseDownHandler, onThumbMouseUpHandler, - onTrackClickHandler, + onTrackMouseUpHandler, onThumbFocusHandler, onThumbBlurHandler, onHelperChangeHandler, diff --git a/packages/dnb-eufemia/src/components/slider/stories/Slider.stories.tsx b/packages/dnb-eufemia/src/components/slider/stories/Slider.stories.tsx index 0c0ecaea3ce..052cfaf9376 100644 --- a/packages/dnb-eufemia/src/components/slider/stories/Slider.stories.tsx +++ b/packages/dnb-eufemia/src/components/slider/stories/Slider.stories.tsx @@ -8,6 +8,7 @@ import { Wrapper, Box } from 'storybook-utils/helpers' import styled from '@emotion/styled' import { Slider, ToggleButton, Input, FormRow, FormLabel } from '../../' +import { format } from '../../number-format/NumberUtils' export default { title: 'Eufemia/Components/Slider', @@ -49,15 +50,16 @@ export function MultiButtons() { multiThumbBehavior="push" // multiThumbBehavior="omit" // vertical - reverse + // reverse value={value} min={100} max={1000} step={10} stretch - numberFormat={{ decimals: 2, currency: true }} + numberFormat={(value) => format(value, { currency: 'USD' })} + tooltip onChange={({ value, number }) => { - console.log('onChange:', value, number) + // console.log('onChange:', value, number) setValue(value as Array) }} /> @@ -81,8 +83,9 @@ export function MultiButtons() { currency: 'EUR', currency_display: 'symbol', }} + tooltip onChange={({ value, number }) => { - console.log('onChange:', value, number) + // console.log('onChange:', value, number) setValueSecond(value as number) }} /> @@ -109,7 +112,7 @@ const SliderStory = () => { max={100} value={value} step={0.05} - numberFormat={{ decimals: 2, currency: true }} + numberFormat={{ currency: true }} // reverse onChange={({ value, number, rawValue }) => { console.log('onChange:', { value, number, rawValue }) diff --git a/packages/dnb-eufemia/src/components/slider/types.ts b/packages/dnb-eufemia/src/components/slider/types.ts index d76ce1bc690..dddfeaaa590 100644 --- a/packages/dnb-eufemia/src/components/slider/types.ts +++ b/packages/dnb-eufemia/src/components/slider/types.ts @@ -8,6 +8,9 @@ import type { import { IncludeSnakeCase } from '../../shared/helpers/withSnakeCaseProps' export type ValueTypes = number | Array +export type NumberFormatTypes = + | formatOptionParams + | ((value: number) => unknown) export type onChangeEventProps = { value: ValueTypes rawValue: number @@ -74,8 +77,14 @@ export type SliderProps = IncludeSnakeCase<{ /** if set to `true`, then the slider will be 100% in `width`. */ stretch?: boolean - /** Will extend the return object with a `number` property (from `onChange` event). You can use all the options from the [NumberFormat](/uilib/components/number-format/properties) component. It also will use that formatted number in the increase/decrease buttons. If it has to represent a currency, then use e.g. `numberFormat={{ currency: true, decimals: 0 }}` */ - numberFormat?: formatOptionParams + /** provide a function callback or use the options from the [NumberFormat](/uilib/components/number-format/properties) component. It will show a formatted number in the Tooltip (`tooltip={true}`) and enhance the screen reader UX. It will also extend the `onChange` event return object with a formatted `number` property. */ + numberFormat?: NumberFormatTypes + + /** use `true` to show a tooltip on `mouseOver`, `touchStart` and `focus`, showing the current number (if `numberFormat` is given) or the raw value. Defaults to `null`. */ + tooltip: boolean + + /** use `true` to always show the tooltip, in addition to the `tooltip` property. */ + alwaysShowTooltip: boolean /** removes the helper buttons. Defaults to `false`. */ hideButtons?: boolean