From 6f8d74cec4329fe035941793765b1cd58a88825b Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Mon, 5 Aug 2024 14:26:12 +0800 Subject: [PATCH] feat: add internal Button, LinkButton, Link component with updated styling (#410) * feat: add internal Button component with updated styling * feat: add LinkButton internal component * feat: add focus ring utility class * feat: apply focus ring to button * feat: make inverse outline hover bg less opaque * feat: create internal Link component that accepts LinkComponent * feat: use internal Link component * feat: replace usages of Button with LinkButton * feat: format and lint * feat: set minimum height on button * feat: update Button stories for all variants and sizes * feat: add attribution to files modified from react-aria-components * fix: prevent button inside a flex from filling height using height=100% instead of align-self because I still want flex parents to be able to choose alignment if they wish to. * fix: add display:block to button style (for LinkButton) * feat: sync LinkButton stories with Button stories * feat: add "use client" directive on Link * feat: import correct custom tv * fix: format --- package-lock.json | 249 ++++++++++++++++++ packages/components/package.json | 1 + .../next/components/complex/Button/Button.tsx | 3 + .../next/components/complex/Hero/Hero.tsx | 17 +- .../components/complex/Infobar/Infobar.tsx | 18 +- .../internal/Button/Button.stories.tsx | 76 ++++++ .../components/internal/Button/Button.tsx | 101 +++++++ .../next/components/internal/Button/index.ts | 1 + .../ContentPageHeader/ContentPageHeader.tsx | 11 +- .../next/components/internal/Link/Link.tsx | 83 ++++++ .../next/components/internal/Link/index.ts | 1 + .../next/components/internal/Link/utils.ts | 92 +++++++ .../LinkButton/LinkButton.stories.tsx | 76 ++++++ .../internal/LinkButton/LinkButton.tsx | 35 +++ .../components/internal/LinkButton/index.ts | 1 + .../next/layouts/Content/Content.tsx | 2 +- packages/components/src/utils/focusRing.ts | 11 + 17 files changed, 750 insertions(+), 28 deletions(-) create mode 100644 packages/components/src/templates/next/components/internal/Button/Button.stories.tsx create mode 100644 packages/components/src/templates/next/components/internal/Button/Button.tsx create mode 100644 packages/components/src/templates/next/components/internal/Button/index.ts create mode 100644 packages/components/src/templates/next/components/internal/Link/Link.tsx create mode 100644 packages/components/src/templates/next/components/internal/Link/index.ts create mode 100644 packages/components/src/templates/next/components/internal/Link/utils.ts create mode 100644 packages/components/src/templates/next/components/internal/LinkButton/LinkButton.stories.tsx create mode 100644 packages/components/src/templates/next/components/internal/LinkButton/LinkButton.tsx create mode 100644 packages/components/src/templates/next/components/internal/LinkButton/index.ts create mode 100644 packages/components/src/utils/focusRing.ts diff --git a/package-lock.json b/package-lock.json index 5eb30b1d4e..f6940915e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8387,6 +8387,46 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-aria/collections": { + "version": "3.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@react-aria/collections/-/collections-3.0.0-alpha.3.tgz", + "integrity": "sha512-SKsoQrCuz4zIVMwKGz0WcFoRbIP0H8+eRU2XzjmWX9KlRdrfeqIBOxuiU8XO3or0aHdbBI/bC/YtCjVzix5Lrg==", + "dependencies": { + "@react-aria/ssr": "^3.9.5", + "@react-aria/utils": "^3.25.1", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@react-aria/color": { + "version": "3.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.0.0-rc.1.tgz", + "integrity": "sha512-oP9PE0Xpo9uQ/TtH1x8iWhsjtk4OTIoTFdQZyoDsj8d84sqRv6Og9ajBZ/VTaneNK1n4NrPSx+qWfXu+SrWlDg==", + "dependencies": { + "@react-aria/i18n": "^3.12.1", + "@react-aria/interactions": "^3.22.1", + "@react-aria/numberfield": "^3.11.5", + "@react-aria/slider": "^3.7.10", + "@react-aria/spinbutton": "^3.6.7", + "@react-aria/textfield": "^3.14.7", + "@react-aria/utils": "^3.25.1", + "@react-aria/visually-hidden": "^3.8.14", + "@react-stately/color": "^3.7.1", + "@react-stately/form": "^3.0.5", + "@react-types/color": "3.0.0-rc.1", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-aria/combobox": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.10.1.tgz", @@ -9004,6 +9044,21 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-aria/toolbar": { + "version": "3.0.0-beta.7", + "resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.7.tgz", + "integrity": "sha512-PKaXD2qiWcVOn/bX07ipamTc6OlqypqcQRGG7WUL0ZXWfV6AfL7GFPS1B2Jh7Etetq68Ynyuo6R4jT4Jypsjdg==", + "dependencies": { + "@react-aria/focus": "^3.18.1", + "@react-aria/i18n": "^3.12.1", + "@react-aria/utils": "^3.25.1", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-aria/tooltip": { "version": "3.7.6", "resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.7.6.tgz", @@ -9021,6 +9076,25 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-aria/tree": { + "version": "3.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@react-aria/tree/-/tree-3.0.0-alpha.3.tgz", + "integrity": "sha512-o/9B+PVSUYxDM1KxQ/Pl1CytPtIagyidmasd10266hWfwzvPA0ZyakBwIEFj+ROnr9buAdP+A4sOTRo+a6g+YQ==", + "dependencies": { + "@react-aria/gridlist": "^3.9.1", + "@react-aria/i18n": "^3.12.1", + "@react-aria/selection": "^3.19.1", + "@react-aria/utils": "^3.25.1", + "@react-stately/tree": "^3.8.3", + "@react-types/button": "^3.9.6", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-aria/utils": { "version": "3.25.1", "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.25.1.tgz", @@ -9036,6 +9110,23 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-aria/virtualizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-aria/virtualizer/-/virtualizer-4.0.1.tgz", + "integrity": "sha512-JZ6X0l38ZwBU/JgeLwkDA8mknRxqO1nYSVaPZHgOg8fd9BzMRWBjse7VW+Uf09P0uAEFElwlB+RY8UDx+W/Fmg==", + "dependencies": { + "@react-aria/i18n": "^3.12.1", + "@react-aria/interactions": "^3.22.1", + "@react-aria/utils": "^3.25.1", + "@react-stately/virtualizer": "^4.0.1", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-aria/visually-hidden": { "version": "3.8.14", "resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.14.tgz", @@ -9092,6 +9183,26 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-stately/color": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.7.1.tgz", + "integrity": "sha512-pJqM7fZ7+zy8wnzCUkBMkTgmjMs+lBLjQm1k+dFbmXK2SuELiDOQLirrl6j15NVBOKn8avvRHXpAQhGX43GOCQ==", + "dependencies": { + "@internationalized/number": "^3.5.3", + "@internationalized/string": "^3.2.3", + "@react-aria/i18n": "^3.12.1", + "@react-stately/form": "^3.0.5", + "@react-stately/numberfield": "^3.9.5", + "@react-stately/slider": "^3.5.6", + "@react-stately/utils": "^3.10.2", + "@react-types/color": "3.0.0-rc.1", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-stately/combobox": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.9.1.tgz", @@ -9111,6 +9222,18 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-stately/data": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@react-stately/data/-/data-3.11.6.tgz", + "integrity": "sha512-S8q1Ejuhijl8SnyVOdDNFrMrWWnLk/Oh1ZT3KHSbTdpfMRtvhi5HukoiP06jlzz75phnpSPQL40npDtUB/kk3Q==", + "dependencies": { + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-stately/datepicker": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.10.1.tgz", @@ -9177,6 +9300,23 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-stately/layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-stately/layout/-/layout-4.0.1.tgz", + "integrity": "sha512-4oNYFhQprcwP1fNV/p3dbx1a6lzMGBAKLTdcvtCuBCgclNA3etqjdQAUIZ0Bpq+Z8i9qo3c85oxr6Tr8BKQV4w==", + "dependencies": { + "@react-stately/collections": "^3.10.9", + "@react-stately/table": "^3.12.1", + "@react-stately/virtualizer": "^4.0.1", + "@react-types/grid": "^3.2.8", + "@react-types/shared": "^3.24.1", + "@react-types/table": "^3.10.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-stately/list": { "version": "3.10.7", "resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.10.7.tgz", @@ -9391,6 +9531,19 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-stately/virtualizer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-4.0.1.tgz", + "integrity": "sha512-HCje3SlLItQFAiBHH4JZhz74mMCe2g+Q8woJa6kdKlvFqsNdmhtFHuuIr1uW6LWj76j2N0Xaa8Z7fV1f5ovX0Q==", + "dependencies": { + "@react-aria/utils": "^3.25.1", + "@react-types/shared": "^3.24.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-types/breadcrumbs": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.7.tgz", @@ -9437,6 +9590,18 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-types/color": { + "version": "3.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.0.0-rc.1.tgz", + "integrity": "sha512-aw6FzrBlZTWKrFaFskM7e3AFICe6JqH10wO0E919goa3LZDDFbyYEwRpatwjIyiZH1elEUkFPgwqpv3ZcPPn8g==", + "dependencies": { + "@react-types/shared": "^3.24.1", + "@react-types/slider": "^3.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-types/combobox": { "version": "3.12.1", "resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.12.1.tgz", @@ -9474,6 +9639,17 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@react-types/form": { + "version": "3.7.6", + "resolved": "https://registry.npmjs.org/@react-types/form/-/form-3.7.6.tgz", + "integrity": "sha512-lhS2y1bVtRnyYjkM+ylJUp2g663ZNbeZxu2o+mFfD5c2wYmVLA58IWR90c7DL8IVUitoANnZ1JPhhXvutiFpQQ==", + "dependencies": { + "@react-types/shared": "^3.24.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@react-types/grid": { "version": "3.2.8", "resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.2.8.tgz", @@ -29686,6 +29862,45 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-aria-components": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.3.1.tgz", + "integrity": "sha512-yUTA8uHbioQHU5d7iNvSLZLEfQlcTAmyhhkY+NMc8pIGPdtf0qnrlF0nPtJq8Mro5irpVrgUlqKBvvCiKwFNiQ==", + "dependencies": { + "@internationalized/date": "^3.5.5", + "@internationalized/string": "^3.2.3", + "@react-aria/collections": "3.0.0-alpha.3", + "@react-aria/color": "3.0.0-rc.1", + "@react-aria/dnd": "^3.7.1", + "@react-aria/focus": "^3.18.1", + "@react-aria/interactions": "^3.22.1", + "@react-aria/menu": "^3.15.1", + "@react-aria/toolbar": "3.0.0-beta.7", + "@react-aria/tree": "3.0.0-alpha.3", + "@react-aria/utils": "^3.25.1", + "@react-aria/virtualizer": "^4.0.1", + "@react-stately/color": "^3.7.1", + "@react-stately/layout": "^4.0.1", + "@react-stately/menu": "^3.8.1", + "@react-stately/table": "^3.12.1", + "@react-stately/utils": "^3.10.2", + "@react-stately/virtualizer": "^4.0.1", + "@react-types/color": "3.0.0-rc.1", + "@react-types/form": "^3.7.6", + "@react-types/grid": "^3.2.8", + "@react-types/shared": "^3.24.1", + "@react-types/table": "^3.10.1", + "@swc/helpers": "^0.5.0", + "client-only": "^0.0.1", + "react-aria": "^3.34.1", + "react-stately": "^3.32.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-bootstrap": { "version": "2.10.4", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.4.tgz", @@ -30073,6 +30288,39 @@ "react-dom": ">=16.8.0" } }, + "node_modules/react-stately": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.32.1.tgz", + "integrity": "sha512-znw+bqHJk1fvv34O3HoVH61otyYJomRu1gI7A4B3UHCnSFS6E6nMI6D3nRv9RrAWhf4ekLLg35FwDTHDcG1zdg==", + "dependencies": { + "@react-stately/calendar": "^3.5.3", + "@react-stately/checkbox": "^3.6.7", + "@react-stately/collections": "^3.10.9", + "@react-stately/combobox": "^3.9.1", + "@react-stately/data": "^3.11.6", + "@react-stately/datepicker": "^3.10.1", + "@react-stately/dnd": "^3.4.1", + "@react-stately/form": "^3.0.5", + "@react-stately/list": "^3.10.7", + "@react-stately/menu": "^3.8.1", + "@react-stately/numberfield": "^3.9.5", + "@react-stately/overlays": "^3.6.9", + "@react-stately/radio": "^3.10.6", + "@react-stately/searchfield": "^3.5.5", + "@react-stately/select": "^3.6.6", + "@react-stately/selection": "^3.16.1", + "@react-stately/slider": "^3.5.6", + "@react-stately/table": "^3.12.1", + "@react-stately/tabs": "^3.6.8", + "@react-stately/toggle": "^3.7.6", + "@react-stately/tooltip": "^3.4.11", + "@react-stately/tree": "^3.8.3", + "@react-types/shared": "^3.24.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -35897,6 +36145,7 @@ "markdown-to-jsx": "^7.4.7", "minisearch": "^6.3.0", "react-aria": "^3.34.1", + "react-aria-components": "^1.3.1", "react-icons": "^5.2.0", "tailwind-merge": "^2.4.0", "tailwind-variants": "^0.2.1", diff --git a/packages/components/package.json b/packages/components/package.json index 8805f80e60..6cf39032c0 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -105,6 +105,7 @@ "markdown-to-jsx": "^7.4.7", "minisearch": "^6.3.0", "react-aria": "^3.34.1", + "react-aria-components": "^1.3.1", "react-icons": "^5.2.0", "tailwind-merge": "^2.4.0", "tailwind-variants": "^0.2.1", diff --git a/packages/components/src/templates/next/components/complex/Button/Button.tsx b/packages/components/src/templates/next/components/complex/Button/Button.tsx index a6e89be482..3824151a7b 100644 --- a/packages/components/src/templates/next/components/complex/Button/Button.tsx +++ b/packages/components/src/templates/next/components/complex/Button/Button.tsx @@ -104,6 +104,9 @@ const LinkButton = (props: Omit) => { ) } +/** + * @deprecated use LinkButton instead + */ const Button = (props: Omit) => { if (props.variant === "outline") { return diff --git a/packages/components/src/templates/next/components/complex/Hero/Hero.tsx b/packages/components/src/templates/next/components/complex/Hero/Hero.tsx index 12145177c5..27e1a96a5d 100644 --- a/packages/components/src/templates/next/components/complex/Hero/Hero.tsx +++ b/packages/components/src/templates/next/components/complex/Hero/Hero.tsx @@ -1,6 +1,6 @@ import type { HeroProps } from "~/interfaces/complex/Hero" import { ComponentContent } from "../../internal/customCssClass" -import Button from "../Button" +import { LinkButton } from "../../internal/LinkButton/LinkButton" const Hero = ({ title, @@ -27,18 +27,15 @@ const Hero = ({ {subtitle &&

{subtitle}

} {buttonLabel && buttonUrl && (
-
)} diff --git a/packages/components/src/templates/next/components/complex/Infobar/Infobar.tsx b/packages/components/src/templates/next/components/complex/Infobar/Infobar.tsx index 404186bd0a..c3b7b790b3 100644 --- a/packages/components/src/templates/next/components/complex/Infobar/Infobar.tsx +++ b/packages/components/src/templates/next/components/complex/Infobar/Infobar.tsx @@ -1,6 +1,6 @@ import type { InfobarProps } from "~/interfaces" import { ComponentContent } from "../../internal/customCssClass" -import Button from "../Button" +import { LinkButton } from "../../internal/LinkButton" const Infobar = ({ title, @@ -27,20 +27,18 @@ const Infobar = ({
{buttonLabel && buttonUrl && ( -
diff --git a/packages/components/src/templates/next/components/internal/Button/Button.stories.tsx b/packages/components/src/templates/next/components/internal/Button/Button.stories.tsx new file mode 100644 index 0000000000..69a627ff93 --- /dev/null +++ b/packages/components/src/templates/next/components/internal/Button/Button.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { Button } from "./Button" + +const BUTTON_SIZES = ["base", "lg"] as const + +const meta: Meta = { + title: "Next/Internal Components/Button", + component: Button, + render: (args) => ( +
+ {BUTTON_SIZES.map((size) => ( +
+ ), + argTypes: { + colorScheme: { + options: ["default", "inverse"], + control: { + type: "select", + }, + }, + variant: { + options: ["solid", "outline"], + control: { + type: "select", + }, + }, + }, + parameters: { + themes: { + themeOverride: "Isomer Next", + }, + }, +} +export default meta +type Story = StoryObj + +// Default scenario +export const Default: Story = { + args: { + children: "Work with us", + }, +} + +export const LongerButtonText: Story = { + args: { + children: "slightly longer button text", + }, +} + +export const OutlineButton: Story = { + args: { + ...Default.args, + variant: "outline", + }, +} + +export const InverseDefaultButton: Story = { + decorators: [ + (storyFn) =>
{storyFn()}
, + ], + args: { + ...Default.args, + colorScheme: "inverse", + }, +} + +export const InverseOutlineButton: Story = { + decorators: InverseDefaultButton.decorators, + args: { + ...OutlineButton.args, + colorScheme: "inverse", + }, +} diff --git a/packages/components/src/templates/next/components/internal/Button/Button.tsx b/packages/components/src/templates/next/components/internal/Button/Button.tsx new file mode 100644 index 0000000000..83be3b764f --- /dev/null +++ b/packages/components/src/templates/next/components/internal/Button/Button.tsx @@ -0,0 +1,101 @@ +"use client" + +import type { ButtonProps as AriaButtonProps } from "react-aria-components" +import type { VariantProps } from "tailwind-variants" +import { forwardRef } from "react" +import { Button as AriaButton, composeRenderProps } from "react-aria-components" + +import { tv } from "~/lib/tv" +import { focusRing } from "~/utils/focusRing" + +export const buttonStyles = tv({ + base: "box-border block h-full w-fit cursor-pointer rounded text-center transition", + extend: focusRing, + variants: { + variant: { + solid: "", + outline: "", + }, + colorScheme: { + default: "", + inverse: "", + }, + isDisabled: { + true: "cursor-not-allowed", + }, + size: { + base: "prose-headline-base-medium min-h-12 px-5 py-3", + lg: "prose-headline-lg-medium min-h-[3.25rem] px-6 py-3.5", + }, + }, + compoundVariants: [ + { + variant: "solid", + colorScheme: "default", + className: + "bg-brand-canvas-inverse text-base-content-inverse hover:bg-brand-interaction-hover active:bg-brand-interaction-pressed", + }, + { + variant: "solid", + colorScheme: "inverse", + className: "bg-base-canvas text-base-content", + }, + { + variant: "outline", + colorScheme: "inverse", + className: + "border border-base-divider-inverse text-base-content-inverse hover:bg-base-canvas-inverse-overlay/80", + }, + { + variant: "outline", + colorScheme: "default", + className: "border border-brand-canvas-inverse text-brand-canvas-inverse", + }, + { + variant: "outline", + size: "lg", + // -1 px for border + className: "px-[23px] py-[13px]", + }, + { + variant: "outline", + size: "base", + // -1 px for border + className: "px-[19px] py-[11px]", + }, + ], + defaultVariants: { + variant: "solid", + colorScheme: "default", + size: "base", + }, +}) + +export interface ButtonProps + extends AriaButtonProps, + VariantProps {} + +/** + * You probaby do not want to use this component if you are rendering a link. + * Use `LinkButton` component instead. + */ +export const Button = forwardRef( + ({ className, variant, colorScheme, size, ...props }, ref) => { + return ( + + buttonStyles({ + ...renderProps, + variant, + size, + className, + colorScheme, + }), + )} + /> + ) + }, +) +Button.displayName = "Button" diff --git a/packages/components/src/templates/next/components/internal/Button/index.ts b/packages/components/src/templates/next/components/internal/Button/index.ts new file mode 100644 index 0000000000..678370120b --- /dev/null +++ b/packages/components/src/templates/next/components/internal/Button/index.ts @@ -0,0 +1 @@ +export * from "./Button" diff --git a/packages/components/src/templates/next/components/internal/ContentPageHeader/ContentPageHeader.tsx b/packages/components/src/templates/next/components/internal/ContentPageHeader/ContentPageHeader.tsx index 81a404a462..e4f0651b69 100644 --- a/packages/components/src/templates/next/components/internal/ContentPageHeader/ContentPageHeader.tsx +++ b/packages/components/src/templates/next/components/internal/ContentPageHeader/ContentPageHeader.tsx @@ -1,7 +1,7 @@ import type { ContentPageHeaderProps } from "~/interfaces" import { getFormattedDate } from "~/utils" -import Button from "../../complex/Button" import Breadcrumb from "../Breadcrumb" +import { LinkButton } from "../LinkButton" const ContentPageHeader = ({ title, @@ -23,12 +23,9 @@ const ContentPageHeader = ({ {buttonLabel && buttonUrl && (
-
)} diff --git a/packages/components/src/templates/next/components/internal/Link/Link.tsx b/packages/components/src/templates/next/components/internal/Link/Link.tsx new file mode 100644 index 0000000000..15cb071714 --- /dev/null +++ b/packages/components/src/templates/next/components/internal/Link/Link.tsx @@ -0,0 +1,83 @@ +"use client" + +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * Original file: https://github.com/adobe/react-spectrum/blob/d6d5bc03cf6cb39d37b2a9e2de201f950b0d8994/packages/react-aria-components/src/Link.tsx + * Original file license: Apache-2.0 + * + * This is a modified version of the original file. + */ +import type { ElementType } from "react" +import type { + LinkProps as BaseLinkProps, + ContextValue, +} from "react-aria-components" +import { createContext, forwardRef } from "react" +import { mergeProps, useFocusRing, useHover, useLink } from "react-aria" +import { useContextProps } from "react-aria-components" + +import { useRenderProps } from "./utils" + +export interface LinkProps extends BaseLinkProps { + LinkComponent?: ElementType + "aria-current"?: string +} + +const LinkContext = + createContext>(null) + +/** + * Modified version of `react-aria-component`'s Link component to accept a `LinkComponent` prop. + */ +export const Link = forwardRef((_props, _ref) => { + const [props, ref] = useContextProps(_props, _ref, LinkContext) + + const ElementType: ElementType = props.href ? "a" : "span" + const { linkProps, isPressed } = useLink( + { ...props, elementType: ElementType }, + ref, + ) + + const { hoverProps, isHovered } = useHover(props) + const { focusProps, isFocused, isFocusVisible } = useFocusRing() + + const renderProps = useRenderProps({ + ...props, + defaultClassName: "react-aria-Link", + values: { + isCurrent: !!props["aria-current"], + isDisabled: props.isDisabled || false, + isPressed, + isHovered, + isFocused, + isFocusVisible, + }, + }) + + const ElementToRender = props.href ? props.LinkComponent ?? "a" : "span" + + return ( + + {renderProps.children} + + ) +}) diff --git a/packages/components/src/templates/next/components/internal/Link/index.ts b/packages/components/src/templates/next/components/internal/Link/index.ts new file mode 100644 index 0000000000..459c7bf47a --- /dev/null +++ b/packages/components/src/templates/next/components/internal/Link/index.ts @@ -0,0 +1 @@ +export * from "./Link" diff --git a/packages/components/src/templates/next/components/internal/Link/utils.ts b/packages/components/src/templates/next/components/internal/Link/utils.ts new file mode 100644 index 0000000000..182a3e513a --- /dev/null +++ b/packages/components/src/templates/next/components/internal/Link/utils.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2022 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * Original file: https://github.com/adobe/react-spectrum/blob/63a2ff648edbd4a5e41827b087701237e63eca83/packages/react-aria-components/src/utils.tsx + * Original file license: Apache-2.0 + */ + +import type { CSSProperties, ReactNode } from "react" +import { useMemo } from "react" + +interface RenderPropsHookOptions { + /** The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state. */ + className?: + | string + | ((values: T & { defaultClassName: string | undefined }) => string) + /** The inline [style](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style) for the element. A function may be provided to compute the style based on component state. */ + style?: + | CSSProperties + | ((values: T & { defaultStyle: CSSProperties }) => CSSProperties) + values: T + defaultChildren?: ReactNode + defaultClassName?: string + defaultStyle?: CSSProperties + /** The children of the component. A function may be provided to alter the children based on component state. */ + children?: + | ReactNode + | ((values: T & { defaultChildren: ReactNode | undefined }) => ReactNode) +} + +export function useRenderProps(props: RenderPropsHookOptions) { + const { + className, + style, + children, + defaultClassName = undefined, + defaultChildren = undefined, + defaultStyle, + values, + } = props + + return useMemo(() => { + let computedClassName: string | undefined + let computedStyle: React.CSSProperties | undefined + let computedChildren: React.ReactNode | undefined + + if (typeof className === "function") { + computedClassName = className({ ...values, defaultClassName }) + } else { + computedClassName = className + } + + if (typeof style === "function") { + computedStyle = style({ ...values, defaultStyle: defaultStyle ?? {} }) + } else { + computedStyle = style + } + + if (typeof children === "function") { + computedChildren = children({ ...values, defaultChildren }) + } else if (children == null) { + computedChildren = defaultChildren + } else { + computedChildren = children + } + + return { + className: computedClassName ?? defaultClassName, + style: + computedStyle ?? defaultStyle + ? { ...defaultStyle, ...computedStyle } + : undefined, + children: computedChildren ?? defaultChildren, + "data-rac": "", + } + }, [ + className, + style, + children, + defaultClassName, + defaultChildren, + defaultStyle, + values, + ]) +} diff --git a/packages/components/src/templates/next/components/internal/LinkButton/LinkButton.stories.tsx b/packages/components/src/templates/next/components/internal/LinkButton/LinkButton.stories.tsx new file mode 100644 index 0000000000..fbfc7746ac --- /dev/null +++ b/packages/components/src/templates/next/components/internal/LinkButton/LinkButton.stories.tsx @@ -0,0 +1,76 @@ +import type { Meta, StoryObj } from "@storybook/react" + +import { LinkButton } from "./LinkButton" + +const BUTTON_SIZES = ["base", "lg"] as const + +const meta: Meta = { + title: "Next/Internal Components/LinkButton", + component: LinkButton, + render: (args) => ( +
+ {BUTTON_SIZES.map((size) => ( + + ))} +
+ ), + argTypes: { + colorScheme: { + options: ["default", "inverse"], + control: { + type: "select", + }, + }, + variant: { + options: ["solid", "outline"], + control: { + type: "select", + }, + }, + }, + parameters: { + themes: { + themeOverride: "Isomer Next", + }, + }, +} +export default meta +type Story = StoryObj + +// Default scenario +export const Default: Story = { + args: { + children: "Work with us", + }, +} + +export const LongerButtonText: Story = { + args: { + children: "slightly longer (link) button text", + }, +} + +export const OutlineVariant: Story = { + args: { + ...Default.args, + variant: "outline", + }, +} + +export const InverseDefaultVariant: Story = { + decorators: [ + (storyFn) =>
{storyFn()}
, + ], + args: { + ...Default.args, + colorScheme: "inverse", + }, +} + +export const InverseOutlineVariant: Story = { + decorators: InverseDefaultVariant.decorators, + args: { + ...OutlineVariant.args, + colorScheme: "inverse", + }, +} diff --git a/packages/components/src/templates/next/components/internal/LinkButton/LinkButton.tsx b/packages/components/src/templates/next/components/internal/LinkButton/LinkButton.tsx new file mode 100644 index 0000000000..ba31dbd476 --- /dev/null +++ b/packages/components/src/templates/next/components/internal/LinkButton/LinkButton.tsx @@ -0,0 +1,35 @@ +"use client" + +import type { ElementType } from "react" +import type { VariantProps } from "tailwind-variants" +import { composeRenderProps } from "react-aria-components" + +import type { LinkProps } from "../Link" +import { buttonStyles } from "../Button" +import { Link } from "../Link" + +export interface LinkButtonProps + extends LinkProps, + VariantProps { + LinkComponent?: ElementType +} + +/** + * Link that looks like a button. + */ +export const LinkButton = ({ + className, + variant, + size, + colorScheme, + ...props +}: LinkButtonProps) => { + return ( + + buttonStyles({ ...renderProps, variant, size, className, colorScheme }), + )} + /> + ) +} diff --git a/packages/components/src/templates/next/components/internal/LinkButton/index.ts b/packages/components/src/templates/next/components/internal/LinkButton/index.ts new file mode 100644 index 0000000000..96efb3c90a --- /dev/null +++ b/packages/components/src/templates/next/components/internal/LinkButton/index.ts @@ -0,0 +1 @@ +export * from "./LinkButton" diff --git a/packages/components/src/templates/next/layouts/Content/Content.tsx b/packages/components/src/templates/next/layouts/Content/Content.tsx index 0fa72ac05a..3535371d9e 100644 --- a/packages/components/src/templates/next/layouts/Content/Content.tsx +++ b/packages/components/src/templates/next/layouts/Content/Content.tsx @@ -198,7 +198,7 @@ const ContentLayout = ({ Back to top diff --git a/packages/components/src/utils/focusRing.ts b/packages/components/src/utils/focusRing.ts new file mode 100644 index 0000000000..9cab3d0cf9 --- /dev/null +++ b/packages/components/src/utils/focusRing.ts @@ -0,0 +1,11 @@ +import { tv } from "~/lib/tv" + +export const focusRing = tv({ + base: "outline outline-offset-2 outline-brand-interaction", + variants: { + isFocusVisible: { + false: "outline-0", + true: "outline-2", + }, + }, +})