From bf1305666e3e9155eba1b3c3da11bf1689ba8e5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guilherme=20Ara=C3=BAjo?= Date: Sun, 15 Oct 2023 06:54:11 -0300 Subject: [PATCH] feat: create tabs component (#5964) * feat(Tabs): create component * chore: update collaborator guide * fix: use open sans font * refactor: nest tabs trigger class * refactor: format * docs: add radix url * style: ident * refactor: remove decorated component * refactor: use tabs prop * refactor: change tab default value * refactor: remove duplicated defaultValue prop * refactor: remove react globals types * refactor: review * test(Tabs): create initial unit tests * refactor: remove tabs content * fix: fixes * test: fix keys * fix: package-lock --------- Co-authored-by: Claudio Wunder --- COLLABORATOR_GUIDE.md | 12 ++++- .../Common/Tabs/__tests__/index.test.mjs | 53 +++++++++++++++++++ components/Common/Tabs/index.module.css | 20 +++++++ components/Common/Tabs/index.stories.tsx | 42 +++++++++++++++ components/Common/Tabs/index.tsx | 41 ++++++++++++++ package-lock.json | 32 ++++++++++- package.json | 1 + tailwind.config.ts | 1 + 8 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 components/Common/Tabs/__tests__/index.test.mjs create mode 100644 components/Common/Tabs/index.module.css create mode 100644 components/Common/Tabs/index.stories.tsx create mode 100644 components/Common/Tabs/index.tsx diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md index 67dc62ddd2efe..8b019e3d08db2 100644 --- a/COLLABORATOR_GUIDE.md +++ b/COLLABORATOR_GUIDE.md @@ -79,6 +79,7 @@ The Website also uses several other Open Source libraries (not limited to) liste - [PostCSS Simple Vars](https://github.com/postcss/postcss-simple-vars) - [Tailwind][] is used as our CSS Framework and the Foundation of our Design System - [Hero Icons](https://heroicons.com/) is an SVG Icon Library used within our Codebase +- [Radix UI][] is a collection of customizable UI components - [Shiki][] is a Syntax Highlighter used for our Codeboxes - A [Rehype Plugin](https://rehype-pretty-code.netlify.app/) is used here for transforming `pre` and `code` tags into Syntax Highlighted Codeboxes - [MDX][] and Markdown are used for structuring the Content of the Website @@ -195,8 +196,8 @@ Finally, if you're unfamiliar with how to use Tailwind or how to use Tailwind wi > You can apply Tailwind Tokens with Tailwind's `@apply` CSS rule. [Read more about applying Tailwind classes with `@apply`](https://tailwindcss.com/docs/functions-and-directives#apply). > \[!IMPORTANT]\ -> When using IDEs such as Visual Studio Code, we recommend installing the official [Stylelint](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) -> and [Tailwind](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) Extensions.\ +> When using IDEs such as Visual Studio Code, we recommend installing the official [Stylelint](https://marketplace.visualstudio.com/items?itemName=stylelint.vscode-stylelint) +> and [Tailwind](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) Extensions.\ > These are recommended Extensions for IntelliSense, Syntax Highlighting and Error Checking when styling your Component. ### Best practices when creating a Component @@ -475,6 +476,12 @@ Defining a `.vscode` configuration like this also aides browser-only development The npm ecosystem resolution and installation of `peerDependencies` installation [changed in recent versions](https://nodejs.org/en/blog/npm/peer-dependencies#using-peer-dependencies). The project documents what version of `Node.js` and `npm` to use via the [`.nvmrc` file](https://github.com/nodejs/nodejs.org/blob/main/.nvmrc). Not all contributors have tooling to automatically read this file and adhere to the correct version, however. To ensure all contributors install dependencies the same way, a local `.npmrc` file directly configures peerDependency installation behavior. +### Why we use RadixUI? + +- It is a minimalistic component library broken down in individual packages for each Component +- It already handles all WAI-ARIA and Accessibility shortcuts/bindings needed for Interactive Elements +- It allows us to focus on designing interactive Components without the effort of adding all the surrounding sugar and code needed to make the Component accessibility-friendly. + ## Seeking additional clarification A lot of the current structure is due to retro-compatibility, keeping a simple and familiar file structure and keeping files that have historical reasons or needs. @@ -491,3 +498,4 @@ If you're unfamiliar or curious about something, we recommend opening a Discussi [React]: https://react.dev/ [Shiki]: https://github.com/shikijs/shiki [Tailwind]: https://tailwindcss.com/ +[Radix UI]: https://www.radix-ui.com/ diff --git a/components/Common/Tabs/__tests__/index.test.mjs b/components/Common/Tabs/__tests__/index.test.mjs new file mode 100644 index 0000000000000..31957b4f1a228 --- /dev/null +++ b/components/Common/Tabs/__tests__/index.test.mjs @@ -0,0 +1,53 @@ +import * as TabsPrimitive from '@radix-ui/react-tabs'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import Tabs from '../index'; + +describe('Tabs', () => { + const tabs = [ + { key: 'package', label: 'Package Manager' }, + { key: 'prebuilt', label: 'Prebuilt Installer' }, + { key: 'source', label: 'Source Code' }, + ]; + + beforeEach(() => { + render( + + + Package Manager + + + Prebuilt Installer + + + Source Code + + + ); + }); + + it('renders the correct number of tabs', () => { + const tabElements = screen.getAllByRole('tab'); + expect(tabElements).toHaveLength(3); + }); + + it('renders the correct tab content when clicked', async () => { + const user = userEvent.setup(); + + const beforeActiveTabPanel = screen.getAllByRole('tabpanel'); + + expect(beforeActiveTabPanel).toHaveLength(1); + + expect(beforeActiveTabPanel.at(0)).toHaveTextContent('Package Manager'); + + const tabElements = screen.getAllByRole('tab'); + await user.click(tabElements.at(-1)); + + const afterActiveTabPanel = screen.getAllByRole('tabpanel'); + + expect(afterActiveTabPanel).toHaveLength(1); + + expect(afterActiveTabPanel.at(0)).toHaveTextContent('Source Code'); + }); +}); diff --git a/components/Common/Tabs/index.module.css b/components/Common/Tabs/index.module.css new file mode 100644 index 0000000000000..1183a31fa86de --- /dev/null +++ b/components/Common/Tabs/index.module.css @@ -0,0 +1,20 @@ +.tabsList { + @apply flex + gap-1 + font-open-sans; + + .tabsTrigger { + @apply border-b-2 + border-b-transparent + px-1 + pb-[11px] + text-sm + font-semibold + text-neutral-800 + data-[state=active]:border-b-green-600 + data-[state=active]:text-green-600 + dark:text-neutral-200 + dark:data-[state=active]:border-b-green-400 + dark:data-[state=active]:text-green-400; + } +} diff --git a/components/Common/Tabs/index.stories.tsx b/components/Common/Tabs/index.stories.tsx new file mode 100644 index 0000000000000..9453bb8f84415 --- /dev/null +++ b/components/Common/Tabs/index.stories.tsx @@ -0,0 +1,42 @@ +import * as TabsPrimitive from '@radix-ui/react-tabs'; +import type { Meta as MetaObj, StoryObj } from '@storybook/react'; + +import Tabs from './index'; + +type Story = StoryObj; +type Meta = MetaObj; + +export const Default: Story = { + args: { + defaultValue: 'prebuilt', + tabs: [ + { + key: 'package', + label: 'Package Manager', + }, + { + key: 'prebuilt', + label: 'Prebuilt Installer', + }, + { + key: 'source', + label: 'Source Code', + }, + ], + children: ( + <> + + Package Manager + + + Prebuilt Installer + + + Source Code + + + ), + }, +}; + +export default { component: Tabs } as Meta; diff --git a/components/Common/Tabs/index.tsx b/components/Common/Tabs/index.tsx new file mode 100644 index 0000000000000..811b31319e754 --- /dev/null +++ b/components/Common/Tabs/index.tsx @@ -0,0 +1,41 @@ +import * as TabsPrimitive from '@radix-ui/react-tabs'; +import classNames from 'classnames'; +import type { FC, PropsWithChildren } from 'react'; + +import styles from './index.module.css'; + +type Tab = { + key: string; + label: string; +}; + +type TabsProps = { + tabs: Tab[]; + headerClassName?: string; +} & TabsPrimitive.TabsProps; + +const Tabs: FC> = ({ + tabs, + headerClassName, + children, + ...props +}) => ( + + + {tabs.map(tab => ( + + {tab.label} + + ))} + + {children} + +); + +export default Tabs; diff --git a/package-lock.json b/package-lock.json index 8077df7248794..9e92692b08354 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nodevu/core": "~0.1.0", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@types/node": "18.18.3", "@vcarl/remark-headings": "~0.1.0", @@ -4812,7 +4813,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", - "dev": true, "dependencies": { "@babel/runtime": "^7.13.10", "@radix-ui/primitive": "1.0.1", @@ -4925,6 +4925,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", diff --git a/package.json b/package.json index 2dd70005a85c1..c065d20644177 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tabs": "^1.0.4", "@types/node": "18.18.3", "@vcarl/remark-headings": "~0.1.0", "@vercel/analytics": "^1.0.2", diff --git a/tailwind.config.ts b/tailwind.config.ts index b07380d091e06..bc160c2e55e79 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -90,6 +90,7 @@ export default { 900: '#411526', }, white: '#FFFFFF', + transparent: 'transparent', }, fontSize: { xs: ['0.75rem', '1rem'],