diff --git a/src/components/Buttons/button.tsx b/src/components/Buttons/button.tsx index 4fd4739b6b..894b85e762 100644 --- a/src/components/Buttons/button.tsx +++ b/src/components/Buttons/button.tsx @@ -2,11 +2,12 @@ import { forwardRef, JSX, type ButtonHTMLAttributes, + type MouseEvent, type ReactNode, } from 'react'; import { Icon } from '../Icon/icon'; -interface ButtonProperties extends ButtonHTMLAttributes { +export interface ButtonProperties extends ButtonHTMLAttributes { /** * Button contents */ @@ -26,7 +27,7 @@ interface ButtonProperties extends ButtonHTMLAttributes { /** * Optional click handler */ - onClick?: () => void; + onClick?: (event: MouseEvent) => void; /** * Button should be styled as a link? */ diff --git a/src/components/Tabs/tab.scss b/src/components/Tabs/tab.scss new file mode 100644 index 0000000000..0d202c5566 --- /dev/null +++ b/src/components/Tabs/tab.scss @@ -0,0 +1,40 @@ +@use 'sass:math'; +@use '@cfpb/cfpb-design-system/src/elements/abstracts' as *; +@use '@cfpb/cfpb-design-system/src/utilities' as *; + +.tablist { + display: flex; + border-bottom: 1px solid var(--gray-40); + // margin-bottom: -1px; + // position: relative; + // z-index: 10; + + button.tab { + @include heading-4($has-margin-bottom: false); + padding: math.div(math.div($grid-gutter-width, 3), $base-font-size-px) + rem + math.div($grid-gutter-width, $base-font-size-px) + rem; + margin-bottom: -1px; + border: 1px solid transparent; + + &:focus:not(:focus-visible) { + outline: none; + } + + &:focus-visible { + outline-offset: -1px; + } + + &--active { + color: var(--black); + background: var(--gray-5); + text-decoration: none; + pointer-events: none; + border-color: var(--gray-40); + border-bottom-color: transparent; + } + } +} + +.tab-panel { + // border-top: 1px solid var(--gray-40); +} diff --git a/src/components/Tabs/tab.stories.tsx b/src/components/Tabs/tab.stories.tsx new file mode 100644 index 0000000000..0a62665d73 --- /dev/null +++ b/src/components/Tabs/tab.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { Heading, Tab, TabList, TabPanel } from '~/src/index'; + +const meta: Meta = { + title: 'Components (Draft)/Tabs', + tags: ['autodocs'], + component: Tab, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + name: 'Tabs', + render: () => { + const [activeTab, setActiveTab] = useState('one') + const onClick = (event: React.MouseEvent) => { + setActiveTab(event.currentTarget.value); + } + return ( + <> + + + + + + + Panel {activeTab} + + + ) + } +}; + diff --git a/src/components/Tabs/tab.test.tsx b/src/components/Tabs/tab.test.tsx new file mode 100644 index 0000000000..a663ab9abb --- /dev/null +++ b/src/components/Tabs/tab.test.tsx @@ -0,0 +1,20 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { Tab, TabList } from './tab'; + +describe('', () => { + + it('renders tabs', () => { + render( + + One tab + Second tab + + ); + + const tabs = screen.getByRole('tablist'); + expect(tabs).toBeInTheDocument(); + }); + + +}); diff --git a/src/components/Tabs/tab.tsx b/src/components/Tabs/tab.tsx new file mode 100644 index 0000000000..d3fecd3392 --- /dev/null +++ b/src/components/Tabs/tab.tsx @@ -0,0 +1,99 @@ +import classnames from 'classnames'; +import type { HTMLAttributes, ReactNode, MouseEvent } from 'react'; +import type { JSXElement } from '../../types/jsx-element'; +import { Button } from '../Buttons/button'; +import type { ButtonProperties } from '../Buttons/button'; +import './tab.scss'; + +export interface TabProperties extends Omit { + /** + * Id for the tab. Allows it to be associated with its content panel. + */ + id: string; + /** + * Any additional classes for the tab + */ + className?: string; + /** + * Whether this is the active tab + */ + isActive?: boolean; + /** + * Any children to render within the tab. Allows you to wrap any node with tab tag + */ + children?: ReactNode; + /** + * Optional click handler + */ + onClick?: (event: MouseEvent) => void; +} + +export const Tab = ({ + id, + className, + isActive, + onClick = () => null, + children, + ...properties +}: TabProperties): JSXElement => { + const cname = classnames('tab', className, {'tab--active': isActive}); + + return ( + + ); +}; + +export interface TabListProperties extends HTMLAttributes { + className?: string; + children?: ReactNode; +} + +export const TabList = ({ + className, + children, + ...properties +}: TabListProperties): JSXElement => { + const cname = classnames('tablist', className); + + return ( +
+ {children} +
+ ); +}; + +export interface TabPanelProperties extends HTMLAttributes { + id: string; + className?: string; + children?: ReactNode; +} + +export const TabPanel = ({ + id, + className, + children, + ...properties +}: TabPanelProperties): JSXElement => { + const cname = classnames('tab-panel', className); + + return ( +
+ {children} +
+ ); +}; diff --git a/src/index.ts b/src/index.ts index 0931563191..c505308da9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -44,6 +44,7 @@ export { SelectMulti } from './components/Select/select-multi'; export { SelectSingle } from './components/Select/select-single'; export { default as SkipNav } from './components/SkipNav/skip-nav'; export { Summary } from './components/Summaries/summary'; +export { Tab, TabList, TabPanel } from './components/Tabs/tab'; export { Table } from './components/Table/table'; export { TextArea } from './components/TextInput/text-area'; export { TextInput } from './components/TextInput/text-input';