From e5a0bdee825a2769ed17c2eef1bfa5aaf8ab7469 Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Wed, 21 Jun 2023 15:07:20 +0200 Subject: [PATCH 1/2] Rename `DomainTooltip` to `DomainControls` and move tooltip logic out --- .../DomainWidget/BoundEditor.module.css | 2 +- ...p.module.css => DomainControls.module.css} | 17 --- .../controls/DomainWidget/DomainControls.tsx | 128 ++++++++++++++++ .../controls/DomainWidget/DomainTooltip.tsx | 143 ------------------ .../DomainWidget/DomainWidget.module.css | 17 +++ .../controls/DomainWidget/DomainWidget.tsx | 94 +++++++----- .../controls/DomainWidget/ErrorMessage.tsx | 2 +- 7 files changed, 201 insertions(+), 202 deletions(-) rename packages/lib/src/toolbar/controls/DomainWidget/{DomainTooltip.module.css => DomainControls.module.css} (72%) create mode 100644 packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx delete mode 100644 packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.tsx diff --git a/packages/lib/src/toolbar/controls/DomainWidget/BoundEditor.module.css b/packages/lib/src/toolbar/controls/DomainWidget/BoundEditor.module.css index ad8f553e9..17efc29f8 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/BoundEditor.module.css +++ b/packages/lib/src/toolbar/controls/DomainWidget/BoundEditor.module.css @@ -57,5 +57,5 @@ } .actionBtn { - composes: actionBtn from './DomainTooltip.module.css'; + composes: actionBtn from './DomainControls.module.css'; } diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.module.css b/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.module.css similarity index 72% rename from packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.module.css rename to packages/lib/src/toolbar/controls/DomainWidget/DomainControls.module.css index 1fa009d6b..a27a175be 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.module.css +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.module.css @@ -1,20 +1,3 @@ -.tooltip { - composes: popup from '../../Toolbar.module.css'; - z-index: 2; /* above overflow and selector menus */ - /* Add invisible padding around tooltip to extend hover area */ - /* (especially for when enabling auto-scaling hides an error message). */ - padding-left: 2rem; - padding-right: 2rem; - padding-bottom: 2rem; -} - -.tooltipInner { - composes: popupInner from '../../Toolbar.module.css'; - padding: 1rem 0.375rem 1rem 0.75rem; - display: flex; - align-items: center; -} - .tooltipControls > p { margin-bottom: 0.75rem; } diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx b/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx new file mode 100644 index 000000000..cd5f899a4 --- /dev/null +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx @@ -0,0 +1,128 @@ +import type { Domain } from '@h5web/shared'; +import { formatBound } from '@h5web/shared'; +import { forwardRef, useImperativeHandle, useRef } from 'react'; + +import type { DomainErrors } from '../../../vis/models'; +import { DomainError } from '../../../vis/models'; +import ToggleBtn from '../ToggleBtn'; +import type { BoundEditorHandle } from './BoundEditor'; +import BoundEditor from './BoundEditor'; +import styles from './DomainControls.module.css'; +import ErrorMessage from './ErrorMessage'; + +interface Props { + sliderDomain: Domain; + dataDomain: Domain; + errors: DomainErrors; + isAutoMin: boolean; + isAutoMax: boolean; + onAutoMinToggle: () => void; + onAutoMaxToggle: () => void; + isEditingMin: boolean; + isEditingMax: boolean; + onEditMin: (force: boolean) => void; + onEditMax: (force: boolean) => void; + onChangeMin: (val: number) => void; + onChangeMax: (val: number) => void; + onSwap: () => void; +} + +interface Handle { + cancelEditing: () => void; +} + +const DomainControls = forwardRef((props, ref) => { + const { sliderDomain, dataDomain, errors } = props; + const { isAutoMin, isAutoMax, isEditingMin, isEditingMax } = props; + const { + onAutoMinToggle, + onAutoMaxToggle, + onEditMin, + onEditMax, + onChangeMin, + onChangeMax, + onSwap, + } = props; + + const { minGreater, minError, maxError } = errors; + const minEditorRef = useRef(null); + const maxEditorRef = useRef(null); + + /* Expose `cancelEditing` function to parent component through ref handle so that + editing can be cancelled when the user closes the domain tooltip. */ + useImperativeHandle(ref, () => ({ + cancelEditing: () => { + minEditorRef.current?.cancel(); + maxEditorRef.current?.cancel(); + }, + })); + + return ( +
+ {minGreater && ( + + )} + + {minError && } + + + {maxError && } + +

+ Data range{' '} + + [{' '} + + {formatBound(dataDomain[0])} + {' '} + ,{' '} + + {formatBound(dataDomain[1])} + {' '} + ] + +

+ +

+ Autoscale{' '} + + +

+
+ ); +}); + +DomainControls.displayName = 'DomainControls'; + +export type { Handle as DomainControlsHandle }; +export default DomainControls; diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.tsx b/packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.tsx deleted file mode 100644 index df0f83ee4..000000000 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainTooltip.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import type { Domain } from '@h5web/shared'; -import { formatBound } from '@h5web/shared'; -import type { ReactNode } from 'react'; -import { forwardRef, useImperativeHandle, useRef } from 'react'; - -import type { DomainErrors } from '../../../vis/models'; -import { DomainError } from '../../../vis/models'; -import ToggleBtn from '../ToggleBtn'; -import type { BoundEditorHandle } from './BoundEditor'; -import BoundEditor from './BoundEditor'; -import styles from './DomainTooltip.module.css'; -import ErrorMessage from './ErrorMessage'; - -interface Props { - id: string; - open: boolean; - sliderDomain: Domain; - dataDomain: Domain; - errors: DomainErrors; - isAutoMin: boolean; - isAutoMax: boolean; - onAutoMinToggle: () => void; - onAutoMaxToggle: () => void; - isEditingMin: boolean; - isEditingMax: boolean; - onEditMin: (force: boolean) => void; - onEditMax: (force: boolean) => void; - onChangeMin: (val: number) => void; - onChangeMax: (val: number) => void; - onSwap: () => void; - children?: ReactNode; -} - -interface Handle { - cancelEditing: () => void; -} - -const DomainTooltip = forwardRef((props, ref) => { - const { id, open, sliderDomain, dataDomain, errors, children } = props; - const { isAutoMin, isAutoMax, isEditingMin, isEditingMax } = props; - const { - onAutoMinToggle, - onAutoMaxToggle, - onEditMin, - onEditMax, - onChangeMin, - onChangeMax, - onSwap, - } = props; - - const { minGreater, minError, maxError } = errors; - const minEditorRef = useRef(null); - const maxEditorRef = useRef(null); - - /* Expose `cancelEditing` function to parent component through ref handle so that - editing can be cancelled when the user closes the domain tooltip. */ - useImperativeHandle(ref, () => ({ - cancelEditing: () => { - minEditorRef.current?.cancel(); - maxEditorRef.current?.cancel(); - }, - })); - - return ( - - ); -}); - -DomainTooltip.displayName = 'DomainTooltip'; - -export type { Handle as DomainTooltipHandle }; -export default DomainTooltip; diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css index e65b3fd26..38bc62a9b 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css @@ -19,3 +19,20 @@ .root[data-disabled] > .sliderContainer { opacity: 0.5; /* edit button is itself disabled, so don't fade it more */ } + +.tooltip { + composes: popup from '../../Toolbar.module.css'; + z-index: 2; /* above overflow and selector menus */ + /* Add invisible padding around tooltip to extend hover area */ + /* (especially for when enabling auto-scaling hides an error message). */ + padding-left: 2rem; + padding-right: 2rem; + padding-bottom: 2rem; +} + +.tooltipInner { + composes: popupInner from '../../Toolbar.module.css'; + padding: 1rem 0.375rem 1rem 0.75rem; + display: flex; + align-items: center; +} diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx index 576563a48..b4ea746c4 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx @@ -7,9 +7,9 @@ import { useSafeDomain, useVisDomain } from '../../../vis/heatmap/hooks'; import type { CustomDomain, HistogramParams } from '../../../vis/models'; import Histogram from '../Histogram/Histogram'; import ToggleBtn from '../ToggleBtn'; +import type { DomainControlsHandle } from './DomainControls'; +import DomainControls from './DomainControls'; import DomainSlider from './DomainSlider'; -import type { DomainTooltipHandle } from './DomainTooltip'; -import DomainTooltip from './DomainTooltip'; import styles from './DomainWidget.module.css'; const TOOLTIP_ID = 'domain-tooltip'; @@ -18,9 +18,9 @@ interface Props { dataDomain: Domain; customDomain: CustomDomain; scaleType: ColorScaleType; - onCustomDomainChange: (domain: CustomDomain) => void; histogram?: HistogramParams; disabled?: boolean; + onCustomDomainChange: (domain: CustomDomain) => void; } function DomainWidget(props: Props) { @@ -56,7 +56,7 @@ function DomainWidget(props: Props) { } const rootRef = useRef(null); - const tooltipRef = useRef(null); + const tooltipRef = useRef(null); useClickOutside(rootRef, cancelEditing); useKeyboardEvent('Escape', () => { @@ -106,48 +106,62 @@ function DomainWidget(props: Props) { onToggle={() => toggleEditing(!isEditing)} /> - { - const newMin = isAutoMin ? dataDomain[0] : null; - onCustomDomainChange([newMin, customDomain[1]]); - if (!isAutoMin) { - toggleEditingMin(false); - } - }} - onAutoMaxToggle={() => { - const newMax = isAutoMax ? dataDomain[1] : null; - onCustomDomainChange([customDomain[0], newMax]); - if (!isAutoMax) { - toggleEditingMax(false); - } - }} - isEditingMin={isEditingMin} - isEditingMax={isEditingMax} - onEditMin={toggleEditingMin} - onEditMax={toggleEditingMax} - onChangeMin={(val) => onCustomDomainChange([val, customDomain[1]])} - onChangeMax={(val) => onCustomDomainChange([customDomain[0], val])} - onSwap={() => onCustomDomainChange([customDomain[1], customDomain[0]])} + className={styles.tooltip} + role="dialog" + aria-label="Edit domain" + hidden={!hovered && !isEditing} > - {histogram && ( - + {histogram && ( + + onCustomDomainChange([val, customDomain[1]]) + } + onChangeMax={(val) => + onCustomDomainChange([customDomain[0], val]) + } + {...histogram} + /> + )} + { + const newMin = isAutoMin ? dataDomain[0] : null; + onCustomDomainChange([newMin, customDomain[1]]); + if (!isAutoMin) { + toggleEditingMin(false); + } + }} + onAutoMaxToggle={() => { + const newMax = isAutoMax ? dataDomain[1] : null; + onCustomDomainChange([customDomain[0], newMax]); + if (!isAutoMax) { + toggleEditingMax(false); + } + }} + isEditingMin={isEditingMin} + isEditingMax={isEditingMax} + onEditMin={toggleEditingMin} + onEditMax={toggleEditingMax} onChangeMin={(val) => onCustomDomainChange([val, customDomain[1]])} onChangeMax={(val) => onCustomDomainChange([customDomain[0], val])} - {...histogram} + onSwap={() => + onCustomDomainChange([customDomain[1], customDomain[0]]) + } /> - )} - + + ); } diff --git a/packages/lib/src/toolbar/controls/DomainWidget/ErrorMessage.tsx b/packages/lib/src/toolbar/controls/DomainWidget/ErrorMessage.tsx index 6bed2fbb7..ab4f4ec38 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/ErrorMessage.tsx +++ b/packages/lib/src/toolbar/controls/DomainWidget/ErrorMessage.tsx @@ -2,7 +2,7 @@ import { FiCornerDownRight } from 'react-icons/fi'; import { MdSwapVert } from 'react-icons/md'; import { DomainError } from '../../../vis/models'; -import styles from './DomainTooltip.module.css'; +import styles from './DomainControls.module.css'; const ERRORS = { [DomainError.MinGreater]: { From 86bad09b5c878590980e5860c24b08021dc933cf Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Wed, 19 Jul 2023 14:09:08 +0200 Subject: [PATCH 2/2] Export and document `DomainControls` --- apps/storybook/src/DomainControls.stories.tsx | 73 +++++++++++++++++++ packages/lib/src/index.ts | 3 + .../controls/DomainWidget/DomainControls.tsx | 1 + 3 files changed, 77 insertions(+) create mode 100644 apps/storybook/src/DomainControls.stories.tsx diff --git a/apps/storybook/src/DomainControls.stories.tsx b/apps/storybook/src/DomainControls.stories.tsx new file mode 100644 index 000000000..26dc9abdb --- /dev/null +++ b/apps/storybook/src/DomainControls.stories.tsx @@ -0,0 +1,73 @@ +import { DomainControls } from '@h5web/lib'; +import type { Domain } from '@h5web/shared'; +import { useToggle } from '@react-hookz/web'; +import type { Meta, StoryObj } from '@storybook/react'; +import { useEffect, useState } from 'react'; + +const meta = { + title: 'Toolbar/DomainControls', + component: DomainControls, + argTypes: { + sliderDomain: { control: false }, + errors: { control: false }, + isAutoMin: { control: false }, + isAutoMax: { control: false }, + isEditingMin: { control: false }, + isEditingMax: { control: false }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default = { + render: (args) => { + const { dataDomain } = args; + + const [sliderDomain, setSliderDomain] = useState(dataDomain); + const [isAutoMin, toggleAutoMin] = useToggle(); + const [isAutoMax, toggleAutoMax] = useToggle(); + const [isEditingMin, toggleEditingMin] = useToggle(); + const [isEditingMax, toggleEditingMax] = useToggle(); + + useEffect(() => { + setSliderDomain(dataDomain); + }, [dataDomain]); + + return ( +
+ { + toggleAutoMin(); + if (!isAutoMin) { + setSliderDomain([dataDomain[0], sliderDomain[1]]); + toggleEditingMin(false); + } + }} + onAutoMaxToggle={() => { + toggleAutoMax(); + if (!isAutoMax) { + setSliderDomain([sliderDomain[0], dataDomain[1]]); + toggleEditingMax(false); + } + }} + isEditingMin={isEditingMin} + isEditingMax={isEditingMax} + onEditMin={toggleEditingMin} + onEditMax={toggleEditingMax} + onChangeMin={(val) => setSliderDomain([val, sliderDomain[1]])} + onChangeMax={(val) => setSliderDomain([sliderDomain[0], val])} + onSwap={() => setSliderDomain([sliderDomain[1], sliderDomain[0]])} + /> +
+ ); + }, + args: { + dataDomain: [4, 400], + }, +} satisfies Story; diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index c79c83638..f700f2655 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -17,6 +17,7 @@ export { default as ToggleBtn } from './toolbar/controls/ToggleBtn'; export { default as ToggleGroup } from './toolbar/controls/ToggleGroup'; export { default as DomainWidget } from './toolbar/controls/DomainWidget/DomainWidget'; export { default as DomainSlider } from './toolbar/controls/DomainWidget/DomainSlider'; +export { default as DomainControls } from './toolbar/controls/DomainWidget/DomainControls'; export { default as ColorMapSelector } from './toolbar/controls/ColorMapSelector/ColorMapSelector'; export { default as ScaleSelector } from './toolbar/controls/ScaleSelector/ScaleSelector'; export { default as GridToggler } from './toolbar/controls/GridToggler'; @@ -30,6 +31,8 @@ export { default as Histogram } from './toolbar/controls/Histogram/Histogram'; export type { ToolbarProps } from './toolbar/Toolbar'; export type { DomainWidgetProps } from './toolbar/controls/DomainWidget/DomainWidget'; export type { DomainSliderProps } from './toolbar/controls/DomainWidget/DomainSlider'; +export type { DomainControlsHandle } from './toolbar/controls/DomainWidget/DomainControls'; +export type { DomainControlsProps } from './toolbar/controls/DomainWidget/DomainControls'; export type { HistogramProps } from './toolbar/controls/Histogram/Histogram'; // Building blocks diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx b/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx index cd5f899a4..c45aa3de7 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainControls.tsx @@ -125,4 +125,5 @@ const DomainControls = forwardRef((props, ref) => { DomainControls.displayName = 'DomainControls'; export type { Handle as DomainControlsHandle }; +export type { Props as DomainControlsProps }; export default DomainControls;