Skip to content

Commit

Permalink
fix: button menu click handler (#290)
Browse files Browse the repository at this point in the history
- replace custom change handler with native click handler
- add `onClick` to menu options for specific actions
  • Loading branch information
uipoet authored Apr 6, 2022
1 parent 96b5cf4 commit e03666d
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 76 deletions.
26 changes: 21 additions & 5 deletions src/docs/pages/examples/button-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ButtonMenu, Card, useHashRef } from "ninjakit";
import { MdMenu } from "react-icons/md";
import { MdFavorite, MdMenu } from "react-icons/md";

import styles from "../examples.module.css";
import { InputMenuState, options, useButtonMenuState } from "./state";
import { InputMenuState, useButtonMenuState } from "./state";

export function ButtonMenuExample() {
const hashRef = useHashRef<HTMLDivElement>({ id: "button-menu" });
Expand All @@ -24,10 +24,26 @@ export function ButtonMenuExample() {
id="button-menu-example"
label="Label"
leadingIcon={leadingIcon ? <MdMenu /> : undefined}
onChange={({ currentTarget: { value } }) =>
console.info("ButtonMenu change:", value)
onClick={({ currentTarget: { value } }) =>
console.info("ButtonMenu click:", value)
}
options={options}
options={[
"Item One",
{
leadingIcon: <MdFavorite />,
onClick: () => console.info("Clicked: Item Two"),
value: "Item Two",
},
"Item Three",
{ separator: true },
"Item Four",
"Item Five",
{ disabled: true, value: "Item Six" },
"Item Seven",
"Item Eight",
"Item Nine",
"Item Ten",
]}
/>
</section>
<InputMenuState state={state} />
Expand Down
14 changes: 0 additions & 14 deletions src/docs/pages/examples/button-menu/state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,3 @@ export function InputMenuState({
</aside>
);
}

export const options = [
"Item One",
"Item Two",
"Item Three",
{ separator: true },
"Item Four",
"Item Five",
{ disabled: true, value: "Item Six" },
"Item Seven",
"Item Eight",
"Item Nine",
"Item Ten",
];
16 changes: 14 additions & 2 deletions src/docs/pages/examples/input-menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Card, InputMenu, useHashRef } from "ninjakit";
import { MdFavorite } from "react-icons/md";

import styles from "../examples.module.css";
import { InputMenuState, options, useInputMenuState } from "./state";
import { InputMenuState, useInputMenuState } from "./state";

export function InputMenuExample() {
const hashRef = useHashRef<HTMLDivElement>({ id: "input-menu" });
Expand All @@ -25,7 +25,19 @@ export function InputMenuExample() {
onChange={({ currentTarget: { value } }) =>
console.info("InputMenu change:", value)
}
options={options}
options={[
"Item One",
"Item Two",
"Item Three",
{ separator: true },
"Item Four",
"Item Five",
{ disabled: true, value: "Item Six" },
"Item Seven",
{ leadingIcon: <MdFavorite />, value: "Item Eight" },
"Item Nine",
"Item Ten",
]}
/>
</section>
<InputMenuState state={state} />
Expand Down
15 changes: 0 additions & 15 deletions src/docs/pages/examples/input-menu/state.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Checkbox, InputMenu } from "ninjakit";
import { Dispatch, SetStateAction, useState } from "react";
import { MdFavorite } from "react-icons/md";

type Appearance = "filled" | "outlined";

Expand Down Expand Up @@ -87,17 +86,3 @@ export function InputMenuState({
</aside>
);
}

export const options = [
"Item One",
"Item Two",
"Item Three",
{ separator: true },
"Item Four",
"Item Five",
{ disabled: true, value: "Item Six" },
"Item Seven",
{ leadingIcon: <MdFavorite />, value: "Item Eight" },
"Item Nine",
"Item Ten",
];
15 changes: 6 additions & 9 deletions src/lib/components/menu/button.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import { Button, MenuOptions } from "ninjakit";
import { useRef } from "react";
import { MdArrowDropDown, MdArrowDropUp } from "react-icons/md";

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

export function ButtonMenu({
className: classNameOverride,
container,
id,
onChange,
onClick,
options,
...props
}: Omit<JSX.IntrinsicElements["button"], "id" | "onChange"> &
}: Omit<JSX.IntrinsicElements["button"], "id"> &
ButtonProps & {
container?: HTMLElement;
id: string;
onChange: ButtonChangeHandler;
options: MenuOptions;
}) {
const {
Expand All @@ -27,13 +25,12 @@ export function ButtonMenu({
handleClickControl,
handleKeyDownControl,
menuId,
refControl,
refFieldset,
refMenu,
style,
setExpanded,
} = useMenu({ classNameOverride, id });

const refControl = useRef<HTMLButtonElement | null>(null);
} = useMenu<HTMLButtonElement>({ classNameOverride, id });

return (
<fieldset className={className} ref={refFieldset}>
Expand All @@ -54,7 +51,7 @@ export function ButtonMenu({
container={container}
controlElement={refControl.current}
menuId={menuId}
onChange={onChange}
onClick={onClick}
options={options}
ref={refMenu}
setExpanded={setExpanded}
Expand Down
24 changes: 14 additions & 10 deletions src/lib/components/menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ import {
useFloating,
} from "@floating-ui/react-dom";
import { classNames } from "ninjakit";
import { KeyboardEventHandler, ReactNode, useEffect, useState } from "react";
import {
KeyboardEventHandler,
MouseEventHandler,
ReactNode,
useEffect,
useRef,
useState,
} from "react";

import { firstHTMLElementChild } from "../../util";
import styles from "./menu.module.css";
Expand All @@ -15,6 +22,7 @@ export type MenuOptions<T extends string = string> = (
| {
disabled?: boolean;
leadingIcon?: ReactNode;
onClick?: MouseEventHandler<HTMLButtonElement>;
separator?: boolean;
value?: T;
}
Expand All @@ -23,18 +31,17 @@ export type MenuOptions<T extends string = string> = (
}
)[];

export function useMenu({
export function useMenu<T>({
classNameOverride,
flex,
id,
input,
}: {
flex?: boolean;
id: string;
input?: true;
classNameOverride?: string;
}) {
const [expanded, setExpanded] = useState(false);
const refControl = useRef<T | null>(null);
const { x, y, reference, floating, refs, strategy, update } = useFloating({
middleware: [flip(), shift()],
placement: "bottom-start",
Expand All @@ -60,10 +67,6 @@ export function useMenu({
}, [refs.reference, refs.floating, update]);
const handleClickControl = () => setExpanded(!expanded);
const handleKeyDownControl: KeyboardEventHandler = (event) => {
const element = input
? event.currentTarget.parentElement?.parentElement || null
: event.currentTarget;

if (expanded)
switch (event.key) {
case " ":
Expand All @@ -75,7 +78,6 @@ export function useMenu({
return event.preventDefault();
case "ArrowDown":
case "Tab":
if (element === null) return;
event.preventDefault();
return firstHTMLElementChild(
document.getElementById(menuId)
Expand All @@ -97,7 +99,8 @@ export function useMenu({
className: classNames({
[styles.fieldset]: true,
[styles.flex]: flex,
[input ? styles.input : styles.button]: true,
[refControl instanceof HTMLInputElement ? styles.input : styles.button]:
true,
classNameOverride,
}),
expanded,
Expand All @@ -109,6 +112,7 @@ export function useMenu({
top: y ?? "",
},
menuId,
refControl,
refFieldset: reference,
refMenu: floating,
setExpanded,
Expand Down
7 changes: 3 additions & 4 deletions src/lib/components/menu/input.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MenuOptions, TextInput } from "ninjakit";
import { forwardRef, useRef } from "react";
import { forwardRef } from "react";
import { MdArrowDropDown, MdArrowDropUp } from "react-icons/md";

import type { InputProps } from "../input";
Expand Down Expand Up @@ -32,13 +32,12 @@ export const InputMenu = forwardRef<
handleClickControl,
handleKeyDownControl,
menuId,
refControl,
refFieldset,
refMenu,
setExpanded,
style,
} = useMenu({ classNameOverride, flex, id, input: true });

const refControl = useRef<HTMLInputElement | null>(null);
} = useMenu<HTMLInputElement>({ classNameOverride, flex, id });

return (
<fieldset className={className} ref={refFieldset}>
Expand Down
35 changes: 18 additions & 17 deletions src/lib/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ export const Menu = forwardRef<
container?: HTMLElement;
controlElement: HTMLInputElement | HTMLButtonElement | null;
menuId: string;
onChange?: ButtonChangeHandler;
onClick?: MouseEventHandler<HTMLButtonElement>;
options: MenuOptions;
setExpanded: (expanded: boolean) => void;
} & Omit<JSX.IntrinsicElements["div"], "onChange">
} & Omit<JSX.IntrinsicElements["div"], "onClick">
>(function Menu(
{
container = document.body,
controlElement,
menuId,
onChange,
onClick,
options,
setExpanded,
...props
Expand All @@ -40,15 +40,15 @@ export const Menu = forwardRef<

setExpanded(false);
};
const handleClickMenuItem = (value: string) => {
if (onChange) {
return onChange({ currentTarget: { value } });
}

if (controlElement) {
const event = new Event("change", { bubbles: true, cancelable: true });
setNativeValue(controlElement, value);
controlElement.dispatchEvent(event);
const handleClickMenuItem: MouseEventHandler<HTMLButtonElement> = (event) => {
if (controlElement instanceof HTMLButtonElement && onClick) onClick(event);
if (controlElement instanceof HTMLInputElement) {
const changeEvent = new Event("change", {
bubbles: true,
cancelable: true,
});
setNativeValue(controlElement, event.currentTarget.value);
controlElement.dispatchEvent(changeEvent);
}
};
const handleKeyDownMenu: KeyboardEventHandler = ({ key }) =>
Expand All @@ -69,8 +69,6 @@ export const Menu = forwardRef<
case "ArrowDown":
event.preventDefault();

console.info("ArrowDown", nextHTMLElementSibling(currentTarget));

return nextHTMLElementSibling(currentTarget)?.focus();
case "ArrowUp":
event.preventDefault();
Expand All @@ -96,7 +94,7 @@ export const Menu = forwardRef<
<button
className={styles.option}
key={`${index}-${option}`}
onClick={() => handleClickMenuItem(option)}
onClick={handleClickMenuItem}
onKeyDown={handleKeyDownMenuItem}
value={option}
>
Expand All @@ -107,7 +105,7 @@ export const Menu = forwardRef<
if (option.separator)
return <hr aria-disabled className={styles.separator} key={index} />;

const { disabled, leadingIcon, value } = option;
const { disabled, leadingIcon, onClick, value } = option;

if (value === undefined) return null;

Expand All @@ -117,7 +115,10 @@ export const Menu = forwardRef<
className={styles.option}
disabled={disabled}
key={`${index}-${value}`}
onClick={() => handleClickMenuItem(value)}
onClick={(event) => {
if (onClick) onClick(event);
handleClickMenuItem(event);
}}
onKeyDown={handleKeyDownMenuItem}
value={value}
>
Expand Down

0 comments on commit e03666d

Please sign in to comment.