From 2986ca4ef6c9d4c13aeb2629d6557044f379bf48 Mon Sep 17 00:00:00 2001 From: dleadbetter Date: Mon, 12 Aug 2024 20:26:17 -0400 Subject: [PATCH] RC #291 - Refactoring FacetSlider and FacetTimeline to always require being controlled --- .../core-data/src/components/FacetSlider.js | 249 ++++++++---------- .../core-data/src/components/FacetTimeline.js | 154 ++++++----- .../src/core-data/FacetSlider.stories.js | 79 +++--- .../src/core-data/FacetTimeline.stories.js | 40 +++ 4 files changed, 291 insertions(+), 231 deletions(-) diff --git a/packages/core-data/src/components/FacetSlider.js b/packages/core-data/src/components/FacetSlider.js index ef67c182..45b7fcce 100644 --- a/packages/core-data/src/components/FacetSlider.js +++ b/packages/core-data/src/components/FacetSlider.js @@ -4,18 +4,13 @@ import { useTimer } from '@performant-software/shared-components'; import * as Slider from '@radix-ui/react-slider'; import * as Tooltip from '@radix-ui/react-tooltip'; import { clsx } from 'clsx'; -import { - ChevronLeft, - ChevronRight, - RotateCcw, - ZoomIn, - ZoomOut -} from 'lucide-react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; import React, { useCallback, useEffect, useState } from 'react'; import _ from 'underscore'; type MarkerProps = { className?: string, + position: 'top' | 'bottom' | 'left' | 'right', value: number }; @@ -73,6 +68,7 @@ const SliderMarker = (props: MarkerProps) => { />
{ ); }; +SliderMarker.defaultProps = { + position: 'top' +}; + type Action = { /** * Class name to apply to the button element. @@ -134,17 +134,24 @@ type Props = { /** * The maximum facet value. */ - defaultMax: number, + max?: number, /** * The minimum facet value. */ - defaultMin: number, + min?: number, /** * Callback fired when the range is changed. */ - onChange?: ([[number, number], [number, number]]) => void, + onValueChange: (value: [number, number]) => void, + + onValueCommit?: (value: [number, number]) => void, + + /** + * Position of the value tooltip marker. + */ + position?: 'top' | 'bottom' | 'left' | 'right', /** * Number of steps to increment the slider. @@ -152,31 +159,27 @@ type Props = { step?: number, /** - * Zoom in/out increment. + * Value for controlled input. */ - zoom?: number + value: [number, number] }; const FacetSlider = (props: Props) => { - const [min, setMin] = useState(props.defaultMin); - const [max, setMax] = useState(props.defaultMax); - const [range, setRange] = useState([props.defaultMin, props.defaultMax]); - /** * Callback fired when the left button is clicked. This function decrements the min range value by the "step" prop. * * @type {(function(): void)|*} */ const onLeft = useCallback(() => { - const [start, end] = range; + const [start, end] = props.value; let newStart = start - props.step; - if (newStart < min) { - newStart = min; + if (newStart < props.min) { + newStart = props.min; } - setRange([newStart, end]); - }, [min, range, props.step]); + props.onValueChange([newStart, end]); + }, [props.min, props.onValueChange, props.step, props.value]); /** * Callback fired when the right button is clicked. This function increments the max range value by the "step" prop. @@ -184,80 +187,15 @@ const FacetSlider = (props: Props) => { * @type {(function(): void)|*} */ const onRight = useCallback(() => { - const [start, end] = range; + const [start, end] = props.value; let newEnd = end + props.step; - if (newEnd > max) { - newEnd = max; - } - - setRange([start, newEnd]); - }, [max, range, props.step]); - - /** - * Zooms in the min/max values. - * - * @type {(function(): void)|*} - */ - const onZoomIn = useCallback(() => { - const newRange = [...range]; - - const newMin = min + props.zoom; - const newMax = max - props.zoom; - - if (newMin >= newMax) { - return; - } - - setMin(newMin); - setMax(newMax); - - if (newMin > newRange[0]) { - newRange[0] = newMin; - } - - if (newMax < newRange[1]) { - newRange[1] = newMax; + if (newEnd > props.max) { + newEnd = props.max; } - setRange(newRange); - }, [max, min, range, props.zoom]); - - /** - * Zooms out the min/max values. - * - * @type {(function(): void)|*} - */ - const onZoomOut = useCallback(() => { - const newMin = min - props.zoom; - const newMax = max + props.zoom; - - if (newMin >= newMax) { - return; - } - - setMin(newMin); - setMax(newMax); - }, [max, min, range, props.zoom]); - - /** - * Resets the min/max values to the defaults. - * - * @type {(function(): void)|*} - */ - const onZoomReset = useCallback(() => { - setMin(props.defaultMin); - setMax(props.defaultMax); - }, [props.defaultMax, props.defaultMin]); - - /** - * Calls the onChange prop when the range value changes. - */ - useEffect(() => { - if (props.onChange) { - props.onChange(range, [min, max]); - } - }, [max, min, range]); + props.onValueChange([start, newEnd]); + }, [props.max, props.onValueChange, props.step, props.value]); return ( <> @@ -277,12 +215,13 @@ const FacetSlider = (props: Props) => { 'relative flex flex-grow h-5 touch-none items-center w-full', props.classNames.root )} - max={max} - min={min} + max={props.max} + min={props.min} minStepsBetweenThumbs={1} - onValueChange={setRange} + onValueChange={props.onValueChange} + onValueCommit={props.onValueCommit} step={1} - value={range} + value={props.value} > { - - - { !_.isEmpty(props.actions) && ( - <> - { _.map(props.actions, (action, index) => ( - - ))} - - )} + { _.map(props.actions, (action, index) => ( + + ))}
)} + {/*{ props.zoom && (*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* { !_.isEmpty(props.actions) && (*/} + {/* <>*/} + {/* { _.map(props.actions, (action, index) => (*/} + {/* */} + {/* { action.icon }*/} + {/* */} + {/* ))}*/} + {/* */} + {/* )}*/} + {/* */} + {/*)}*/} ); }; @@ -379,6 +343,7 @@ const FacetSlider = (props: Props) => { FacetSlider.defaultProps = { classNames: {}, step: 1, + value: [] }; export default FacetSlider; diff --git a/packages/core-data/src/components/FacetTimeline.js b/packages/core-data/src/components/FacetTimeline.js index 06dd4755..e59f5cc2 100644 --- a/packages/core-data/src/components/FacetTimeline.js +++ b/packages/core-data/src/components/FacetTimeline.js @@ -4,7 +4,13 @@ import { useTimer } from '@performant-software/shared-components'; import * as Popover from '@radix-ui/react-popover'; import * as Slider from '@radix-ui/react-slider'; import { clsx } from 'clsx'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { + ChevronLeft, + ChevronRight, + RotateCcw, + ZoomIn, + ZoomOut +} from 'lucide-react'; import React, { useCallback, useEffect, @@ -63,19 +69,79 @@ type Props = { const FACET_EVENT_RANGE = 'event_range_facet'; const FacetTimeline = (props: Props) => { - const [defaultMin, setDefaultMin] = useState(); - const [defaultMax, setDefaultMax] = useState(); + const { range = {}, refine, start = [] } = props.useRange({ attribute: FACET_EVENT_RANGE }); + const [events, setEvents] = useState(); - const [max, setMax] = useState(); - const [min, setMin] = useState(); - const [range, setRange] = useState(); + const [defaultMax] = useState(range.max); + const [defaultMin] = useState(range.min); + const [max, setMax] = useState(range.max); + const [min, setMin] = useState(range.min); + const [value, setValue] = useState([min, max]); const EventsService = useEventsService(); + const ref = useRef(); const { clearTimer, setTimer } = useTimer(); - const ref = useRef(); + /** + * Zooms in the min/max values. + * + * @type {(function(): void)|*} + */ + const onZoomIn = useCallback(() => { + const newMin = min + props.zoom; + const newMax = max - props.zoom; + + if (newMin >= newMax) { + return; + } + + setMin(newMin); + setMax(newMax); + }, [max, min, props.zoom]); + + /** + * Zooms out the min/max values. + * + * @type {(function(): void)|*} + */ + const onZoomOut = useCallback(() => { + const newMin = min - props.zoom; + const newMax = max + props.zoom; + + if (newMin >= newMax) { + return; + } + + setMin(newMin); + setMax(newMax); + }, [max, min, range, props.zoom]); + + /** + * Resets the min/max values to the defaults. + * + * @type {(function(): void)|*} + */ + const onZoomReset = useCallback(() => { + setMin(defaultMin); + setMax(defaultMax); + }, [defaultMax, defaultMin]); - const { range: defaultRange, refine } = props.useRange({ attribute: FACET_EVENT_RANGE }); + /** + * List of actions to provide to the FacetSlider component. + */ + const actions = useMemo(() => [{ + label: 'Zoom In', + icon: , + onClick: onZoomIn + }, { + label: 'Zoom Out', + icon: , + onClick: onZoomOut + }, { + label: 'Zoom Reset', + icon: , + onClick: onZoomReset + }], [onZoomIn, onZoomOut, onZoomReset]); /** * Returns the year value for the passed event. @@ -94,17 +160,6 @@ const FacetTimeline = (props: Props) => { return year; }, []); - /** - * Sets the new range and min/max values on the state. - * - * @type {(function(*, [*,*]): void)|*} - */ - const onChange = useCallback((newRange, [newMin, newMax]) => { - setRange(newRange); - setMin(newMin); - setMax(newMax); - }, []); - /** * Sets the events on the state. * @@ -120,13 +175,13 @@ const FacetTimeline = (props: Props) => { /** * Memo-izes the slider value. */ - const value = useMemo(() => _.pluck(events, 'year'), [events]); + const years = useMemo(() => _.pluck(events, 'year'), [events]); /** * Loads the list of events when the range or min/max values are changed. */ useEffect(() => { - if (!range) { + if (!value) { return; } @@ -136,13 +191,10 @@ const FacetTimeline = (props: Props) => { // Reset the timer to fetch the events setTimer(() => ( EventsService - .fetchAll({ min_year: range[0], max_year: range[1] }) + .fetchAll({ min_year: value[0], max_year: value[1] }) .then(onLoad) )); - - // Call the refine function to update search results - refine(range); - }, [max, min, range]); + }, [onLoad, max, min, value]); /** * Calls the onLoad prop when the events are changed. @@ -154,35 +206,16 @@ const FacetTimeline = (props: Props) => { }, [events, props.onLoad]); /** - * Sets the default min/max values based on the facet range. + * When the upper and/or lower bounds of the range change, update the value and min/max values. */ useEffect(() => { - if (!defaultMin && defaultRange?.min) { - setDefaultMin(defaultRange.min); - setMin(defaultRange.min); - } - - if (!defaultMax && defaultRange?.max) { - setDefaultMax(defaultRange.max); - setMax(defaultRange.max); - } - }, [defaultMin, defaultMax, defaultRange]); + const from = Math.max(range.min, Number.isFinite(start[0]) ? start[0] : range.min); + const to = Math.min(range.max, Number.isFinite(start[1]) ? start[1] : range.max); - /** - * Sets the new range value based on the results of the `useRange` hook. - */ - useEffect(() => { - if (defaultRange && range && defaultRange.min !== range[0] && defaultRange.max !== range[1]) { - setRange([defaultRange.min, defaultRange.max]); - } - }, [defaultRange]); - - /** - * Only render if we have a default min/max value. - */ - if (!(defaultMin && defaultMax)) { - return null; - } + setValue([from, to]); + setMax(range.max); + setMin(range.min); + }, [range.min, range.max]); return (
{ className='relative flex flex-grow h-5 touch-none items-center w-full' max={max} min={min} - value={value} + value={years} > { _.map(events, (event) => ( {
); diff --git a/packages/storybook/src/core-data/FacetSlider.stories.js b/packages/storybook/src/core-data/FacetSlider.stories.js index 0061dc8e..5379a6d9 100644 --- a/packages/storybook/src/core-data/FacetSlider.stories.js +++ b/packages/storybook/src/core-data/FacetSlider.stories.js @@ -1,6 +1,7 @@ // @flow -import React from 'react'; +import { action } from '@storybook/addon-actions'; +import React, { useState } from 'react'; import FacetSlider from '../../../core-data/src/components/FacetSlider'; export default { @@ -8,34 +9,52 @@ export default { component: FacetSlider }; -export const Default = () => ( - -); - -export const CustomStyles = () => ( -
+export const Default = () => { + const [value, setValue] = useState([1500, 2010]); + + return ( + + ); +}; + +export const CustomStyles = () => { + const [value, setValue] = useState([1500, 2010]); + + return ( +
+ +
+ ); +}; + +export const TooltipPosition = () => { + const [value, setValue] = useState([1500, 2010]); + + return ( -
-); - -export const Zoom = () => ( - -); + ); +}; diff --git a/packages/storybook/src/core-data/FacetTimeline.stories.js b/packages/storybook/src/core-data/FacetTimeline.stories.js index 5c7329a5..b7276c74 100644 --- a/packages/storybook/src/core-data/FacetTimeline.stories.js +++ b/packages/storybook/src/core-data/FacetTimeline.stories.js @@ -131,3 +131,43 @@ export const Description = withCoreDataContextProvider(() => ( zoom={10} /> )); + +export const MinMax = withCoreDataContextProvider(() => { + const [min, setMin] = useState(1768); + const [max, setMax] = useState(1777); + + const getRandomInt = (min, max) => { + const minCeiled = Math.ceil(min); + const maxFloored = Math.floor(max); + return Math.floor(Math.random() * (maxFloored - minCeiled) + minCeiled); // The maximum is exclusive and the minimum is inclusive + }; + + const onResetRange = useCallback(() => { + const newMin = getRandomInt(1768, 1777); + const newMax = getRandomInt(newMin, 1777); + + setMin(newMin); + setMax(newMax); + }, []); + + const useRangeTest = () => ({ + range: { min, max }, + refine: () => {} + }); + + return ( + <> + + + + ); +});