Skip to content

Commit

Permalink
Refactor OverflowMenu and InteractionHelp with Floating UI
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed May 21, 2024
1 parent e1ee955 commit aa97ada
Show file tree
Hide file tree
Showing 18 changed files with 249 additions and 244 deletions.
4 changes: 2 additions & 2 deletions apps/storybook/src/Customization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
24 changes: 4 additions & 20 deletions packages/lib/src/toolbar/OverflowMenu.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
93 changes: 58 additions & 35 deletions packages/lib/src/toolbar/OverflowMenu.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,53 +23,67 @@ function OverflowMenu(props: PropsWithChildren<Props>) {
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<HTMLButtonElement>({
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;
}

return (
<>
<Separator />
<div ref={rootRef} className={styles.root}>
<button
className={toolbarStyles.btn}
type="button"
aria-label="More controls"
aria-haspopup="menu"
aria-controls="more-menu"
aria-expanded={isOverflowMenuOpen}
onClick={toggleOverflowMenu}
>
<span className={toolbarStyles.btnLike}>
<FiMenu className={toolbarStyles.icon} />
</span>
</button>

<button
ref={refs.setReference}
id={referenceId}
className={toolbarStyles.btn}
type="button"
aria-label="More controls"
aria-haspopup="dialog"
aria-expanded={isOpen || undefined}
aria-controls={(isOpen && context.floatingId) || undefined}
{...getReferenceProps()}
>
<span className={toolbarStyles.btnLike}>
<FiMenu className={toolbarStyles.icon} />
</span>
</button>

{isOpen && (
<div
id="more-menu"
className={styles.menu}
role="menu"
hidden={!isOverflowMenuOpen}
ref={refs.setFloating}
id={context.floatingId}
className={styles.popup}
style={{
...floatingStyles,
overflow: 'visible', // don't clip nested floating elements
}}
role="dialog"
aria-labelledby={referenceId}
{...getFloatingProps()}
>
<ul className={styles.menuList}>
{validChildren.map((child) => (
// Render cloned child (React elements don't like to be moved around)
<li role="menuitem" key={child.key}>
{cloneElement(child)}
</li>
))}
</ul>
{validChildren.map((child) => (
// Render cloned child (React elements don't like to be moved around)
<div className={styles.control} key={child.key}>
{cloneElement(child)}
</div>
))}
</div>
</div>
)}
</>
);
}
Expand Down
49 changes: 47 additions & 2 deletions packages/lib/src/toolbar/Toolbar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
.selectorWrapper {
display: flex;
}

.option {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -18,7 +17,7 @@ function ColorMapSelector(props: Props) {
const { value, onValueChange, invert, onInversionChange } = props;

return (
<div className={styles.selectorWrapper}>
<>
<Selector
value={value}
onChange={onValueChange}
Expand All @@ -32,7 +31,7 @@ function ColorMapSelector(props: Props) {
value={invert}
onToggle={onInversionChange}
/>
</div>
</>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@
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;
padding-right: 2rem;
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ function DomainWidget(props: Props) {

<div
id={POPUP_ID}
className={styles.popup}
className={styles.popupOuter}
role="dialog"
aria-label="Edit domain"
hidden={!hovered && !isEditing}
>
<div className={styles.popupInner}>
<div className={styles.popup}>
{histogram && (
<Histogram
dataDomain={dataDomain}
Expand Down
22 changes: 12 additions & 10 deletions packages/lib/src/toolbar/controls/ExportMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { useId, useRef, useState } from 'react';
import { FiDownload } from 'react-icons/fi';
import { MdArrowDropDown } from 'react-icons/md';

import toolbarStyles from '../Toolbar.module.css';
import { useFloatingDismiss } from './hooks';
import styles from './Selector/Selector.module.css';
import { download, floatingMinWidth } from './utils';

const PLACEMENTS = {
Expand Down Expand Up @@ -71,26 +71,28 @@ function ExportMenu(props: Props) {
<button
ref={refs.setReference}
id={referenceId}
className={styles.btn}
className={toolbarStyles.btn}
type="button"
disabled={availableEntries.length === 0}
aria-haspopup="menu"
aria-expanded={isOpen || undefined}
aria-controls={context.floatingId}
aria-controls={(isOpen && context.floatingId) || undefined}
{...getReferenceProps()}
>
<span className={styles.btnLike}>
<FiDownload className={styles.icon} />
<span className={styles.label}>Export{isSlice && ' slice'}</span>
<MdArrowDropDown className={styles.arrowIcon} />
<span className={toolbarStyles.btnLike}>
<FiDownload className={toolbarStyles.icon} />
<span className={toolbarStyles.label}>
Export{isSlice && ' slice'}
</span>
<MdArrowDropDown className={toolbarStyles.arrowIcon} />
</span>
</button>

{isOpen && (
<div
ref={refs.setFloating}
id={context.floatingId}
className={styles.menu}
className={toolbarStyles.menu}
style={floatingStyles}
role="menu"
aria-labelledby={referenceId}
Expand All @@ -107,7 +109,7 @@ function ExportMenu(props: Props) {
ref={(node) => {
listRef.current[index] = node;
}}
className={styles.btnOption}
className={toolbarStyles.btnOption}
type="button"
tabIndex={isActive ? 0 : -1}
data-active={isActive || undefined}
Expand All @@ -118,7 +120,7 @@ function ExportMenu(props: Props) {
},
})}
>
<span className={styles.label}>
<span className={toolbarStyles.label}>
Export to {format.toUpperCase()}
</span>
</button>
Expand Down
15 changes: 10 additions & 5 deletions packages/lib/src/toolbar/controls/InteractionHelp.module.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
.list {
margin: 0;
padding: 0.25rem 0.5rem;
list-style-type: none;
}

.entry {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0.25rem 0.25rem;
padding: 0.375rem 0;
white-space: nowrap;
}

.shortcut {
background-color: var(--h5w-interactionHelp-shortcut--bgColor, lightgray);
color: var(--h5w-interactionHelp-shortcut--color, inherit);
margin-left: 1rem;
padding: 0.25rem;
background-color: var(--h5w-interactionHelp-shortcut--bgColor, lightgray);
border-radius: 0.25rem;
flex-grow: 0;
margin-left: 1rem;
color: var(--h5w-interactionHelp-shortcut--color, inherit);
}
Loading

0 comments on commit aa97ada

Please sign in to comment.