From d91250dde55cfb84c487283e053d088ec985f8bd Mon Sep 17 00:00:00 2001 From: Jamie Hoover Date: Tue, 4 Jan 2022 11:00:56 -0800 Subject: [PATCH] feat: menus (#102) - ButtonMenu - InputMenu - Button/AnchorButton leadingIcon/trailingIcon props - useRandomId hook - utils for DOM walking --- package-lock.json | 36 ++++--- package.json | 12 ++- src/docs/app.tsx | 8 ++ src/docs/pages/examples/anchor-button.tsx | 4 +- src/docs/pages/examples/button-menu.tsx | 42 ++++++++ src/docs/pages/examples/button.tsx | 9 +- src/docs/pages/examples/index.tsx | 4 + src/docs/pages/examples/input-menu.tsx | 32 ++++++ src/docs/pages/examples/menu-items.tsx | 30 ++++++ src/lib/components/button/anchor.tsx | 22 +++- src/lib/components/button/button.module.css | 27 +++-- src/lib/components/button/button.tsx | 19 +++- src/lib/components/button/index.tsx | 23 ++-- src/lib/components/input/index.tsx | 2 +- src/lib/components/input/text.tsx | 15 ++- src/lib/components/menu/button.tsx | 68 ++++++++++++ src/lib/components/menu/index.tsx | 114 ++++++++++++++++++++ src/lib/components/menu/input.tsx | 69 ++++++++++++ src/lib/components/menu/item.tsx | 27 +++++ src/lib/components/menu/items.tsx | 32 ++++++ src/lib/components/menu/menu.module.css | 100 +++++++++++++++++ src/lib/hooks/color-scheme.ts | 3 +- src/lib/hooks/random-id.ts | 8 ++ src/lib/index.ts | 5 +- src/lib/util/index.ts | 35 ++++++ tsconfig-types.json | 2 +- tsconfig.json | 2 +- 27 files changed, 690 insertions(+), 60 deletions(-) create mode 100644 src/docs/pages/examples/button-menu.tsx create mode 100644 src/docs/pages/examples/input-menu.tsx create mode 100644 src/docs/pages/examples/menu-items.tsx create mode 100644 src/lib/components/menu/button.tsx create mode 100644 src/lib/components/menu/index.tsx create mode 100644 src/lib/components/menu/input.tsx create mode 100644 src/lib/components/menu/item.tsx create mode 100644 src/lib/components/menu/items.tsx create mode 100644 src/lib/components/menu/menu.module.css create mode 100644 src/lib/hooks/random-id.ts create mode 100644 src/lib/util/index.ts diff --git a/package-lock.json b/package-lock.json index 83c9a53c..8b9ab45d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "react-icons": "4.3.1" }, "devDependencies": { - "@commitlint/cli": "16.0.0", + "@commitlint/cli": "16.0.1", "@commitlint/config-conventional": "16.0.0", "@fontsource/roboto": "4.5.1", "@playwright/test": "1.17.1", @@ -49,7 +49,7 @@ "react-router-dom": "6.2.1", "semantic-release": "18.0.1", "typescript": "4.5.4", - "vite": "2.7.7", + "vite": "2.7.9", "vite-plugin-pwa": "0.11.12", "workbox-window": "6.4.2" }, @@ -61,6 +61,14 @@ "@fontsource/roboto": ">=4", "react": ">=17", "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@babel/code-frame": { @@ -1774,9 +1782,9 @@ "dev": true }, "node_modules/@commitlint/cli": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-16.0.0.tgz", - "integrity": "sha512-MaYQbwsBZ3+OHJQm9cbSMj4P1Eptqqk/p8WY5MTzVZDRei8JcweNaMtwU7P+h9VeBTlFYSn4vkZYiZXzVK2Cww==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-16.0.1.tgz", + "integrity": "sha512-61gGRy65WiVDRsqP0dAR2fAgE3qrTBW3fgz9MySv32y5Ib3ZXXDDq6bGyQqi2dSaPuDYzNCRwwlC7mmQM73T/g==", "dev": true, "dependencies": { "@commitlint/format": "^16.0.0", @@ -18161,9 +18169,9 @@ } }, "node_modules/vite": { - "version": "2.7.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.7.tgz", - "integrity": "sha512-Nm4ingl//gMSj/p1aCBHuTc5Fd8W8Mwdci/HUvqCVq8xaJqF7z08S/LRq1M9kS0jRfJk1/f/CwUyQAr6YgsOLw==", + "version": "2.7.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.9.tgz", + "integrity": "sha512-CyopIJIRrc26Wnbkch5BCHFhQodzeV4jjSegLAEw1RImvXSuoOo2SfpEW9zRVi188WmjJwCqCaoG25C0r4nekw==", "dev": true, "dependencies": { "esbuild": "^0.13.12", @@ -20025,9 +20033,9 @@ "dev": true }, "@commitlint/cli": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-16.0.0.tgz", - "integrity": "sha512-MaYQbwsBZ3+OHJQm9cbSMj4P1Eptqqk/p8WY5MTzVZDRei8JcweNaMtwU7P+h9VeBTlFYSn4vkZYiZXzVK2Cww==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-16.0.1.tgz", + "integrity": "sha512-61gGRy65WiVDRsqP0dAR2fAgE3qrTBW3fgz9MySv32y5Ib3ZXXDDq6bGyQqi2dSaPuDYzNCRwwlC7mmQM73T/g==", "dev": true, "requires": { "@commitlint/format": "^16.0.0", @@ -32323,9 +32331,9 @@ } }, "vite": { - "version": "2.7.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.7.tgz", - "integrity": "sha512-Nm4ingl//gMSj/p1aCBHuTc5Fd8W8Mwdci/HUvqCVq8xaJqF7z08S/LRq1M9kS0jRfJk1/f/CwUyQAr6YgsOLw==", + "version": "2.7.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.9.tgz", + "integrity": "sha512-CyopIJIRrc26Wnbkch5BCHFhQodzeV4jjSegLAEw1RImvXSuoOo2SfpEW9zRVi188WmjJwCqCaoG25C0r4nekw==", "dev": true, "requires": { "esbuild": "^0.13.12", diff --git a/package.json b/package.json index f0294856..0a08d251 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "react-icons": "4.3.1" }, "devDependencies": { - "@commitlint/cli": "16.0.0", + "@commitlint/cli": "16.0.1", "@commitlint/config-conventional": "16.0.0", "@playwright/test": "1.17.1", "@fontsource/roboto": "4.5.1", @@ -40,7 +40,7 @@ "react-router-dom": "6.2.1", "semantic-release": "18.0.1", "typescript": "4.5.4", - "vite": "2.7.7", + "vite": "2.7.9", "vite-plugin-pwa": "0.11.12", "workbox-window": "6.4.2" }, @@ -88,6 +88,14 @@ "react": ">=17", "react-dom": ">=17" }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + }, "repository": { "type": "git", "url": "https://github.com/ninja/ninjakit.git" diff --git a/src/docs/app.tsx b/src/docs/app.tsx index 4595b58d..80e8ab22 100644 --- a/src/docs/app.tsx +++ b/src/docs/app.tsx @@ -144,6 +144,14 @@ function App() { TextInput + + + ButtonMenu + + + + InputMenu + RadioSet diff --git a/src/docs/pages/examples/anchor-button.tsx b/src/docs/pages/examples/anchor-button.tsx index 5224570f..e24793bc 100644 --- a/src/docs/pages/examples/anchor-button.tsx +++ b/src/docs/pages/examples/anchor-button.tsx @@ -9,8 +9,8 @@ export const AnchorButtonExamples: FunctionComponent = () => ( Elevated Filled - - With Icon + }> + With Icon With External Target diff --git a/src/docs/pages/examples/button-menu.tsx b/src/docs/pages/examples/button-menu.tsx new file mode 100644 index 00000000..52d44373 --- /dev/null +++ b/src/docs/pages/examples/button-menu.tsx @@ -0,0 +1,42 @@ +import { ButtonMenu, Card } from "ninjakit"; +import { FunctionComponent } from "react"; +import { MdMenu } from "react-icons/md"; + +import { useMenuItems } from "./menu-items"; + +export const ButtonMenuExamples: FunctionComponent = () => { + const menuItems = useMenuItems(); + + return ( + +
+ + console.info("ButtonMenu", { value }) + } + > + {menuItems} + + + console.info("ButtonMenu", { value }) + } + > + {menuItems} + + } + onChange={({ currentTarget: { value } }) => + console.info("ButtonMenu", { value }) + } + > + {menuItems} + +
+
+ ); +}; diff --git a/src/docs/pages/examples/button.tsx b/src/docs/pages/examples/button.tsx index 7613f552..3773fd60 100644 --- a/src/docs/pages/examples/button.tsx +++ b/src/docs/pages/examples/button.tsx @@ -7,19 +7,18 @@ export const ButtonExamples: FunctionComponent = () => (
- + +
- ); }); diff --git a/src/lib/components/button/index.tsx b/src/lib/components/button/index.tsx index 7a303514..8d0760ad 100644 --- a/src/lib/components/button/index.tsx +++ b/src/lib/components/button/index.tsx @@ -1,34 +1,43 @@ -import { Children, PropsWithChildren, ReactNode } from "react"; +import { ReactNode } from "react"; import typography from "../typography/typography.module.css"; import styles from "./button.module.css"; -type Appearance = "text" | "outlined" | "elevated" | "tonal" | "filled"; +type Appearance = "elevated" | "filled" | "outlined" | "text" | "tonal"; -export type ButtonProps = PropsWithChildren<{ +export type ButtonProps = { /** @see https://m3.material.io/components/all-buttons */ appearance?: Appearance; -}>; + label?: string; + leadingIcon?: ReactNode; + trailingIcon?: ReactNode; +}; export function useClassName({ appearance, children, + label, + leadingIcon, override, target, + trailingIcon, }: { anchor?: boolean; appearance: Appearance; children: ReactNode; + label?: string; + leadingIcon?: ReactNode; override?: string; target?: string; + trailingIcon?: ReactNode; }): string | undefined { return [ styles.button, styles[appearance], typography.labelLarge, - children && styles.children, - Children.count(children) > 1 && styles.icon, - target === "_blank" && styles.external, + children || label ? styles.children : undefined, + leadingIcon ? styles.leadingIcon : undefined, + trailingIcon || target === "_blank" ? styles.trailingIcon : undefined, override, ].join(" "); } diff --git a/src/lib/components/input/index.tsx b/src/lib/components/input/index.tsx index ff7ef5e0..d515a56c 100644 --- a/src/lib/components/input/index.tsx +++ b/src/lib/components/input/index.tsx @@ -5,7 +5,7 @@ import styles from "./input.module.css"; type Appearance = "filled" | "outlined"; -export type InputProps = JSX.IntrinsicElements["input"] & { +export type InputProps = { /** @see https://material.io/design/components/text-fields.html */ appearance?: Appearance; error?: true | string; diff --git a/src/lib/components/input/text.tsx b/src/lib/components/input/text.tsx index 25107163..5e76cc19 100644 --- a/src/lib/components/input/text.tsx +++ b/src/lib/components/input/text.tsx @@ -1,16 +1,19 @@ -import { forwardRef, InputHTMLAttributes } from "react"; +import { useRandomId } from "ninjakit"; +import { forwardRef } from "react"; import { InputProps, useClassName } from "."; import styles from "./input.module.css"; export const TextInput = forwardRef< HTMLInputElement, - InputHTMLAttributes & InputProps + JSX.IntrinsicElements["input"] & InputProps >(function ( { appearance, + "aria-expanded": ariaExpanded, className: override, error, + id, label, leadingIcon, onClickTrailingIcon: handleClick, @@ -20,15 +23,21 @@ export const TextInput = forwardRef< }, ref ) { + const randomId = useRandomId(); const className = useClassName({ appearance, error, leadingIcon, override }); return ( -