Skip to content

Commit

Permalink
Refactor Export menu with Floating UI
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed May 14, 2024
1 parent 3c4240f commit 960d998
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 99 deletions.
1 change: 1 addition & 0 deletions packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
}
},
"dependencies": {
"@floating-ui/react": "0.26.14",
"@react-hookz/web": "24.0.4",
"@visx/axis": "3.10.1",
"@visx/drag": "3.3.0",
Expand Down
66 changes: 0 additions & 66 deletions packages/lib/src/toolbar/controls/ExportEntry.tsx

This file was deleted.

130 changes: 106 additions & 24 deletions packages/lib/src/toolbar/controls/ExportMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,127 @@
import ram from 'react-aria-menubutton'; // CJS
import {
autoUpdate,
FloatingFocusManager,
offset,
useClick,
useDismiss,
useFloating,
useInteractions,
useListNavigation,
} from '@floating-ui/react';
import { assertDefined } from '@h5web/shared/guards';
import { useToggle } from '@react-hookz/web';
import { useRef, useState } from 'react';
import { FiDownload } from 'react-icons/fi';
import { MdArrowDropDown } from 'react-icons/md';

import type { ExportEntryProps } from './ExportEntry';
import ExportEntry from './ExportEntry';
import styles from './Selector/Selector.module.css';
import { download } from './utils';

const { Button, Menu, Wrapper } = ram;
const PLACEMENTS = {
center: 'bottom',
left: 'bottom-start',
right: 'bottom-end',
} as const;

interface ExportEntry {
format: string;
url: URL | (() => Promise<URL | Blob>) | undefined;
}

interface Props {
entries: ExportEntryProps[];
entries: ExportEntry[];
isSlice?: boolean;
align?: 'center' | 'left' | 'right';
align?: keyof typeof PLACEMENTS;
}

function ExportMenu(props: Props) {
const { entries, isSlice, align = 'center' } = props;
const availableEntries = entries.filter(({ url }) => !!url);

const [isOpen, toggle] = useToggle();
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const listRef = useRef<(HTMLButtonElement | null)[]>([]);

const { refs, floatingStyles, context } = useFloating<HTMLButtonElement>({
open: isOpen,
placement: PLACEMENTS[align],
middleware: [offset(6)],
onOpenChange: toggle,
whileElementsMounted: autoUpdate,
});

const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions(
[
useClick(context),
useDismiss(context),
useListNavigation(context, {
listRef,
activeIndex,
loop: true,
focusItemOnHover: false,
onNavigate: setActiveIndex,
}),
],
);

return (
<Wrapper className={styles.wrapper}>
<Button
<>
<button
ref={refs.setReference}
className={styles.btn}
tag="button"
disabled={!entries.some(({ url }) => !!url)}
type="button"
disabled={availableEntries.length === 0}
{...getReferenceProps()}
>
<div className={styles.btnLike}>
<span className={styles.btnLike}>
<FiDownload className={styles.icon} />
<span className={styles.selectedOption}>
Export{isSlice && ' slice'}
</span>
<span className={styles.label}>Export{isSlice && ' slice'}</span>
<MdArrowDropDown className={styles.arrowIcon} />
</div>
</Button>
<Menu className={styles.menu} data-align={align}>
<div className={styles.list}>
{entries.map((entry) => (
<ExportEntry key={entry.format} {...entry} />
))}
</div>
</Menu>
</Wrapper>
</span>
</button>

{isOpen && (
<FloatingFocusManager
context={context}
initialFocus={-1}
modal={false}
guards={false}
>
<div
ref={refs.setFloating}
className={styles.exportMenu}
style={floatingStyles}
{...getFloatingProps()}
role="menu"
>
{availableEntries.map((entry, index) => {
const { format, url } = entry;
const isActive = activeIndex === index;
assertDefined(url);

return (
<button
key={format}
ref={(node) => {
listRef.current[index] = node;
}}
className={styles.btnOption}
{...getItemProps()}
type="button"
tabIndex={isActive ? 0 : -1}
data-active={isActive || undefined}
onClick={() => void download(url, `data.${format}`)}
>
<span className={styles.label}>
Export to {format.toUpperCase()}
</span>
</button>
);
})}
</div>
</FloatingFocusManager>
)}
</>
);
}

Expand Down
26 changes: 17 additions & 9 deletions packages/lib/src/toolbar/controls/Selector/Selector.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@
transform: rotate(180deg);
}

.exportMenu {
display: flex;
flex-direction: column;
padding: 0.25rem 0;
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;
}

.menu {
position: absolute;
top: calc(100% + 0.75rem);
Expand Down Expand Up @@ -95,28 +105,26 @@
}

.option {
flex: 1;
padding: 0.5rem 0.75rem;
cursor: pointer;
white-space: nowrap;
}

.linkOption {
composes: btn from '../../utils.module.css';
composes: option;
padding: 0.5rem 0.75rem;
}

.option:hover,
.option:focus {
.option[data-active] {
background-color: var(--h5w-selector-option-hover--bgColor, whitesmoke);
}

.option:focus {
.option:focus-visible {
outline: 1px solid var(--h5w-selector-option-focus--outlineColor, gray);
outline-offset: -1px;
}

.option[data-selected] {
background-color: var(--h5w-selector-option-selected--bgColor, #eee);
}

.btnOption {
composes: btn from '../../utils.module.css';
composes: option;
}
17 changes: 17 additions & 0 deletions packages/lib/src/toolbar/controls/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export async function download(
url: URL | (() => Promise<URL | Blob>),
filename: string,
) {
const urlOrBlob = url instanceof URL ? url : await url();

const anchor = document.createElement('a');
anchor.download = filename;
document.body.append(anchor);

anchor.href =
urlOrBlob instanceof Blob ? URL.createObjectURL(urlOrBlob) : urlOrBlob.href;
anchor.click();

URL.revokeObjectURL(anchor.href);
anchor.remove();
}
54 changes: 54 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 960d998

Please sign in to comment.