Skip to content

Commit

Permalink
feat: add Tabs component (#30)
Browse files Browse the repository at this point in the history
* feat: add a `Tabs` component

* feat: improve Tabs styling and add smaller size

* fix: remove radius on mobile selected tab

* test: add test for Tabs

* docs: add Storybook docs for Tabs

* chore: add Tabs export to index.ts file

* chore: resolve some Tabs styling issues

* test: add click test for tabs

* chore: show not allowed cursor for disabled tabs

* feat: redo the hover state for Tabs

* chore: remove empty CSS ruleset
  • Loading branch information
jaredcwhite authored Jun 20, 2023
1 parent 5d6366c commit f06e207
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 3 deletions.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as Message } from "./src/blocks/Message"
export { default as Toast } from "./src/blocks/Toast"
export { default as Card } from "./src/blocks/Card"
export { default as Grid } from "./src/layout/Grid"
export { default as Tabs } from "./src/navigation/Tabs"
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"markdown-to-jsx": "^6.11.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-tabs": "^6.0.0",
"typescript": "^4.4.2"
},
"devDependencies": {
Expand All @@ -55,11 +56,11 @@
"@testing-library/user-event": "^13.2.1",
"@types/jest": "^27.0.1",
"babel-plugin-named-exports-order": "^0.0.2",
"postcss": "8.4.19",
"postcss-import": "^14.1.0",
"postcss-load-config": "^3.1.4",
"postcss-loader": "7.0.1",
"postcss-nested": "6.0.0",
"postcss": "8.4.19",
"prop-types": "^15.8.1",
"react-scripts": "5.0.1",
"sass": "1.56.1",
Expand Down
140 changes: 140 additions & 0 deletions src/navigation/Tabs.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
.tabs {
--tab-font: var(--bloom-font-alt-sans);
--tab-font-size: var(--bloom-type-body);
--tab-font-size-sm: var(--bloom-type-label);
--tab-font-weight: var(--bloom-font-weight-semibold);
--tab-font-weight-sm: var(--bloom-font-weight-regular);
--tab-background-color: var(--bloom-bg-color-surface);
--tab-label-color: var(--bloom-text-color-light);
--tab-padding-inline: var(--bloom-s6);
--tab-padding-block: var(--bloom-s4);
--tab-padding-inline-sm: var(--bloom-s6);
--tab-padding-block-sm: var(--bloom-s3);
--tab-border-radius: var(--bloom-rounded-lg);
--tab-border-color: var(--bloom-border-color);
--tab-border-width: var(--bloom-border-1);
--tab-hover-background-color: var(--bloom-bg-color-surface-primary);
--tab-active-label-color: var(--bloom-color-on-surface);
--tab-active-indicator-width: var(--bloom-border-2);
--tab-active-indicator-color: var(--bloom-color-primary);
--tab-panel-padding: var(--bloom-spacer-section);
--tab-panel-background-color: var(--bloom-bg-color-surface);
}

.tabs-tablist {
display: flex;
font-family: var(--tab-font);
font-size: var(--tab-font-size);
font-weight: var(--tab-font-weight);
margin: 0;
margin-block-end: -1px;
padding: 0;
list-style-type: none;

&[data-size="sm"] {
font-size: var(--tab-font-size-sm);
font-weight: var(--tab-font-weight-sm);
}
}

.tabs-tab {
background-color: var(--tab-background-color);
color: var(--tab-label-color);
padding-inline: var(--tab-padding-inline);
padding-block: var(--tab-padding-block) calc(var(--tab-padding-block) - var(--tab-active-indicator-width));

border: var(--tab-border-width) solid var(--tab-border-color);
border-radius: 0;

cursor: pointer;
position: relative;

[data-size="sm"] > & {
padding-inline: var(--tab-padding-inline-sm);
padding-block: var(--tab-padding-block-sm) calc(var(--tab-padding-block-sm) - var(--tab-active-indicator-width));
}

&:focus-visible {
outline: 2px solid var(--bloom-color-accent-cool);
outline-offset: 2px;
}

&:hover {
color: var(--tab-active-indicator-color);
background-color: var(--tab-hover-background-color);
}

&:not(:last-of-type) {
border-right-width: 0;
}

&:first-of-type {
border-top-left-radius: var(--tab-border-radius);
}

&:last-of-type {
border-top-right-radius: var(--tab-border-radius);
}

&[aria-selected="true"] {
border-bottom-color: var(--tab-active-indicator-color);
border-bottom-width: var(--tab-active-indicator-width);
z-index: 1;
}

&:not(:hover)[aria-selected="true"] {
color: var(--tab-active-label-color);
}

&[aria-disabled="true"] {
color: var(--bloom-text-color-disabled);
border-bottom-width: var(--tab-border-width);
cursor: not-allowed;
}
}

.tabs-panel {
padding: var(--tab-panel-padding);
background-color: var(--tab-panel-background-color);
border: var(--tab-border-width) solid var(--tab-border-color);

&:not(.is-active) {
display: none;
}
}

@media (max-width: 640px) {
.tabs-tablist {
flex-direction: column;
}

.tabs-tab {
border-color: var(--tab-border-color);
border-bottom-width: 0;

&:first-of-type {
border-top-right-radius: var(--tab-border-radius);
}

&:last-of-type {
border-top-right-radius: 0;
border-bottom-left-radius: var(--tab-border-radius);
border-bottom-right-radius: var(--tab-border-radius);
border-bottom-width: var(--tab-border-width);
}

&:not(:last-of-type) {
border-right-width: var(--tab-border-width);
border-bottom-width: 0;
}

&[aria-selected="true"] {
border-bottom-color: var(--tab-border-color);
box-shadow: inset calc(var(--tab-active-indicator-width) + var(--tab-border-width)) 0px 0px var(--tab-active-indicator-color);
}
}

.tabs-panel {
margin-block-start: var(--bloom-spacer-label);
}
}
92 changes: 92 additions & 0 deletions src/navigation/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from "react"

import "./Tabs.scss"

import {
Tab as ReactTab,
Tabs as ReactTabs,
TabList as ReactTabList,
TabPanel as ReactTabPanel,
} from "react-tabs"

export interface TabsProps {
children: React.ReactNode
className?: string
defaultFocus?: boolean
defaultIndex?: number
disabledTabClassName?: string
domRef?: (node?: HTMLElement) => void
focusTabOnClick?: boolean
forceRenderTabPanel?: boolean
onSelect?: (index: number, last: number, event: Event) => boolean | void
selectedIndex?: number
selectedTabClassName?: string
selectedTabPanelClassName?: string
}

const Tabs = (props: TabsProps) => {
const className = ["tabs"]
if (props.className) className.push(props.className)
const focusTab = typeof props.focusTabOnClick !== "undefined" ? props.focusTabOnClick : false

return (
<ReactTabs {...props} focusTabOnClick={focusTab} className={className.join(" ")} />
)
}

export interface TabProps {
children: React.ReactNode
className?: string
disabled?: boolean
disabledClassName?: string
selectedClassName?: string
tabIndex?: string
}

const Tab = (props: TabProps) => {
const className = ["tabs-tab"]
if (props.className) className.push(props.className)
return (
<ReactTab selectedClassName="is-active" {...props} className={className} />
)
}

Tab.tabsRole = "Tab"

export interface TabListProps {
children: React.ReactNode
size?: "sm" | "base"
className?: string
}

const TabList = (props: TabListProps) => {
const className = ["tabs-tablist"]
if (props.className) className.push(props.className)

return <ReactTabList data-size={props.size || "base"} className={className} children={props.children} />
}

TabList.tabsRole = "TabList"

export interface TabPanelProps {
children: React.ReactNode
className?: string
forceRender?: boolean
selectedClassName?: string
}

const TabPanel = (props: TabPanelProps) => {
const className = ["tabs-panel"]
if (props.className) className.push(props.className)
return (
<ReactTabPanel selectedClassName="is-active" {...props} className={className} />
)
}

TabPanel.tabsRole = "TabPanel"

Tabs.Tab = Tab
Tabs.TabList = TabList
Tabs.TabPanel = TabPanel

export {Tabs as default, Tab, TabList, TabPanel}
45 changes: 45 additions & 0 deletions src/navigation/__stories__/Tabs.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ArgsTable } from "@storybook/addon-docs"
import Tabs from "../Tabs"
import { Swatch } from "../../../documentation/components/Swatch.tsx"

# &lt;Tabs /&gt;

## Properties

<ArgsTable of={Tabs} />

## Tab List Properties

<ArgsTable of={Tabs.TabList} />

## Tab Properties

<ArgsTable of={Tabs.Tab} />

## Tab Panel Properties

<ArgsTable of={Tabs.TabPanel} />

## Theme Variables

| Name | Description | Default |
| ------------------------------ | ----------------------------------------- | ------------------------------ |
| `--tab-font` | Font family | `--bloom-font-alt-sans` |
| `--tab-font-size` | Font size | `--bloom-type-body` |
| `--tab-font-size-sm` | Font size on mobile screens | `--bloom-type-label` |
| `--tab-font-weight` | Font weight | `--bloom-font-weight-semibold` |
| `--tab-font-weight-sm` | Font weight on mobile screens | `--bloom-font-weight-regular` |
| `--tab-background-color` | <Swatch color="bloom-bg-color-surface" border={true} /> | `--bloom-bg-color-surface` |
| `--tab-label-color` | <Swatch color="bloom-text-color-light" /> | `--bloom-text-color-light` |
| `--tab-padding-inline` | Horizontal spacing within tab interior | `--bloom-s6` |
| `--tab-padding-block` | Vertical spacing within tab interior | `--bloom-s4` |
| `--tab-padding-inline-sm` | Horizontal spacing on mobile screens | `--bloom-s6` |
| `--tab-padding-block-sm` | Vertical spacing on mobile screens | `--bloom-s3` |
| `--tab-border-radius` | Tab border radius | `--bloom-rounded-lg` |
| `--tab-border-color` | <Swatch color="bloom-border-color" /> | `--bloom-border-color` |
| `--tab-border-width` | Tab border radius | `--bloom-border-1` |
| `--tab-active-label-color` | <Swatch color="bloom-color-on-surface" /> | `--bloom-color-on-surface` |
| `--tab-active-indicator-width` | Width of the colorful border | `--bloom-border-2` |
| `--tab-active-indicator-color` | <Swatch color="bloom-color-primary" /> | `--bloom-color-primary` |
| `--tab-panel-padding` | Spacing within the panel interior | `--bloom-spacer-section` |
| `--tab-panel-background-color` | <Swatch color="bloom-bg-color-surface" border={true} /> | `--bloom-bg-color-surface` |
Loading

0 comments on commit f06e207

Please sign in to comment.