From aa97adabdff962d9f4d7a97894ec543ad49c30a0 Mon Sep 17 00:00:00 2001 From: Axel Bocciarelli Date: Tue, 21 May 2024 11:04:51 +0200 Subject: [PATCH] Refactor `OverflowMenu` and `InteractionHelp` with Floating UI --- apps/storybook/src/Customization.mdx | 4 +- .../lib/src/toolbar/OverflowMenu.module.css | 24 +---- packages/lib/src/toolbar/OverflowMenu.tsx | 93 ++++++++++++------- packages/lib/src/toolbar/Toolbar.module.css | 49 +++++++++- .../ColorMapSelector.module.css | 5 +- .../ColorMapSelector/ColorMapSelector.tsx | 5 +- .../DomainWidget/DomainWidget.module.css | 13 ++- .../controls/DomainWidget/DomainWidget.tsx | 4 +- .../lib/src/toolbar/controls/ExportMenu.tsx | 22 +++-- .../controls/InteractionHelp.module.css | 15 ++- .../src/toolbar/controls/InteractionHelp.tsx | 90 ++++++++++++------ .../ScaleSelector/ScaleOption.module.css | 4 - .../controls/ScaleSelector/ScaleOption.tsx | 11 ++- .../src/toolbar/controls/Selector/Option.tsx | 4 +- .../controls/Selector/Selector.module.css | 87 ++--------------- .../toolbar/controls/Selector/Selector.tsx | 51 +++++----- .../src/toolbar/controls/Selector/models.ts | 5 +- packages/lib/src/toolbar/utils.module.css | 7 -- 18 files changed, 249 insertions(+), 244 deletions(-) delete mode 100644 packages/lib/src/toolbar/controls/ScaleSelector/ScaleOption.module.css diff --git a/apps/storybook/src/Customization.mdx b/apps/storybook/src/Customization.mdx index 004a3c410..676c8731f 100644 --- a/apps/storybook/src/Customization.mdx +++ b/apps/storybook/src/Customization.mdx @@ -135,13 +135,13 @@ as you see fit. For instance, if you'd like to change the color of the curve of | `--h5w-btnRaised--shadowColor` | `gray` | Box shadow color of raised buttons | | `--h5w-btnRaised-hover--shadowColor` | `dimgray` | Box shadow color of raised buttons on hover | -##### Selectors +##### Selectors & menus | Name | Default | Description | | ------------------------------------------- | -------------------------------------------- | ------------------------------------ | | `--h5w-selector-label--color` | `var(--h5w-toolbar-label--color, royalblue)` | Text color of label | -| `--h5w-selector-menu--bgColor` | `white` | Background color of menu | | `--h5w-selector-arrowIcon--color` | `dimgray` | Color of arrow icon | +| `--h5w-selector-menu--bgColor` | `white` | Background color of menus | | `--h5w-selector-option-hover--bgColor` | `whitesmoke` | Background color of options on hover | | `--h5w-selector-option-selected--bgColor` | `#eee` | Background color of selected option | | `--h5w-selector-option-focus--outlineColor` | `gray` | Outline color of focused option | diff --git a/packages/lib/src/toolbar/OverflowMenu.module.css b/packages/lib/src/toolbar/OverflowMenu.module.css index e94bca2cf..018dc8475 100644 --- a/packages/lib/src/toolbar/OverflowMenu.module.css +++ b/packages/lib/src/toolbar/OverflowMenu.module.css @@ -1,26 +1,10 @@ -.root { - position: relative; - display: flex; -} - -.menu { +.popup { composes: popup from './utils.module.css'; - right: 0.25rem; -} - -.menuList { - composes: popupInner from './utils.module.css'; - display: grid; - grid-template-columns: 1fr; - grid-gap: 0.25rem; - justify-items: flex-start; - margin: 0; - padding: 0.375rem 0.25rem; - list-style-type: none; + padding: 0.25rem; } -.menu > li { +.control { display: flex; justify-content: flex-end; - min-height: 2.25rem; + min-height: calc(var(--h5w-btn--height, 1.875rem) + 0.5rem); } diff --git a/packages/lib/src/toolbar/OverflowMenu.tsx b/packages/lib/src/toolbar/OverflowMenu.tsx index 726b61852..942c318fa 100644 --- a/packages/lib/src/toolbar/OverflowMenu.tsx +++ b/packages/lib/src/toolbar/OverflowMenu.tsx @@ -1,9 +1,18 @@ -import { useClickOutside, useToggle } from '@react-hookz/web'; +import { + autoUpdate, + offset, + shift, + useClick, + useFloating, + useInteractions, +} from '@floating-ui/react'; +import { useToggle } from '@react-hookz/web'; import type { PropsWithChildren } from 'react'; -import { cloneElement, isValidElement, useRef } from 'react'; +import { cloneElement, isValidElement, useId } from 'react'; import { FiMenu } from 'react-icons/fi'; import flattenChildren from 'react-keyed-flatten-children'; +import { useFloatingDismiss } from './controls/hooks'; import styles from './OverflowMenu.module.css'; import Separator from './Separator'; import toolbarStyles from './Toolbar.module.css'; @@ -14,15 +23,22 @@ function OverflowMenu(props: PropsWithChildren) { const { children } = props; const validChildren = flattenChildren(children).filter(isValidElement); - const rootRef = useRef(null); - const [isOverflowMenuOpen, toggleOverflowMenu] = useToggle(false); + const [isOpen, toggle] = useToggle(); + const referenceId = useId(); - useClickOutside(rootRef, () => { - if (isOverflowMenuOpen) { - toggleOverflowMenu(false); - } + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + placement: 'bottom-end', + middleware: [offset(6), shift({ padding: 6 })], + onOpenChange: toggle, + whileElementsMounted: autoUpdate, }); + const { getReferenceProps, getFloatingProps } = useInteractions([ + useClick(context), + useFloatingDismiss(context), + ]); + if (validChildren.length === 0) { return null; } @@ -30,37 +46,44 @@ function OverflowMenu(props: PropsWithChildren) { return ( <> -
- + + + {isOpen && ( -
+ )} ); } diff --git a/packages/lib/src/toolbar/Toolbar.module.css b/packages/lib/src/toolbar/Toolbar.module.css index 1e0b3a108..e2b59caec 100644 --- a/packages/lib/src/toolbar/Toolbar.module.css +++ b/packages/lib/src/toolbar/Toolbar.module.css @@ -52,10 +52,55 @@ composes: label from './utils.module.css'; } +.arrowIcon { + align-self: center; + margin-top: 1px; + margin-left: 1px; + margin-right: -0.25rem; + color: var(--h5w-selector-arrowIcon--color, dimgray); + font-size: 1.25em; +} + +.btn[aria-expanded='true'] .arrowIcon { + transform: rotate(180deg); +} + .popup { composes: popup from './utils.module.css'; } -.popupInner { - composes: popupInner from './utils.module.css'; +.menu { + display: flex; + flex-direction: column; + padding: 0.25rem 0; + overflow: hidden auto; + scrollbar-width: thin; + background-color: var(--h5w-selector-menu--bgColor, white); + box-shadow: + rgba(0, 0, 0, 0.1) 0px 0px 0px 1px, + rgba(0, 0, 0, 0.1) 0px 4px 11px; +} + +.btnOption { + composes: btnClean from global; + display: flex; + align-items: center; + padding: 0.5rem 0.75rem; + line-height: 1.5em; /* same as icon, if any */ + transition: background-color 0.05s ease-in-out; + white-space: nowrap; +} + +.btnOption:hover, +.btnOption[data-active] { + background-color: var(--h5w-selector-option-hover--bgColor, whitesmoke); +} + +.btnOption:focus-visible { + outline: 1px solid var(--h5w-selector-option-focus--outlineColor, gray); + outline-offset: -1px; +} + +.btnOption[aria-selected] { + background-color: var(--h5w-selector-option-selected--bgColor, #eee); } diff --git a/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.module.css b/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.module.css index 54b6f7fa0..5a9f860d2 100644 --- a/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.module.css +++ b/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.module.css @@ -1,8 +1,5 @@ -.selectorWrapper { - display: flex; -} - .option { + flex: 1; display: flex; align-items: center; justify-content: space-between; diff --git a/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.tsx b/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.tsx index 89c358cbb..c0daa44f5 100644 --- a/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.tsx +++ b/packages/lib/src/toolbar/controls/ColorMapSelector/ColorMapSelector.tsx @@ -4,7 +4,6 @@ import type { ColorMap } from '../../../vis/heatmap/models'; import Selector from '../Selector/Selector'; import ToggleBtn from '../ToggleBtn'; import ColorMapOption from './ColorMapOption'; -import styles from './ColorMapSelector.module.css'; import { COLORMAP_GROUPS } from './groups'; interface Props { @@ -18,7 +17,7 @@ function ColorMapSelector(props: Props) { const { value, onValueChange, invert, onInversionChange } = props; return ( -
+ <> -
+ ); } diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css index f2bbd0aea..3f9ec7b42 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.module.css @@ -20,9 +20,12 @@ opacity: 0.5; /* edit button is itself disabled, so don't fade it more */ } -.popup { - composes: popup from '../../utils.module.css'; - z-index: 2; /* above overflow and selector menus */ +.popupOuter { + position: absolute; + z-index: 2; /* above other floating elements (overflow menu, selectors, etc.) */ + bottom: 1px; /* guarantees overlap with toolbar so the popup doesn't close when the pointer moves into it */ + padding-top: 6px; /* matches other floating elements */ + transform: translateY(100%); /* Add invisible padding around popup to extend hover area */ /* (especially for when enabling auto-scaling hides an error message). */ padding-left: 2rem; @@ -30,8 +33,8 @@ padding-bottom: 2rem; } -.popupInner { - composes: popupInner from '../../utils.module.css'; +.popup { + composes: popup from '../../utils.module.css'; display: flex; align-items: center; padding: 1rem 0.375rem 1rem 0.75rem; diff --git a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx index 95f4ffde0..34618fd8c 100644 --- a/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx +++ b/packages/lib/src/toolbar/controls/DomainWidget/DomainWidget.tsx @@ -109,12 +109,12 @@ function DomainWidget(props: Props) {