Skip to content

Commit

Permalink
fix: menu positioning (#154)
Browse files Browse the repository at this point in the history
- flip menu above control when at bottom of parent
- shift menu sideways to avoid appearing outside the viewport
  • Loading branch information
uipoet authored Jan 24, 2022
1 parent cc44a01 commit 9c95adf
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 67 deletions.
76 changes: 76 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"dependencies": {
"@floating-ui/react-dom": "0.4.3",
"react-icons": "4.3.1"
},
"devDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions src/docs/pages/examples/button-menu/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ export function InputMenuState({
setState((state) => ({ ...state, appearance }))
}
options={[
{ label: "Elevated", value: "elevated" },
{ label: "Filled", value: "filled" },
{ label: "Tonal", value: "tonal" },
{ label: "Outlined", value: "outlined" },
{ label: "Text", value: "text" },
]}
/>
<Checkbox
Expand Down
1 change: 1 addition & 0 deletions src/docs/pages/overview/dojo.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

.card > section > h1,
.card > section > h2 {
margin: 0;
transition-duration: var(--nk-transition-duration) font-size linear;
}

Expand Down
45 changes: 20 additions & 25 deletions src/lib/components/menu/button.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,32 @@
import { Button } from "ninjakit";
import { ForwardedRef, forwardRef } from "react";
import { MdArrowDropDown, MdArrowDropUp } from "react-icons/md";

import type { ButtonProps } from "../button";
import { Options, useMenu } from ".";
import { Menu } from "./menu";
import styles from "./menu.module.css";

declare module "react" {
function forwardRef<T, P>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}

export const ButtonMenu = forwardRef(function ButtonMenu<T extends string>(
{
className: override,
id,
onChange,
options,
...props
}: Omit<JSX.IntrinsicElements["button"], "id" | "onChange" | "value"> &
ButtonProps & {
id: string;
onChange: (value: T) => void;
options: Options<T>;
},
ref: ForwardedRef<HTMLButtonElement>
) {
export function ButtonMenu<T extends string>({
className: override,
id,
onChange,
options,
...props
}: Omit<JSX.IntrinsicElements["button"], "id" | "onChange" | "value"> &
ButtonProps & {
id: string;
onChange: (value: T) => void;
options: Options<T>;
}) {
const {
className,
expanded,
handleClickControl,
handleKeyDownControl,
menuId,
refControl,
refMenu,
style,
setExpanded,
} = useMenu({ id, override });

Expand All @@ -48,17 +41,19 @@ export const ButtonMenu = forwardRef(function ButtonMenu<T extends string>(
id={id}
onClick={handleClickControl}
onKeyDown={handleKeyDownControl}
ref={ref}
ref={refControl}
trailingIcon={expanded ? <MdArrowDropUp /> : <MdArrowDropDown />}
/>
{expanded && (
<Menu
<Menu<T>
menuId={menuId}
onChange={onChange}
options={options}
ref={refMenu}
setExpanded={setExpanded}
style={style}
/>
)}
</fieldset>
);
});
}
40 changes: 39 additions & 1 deletion src/lib/components/menu/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { KeyboardEventHandler, ReactNode, useState } from "react";
import {
flip,
getScrollParents,
shift,
useFloating,
} from "@floating-ui/react-dom";
import { KeyboardEventHandler, ReactNode, useEffect, useState } from "react";

import { firstHTMLElementChild } from "../../util";
import styles from "./menu.module.css";
Expand Down Expand Up @@ -34,6 +40,31 @@ export function useMenu({
override,
].join(" ");
const [expanded, setExpanded] = useState(false);
const { x, y, reference, floating, refs, strategy, update } = useFloating({
middleware: [flip(), shift()],
});
useEffect(() => {
if (!refs.reference.current || !refs.floating.current) {
return;
}

const parents = [
...getScrollParents(refs.reference.current),
...getScrollParents(refs.floating.current),
];

parents.forEach((parent) => {
parent.addEventListener("scroll", update);
parent.addEventListener("resize", update);
});

return () => {
parents.forEach((parent) => {
parent.removeEventListener("scroll", update);
parent.removeEventListener("resize", update);
});
};
}, [refs.reference, refs.floating, update]);
const handleClickControl = () => setExpanded(!expanded);
const handleKeyDownControl: KeyboardEventHandler = (event) => {
const element = input
Expand Down Expand Up @@ -72,7 +103,14 @@ export function useMenu({
expanded,
handleClickControl,
handleKeyDownControl,
style: {
left: x ?? "",
position: strategy,
top: y ?? "",
},
menuId,
refControl: reference,
refMenu: floating,
setExpanded,
};
}
Expand Down
47 changes: 21 additions & 26 deletions src/lib/components/menu/input.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,34 @@
import { TextInput } from "ninjakit";
import { ForwardedRef, forwardRef } from "react";
import { MdArrowDropDown, MdArrowDropUp } from "react-icons/md";

import type { InputProps } from "../input";
import { Options, useMenu } from ".";
import { Menu } from "./menu";
import styles from "./menu.module.css";

declare module "react" {
function forwardRef<T, P>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
}

export const InputMenu = forwardRef(function InputMenu<T extends string>(
{
className: override,
flex,
id,
onChange,
options,
readOnly = true,
...props
}: Omit<JSX.IntrinsicElements["input"], "onChange"> &
InputProps & {
onChange: (value: T) => void;
options: Options<T>;
},
ref: ForwardedRef<HTMLInputElement>
) {
export function InputMenu<T extends string>({
className: override,
flex,
id,
onChange,
options,
readOnly = true,
...props
}: Omit<JSX.IntrinsicElements["input"], "onChange"> &
InputProps & {
onChange: (value: T) => void;
options: Options<T>;
}) {
const {
className,
expanded,
handleClickControl,
handleKeyDownControl,
menuId,
refControl,
refMenu,
setExpanded,
style,
} = useMenu({ flex, id, input: true, override });

return (
Expand All @@ -52,18 +45,20 @@ export const InputMenu = forwardRef(function InputMenu<T extends string>(
onClickTrailingIcon={handleClickControl}
onKeyDown={handleKeyDownControl}
readOnly={readOnly}
ref={ref}
ref={refControl}
trailingIcon={expanded ? <MdArrowDropUp /> : <MdArrowDropDown />}
/>
{expanded && (
<Menu
<Menu<T>
input
menuId={menuId}
onChange={onChange}
options={options}
ref={refMenu}
setExpanded={setExpanded}
style={style}
/>
)}
</fieldset>
);
});
}
Loading

0 comments on commit 9c95adf

Please sign in to comment.