Skip to content

Commit

Permalink
feat(TabsNav): add TabsNav component (#91)
Browse files Browse the repository at this point in the history
  • Loading branch information
arturbien authored Feb 23, 2024
1 parent b6ec6b5 commit be61ab5
Show file tree
Hide file tree
Showing 18 changed files with 433 additions and 150 deletions.
15 changes: 15 additions & 0 deletions apps/playground/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import {
AccessibilityIcon,
CameraIcon,
Expand Down Expand Up @@ -43,6 +44,8 @@ import {
PopoverTrigger,
Separator,
Strong,
TabsNavLink,
TabsNavRoot,
Text,
TextArea,
TextFieldInput,
Expand Down Expand Up @@ -174,10 +177,14 @@ const WhopSVG = () => {
);
};

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { users } from '../demo/users';
import styles from './page.module.css';

export default function Demo() {
const pathname = usePathname();

return (
<html lang="en" suppressHydrationWarning>
<body className={styles.body}>
Expand Down Expand Up @@ -330,6 +337,14 @@ export default function Demo() {
</Flex>
</aside>
<main className={styles.main}>
<TabsNavRoot>
<TabsNavLink asChild active={pathname == '/dashboard'}>
<Link href="/dashboard">Dashboard</Link>
</TabsNavLink>
<TabsNavLink asChild active={pathname == '/demo'}>
<Link href="/demo">Demo</Link>
</TabsNavLink>
</TabsNavRoot>
<Box pl="7" pr="6">
<Flex
pt="4"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';

import React from 'react';
import { TabsNav } from '../../../src/components';
import { tabsNavPropDefs } from '../../../src/components/tabs-nav.props';

// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
const meta = {
title: 'Components/TabsNav',
component: TabsNav.Root,
args: {
size: tabsNavPropDefs.size.default,
},
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/react/writing-docs/autodocs
tags: ['autodocs'],
} satisfies Meta<typeof TabsNav.Root>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args

export const Default: Story = {
render: (args) => (
<div style={{ width: 600 }}>
<TabsNav.Root {...args}>
<TabsNav.Link active={true} href="#">
Account
</TabsNav.Link>
<TabsNav.Link href="#">Documents</TabsNav.Link>
<TabsNav.Link href="#">Settings</TabsNav.Link>
</TabsNav.Root>
</div>
),
};
1 change: 1 addition & 0 deletions packages/frosted-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@radix-ui/react-direction": "^1.0.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-portal": "^1.0.4",
"@radix-ui/react-progress": "^1.0.3",
Expand Down
144 changes: 144 additions & 0 deletions packages/frosted-ui/src/components/base-tabs-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
.fui-BaseTabsList {
display: flex;
overflow-x: auto;
white-space: nowrap;

scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}

.fui-BaseTabsTrigger {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
flex-shrink: 0;
position: relative;
user-select: none;
}

.fui-BaseTabsTriggerInner,
.fui-BaseTabsTriggerInnerHidden {
display: flex;
align-items: center;
justify-content: center;
}

.fui-BaseTabsTriggerInner {
position: absolute;

:where(.fui-BaseTabsTrigger[data-state='inactive'], .fui-TabsNavLink:not([data-active])) & {
letter-spacing: var(--tabs-trigger-inactive-letter-spacing);
word-spacing: var(--tabs-trigger-inactive-word-spacing);
}

:where(.fui-BaseTabsTrigger[data-state='active'], .fui-TabsNavLink[data-active]) & {
font-weight: var(--font-weight-medium);
letter-spacing: var(--tabs-trigger-active-letter-spacing);
word-spacing: var(--tabs-trigger-active-word-spacing);
}
}

.fui-BaseTabsTriggerInnerHidden {
visibility: hidden;
font-weight: var(--font-weight-medium);
letter-spacing: var(--tabs-trigger-active-letter-spacing);
word-spacing: var(--tabs-trigger-active-word-spacing);
}

.fui-BaseTabsContent {
position: relative;
outline: 0;
}

/***************************************************************************************************
* *
* SIZES *
* *
***************************************************************************************************/

.fui-BaseTabsTrigger {
padding-left: var(--tabs-trigger-padding-x);
padding-right: var(--tabs-trigger-padding-x);
}

.fui-BaseTabsTriggerInner,
.fui-BaseTabsTriggerInnerHidden {
padding: var(--tabs-trigger-inner-padding-y)
var(--tabs-trigger-inner-padding-x);
border-radius: var(--tabs-trigger-inner-border-radius);
}

@breakpoints {
.fui-BaseTabsList {
&:where(.fui-r-size-1) {
height: 36px;
font-size: var(--font-size-1);
line-height: var(--line-height-1);
letter-spacing: var(--letter-spacing-1);
--tabs-trigger-padding-x: var(--space-1);
--tabs-trigger-inner-padding-x: calc(1.5 * var(--space-1));
--tabs-trigger-inner-padding-y: var(--space-1);
--tabs-trigger-inner-border-radius: var(--radius-2);
}
&:where(.fui-r-size-2) {
height: var(--space-7);
font-size: var(--font-size-2);
line-height: var(--line-height-2);
letter-spacing: var(--letter-spacing-2);
--tabs-trigger-padding-x: var(--space-1);
--tabs-trigger-inner-padding-x: calc(1.25 * var(--space-2));
--tabs-trigger-inner-padding-y: var(--space-1);
--tabs-trigger-inner-border-radius: var(--radius-3);
}
}
}

/***************************************************************************************************
* *
* VARIANTS *
* *
***************************************************************************************************/

.fui-BaseTabsList {
box-shadow: inset 0 -1px 0 0 var(--gray-a5);
}

.fui-BaseTabsTrigger {
color: var(--gray-a11);

@media (hover: hover) {
&:where(:hover) {
color: var(--gray-12);
}
&:where(:hover) :where(.fui-BaseTabsTriggerInner) {
background-color: var(--gray-a3);
}
&:where(:focus-visible:hover) :where(.fui-BaseTabsTriggerInner) {
background-color: var(--accent-a3);
}
}
&:where([data-state='active'], [data-active]) {
color: var(--gray-12);
}
&:where(:focus-visible) :where(.fui-BaseTabsTriggerInner) {
outline: 2px solid var(--color-focus-root);
outline-offset: -2px;
}
&:where([data-state='active'], [data-active])::before {
box-sizing: border-box;
content: '';
height: 2px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: var(--accent-10);
}
}

.fui-BaseTabsContent:where(:focus-visible) {
outline: 2px solid var(--color-focus-root);
}
11 changes: 11 additions & 0 deletions packages/frosted-ui/src/components/base-tabs-list.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PropDef } from '../helpers';

const sizes = ['1', '2'] as const;

const baseTabsListPropDefs = {
size: { type: 'enum', values: sizes, default: '2', responsive: true },
} satisfies {
size: PropDef<(typeof sizes)[number]>;
};

export { baseTabsListPropDefs };
2 changes: 2 additions & 0 deletions packages/frosted-ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ export {
} from './table';
export * from './table.props';
export { Tabs, TabsContent, TabsList, TabsRoot, TabsTrigger } from './tabs';
export { TabsNav, TabsNavLink, TabsNavRoot } from './tabs-nav';
export * from './tabs-nav.props';
export * from './tabs.props';
// export * from './toast';
// export * from './toggle';
Expand Down
5 changes: 5 additions & 0 deletions packages/frosted-ui/src/components/tabs-nav.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@import './base-tabs-list.css';

.fui-TabsNavItem {
display: flex;
}
10 changes: 10 additions & 0 deletions packages/frosted-ui/src/components/tabs-nav.props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { asChildProp } from '../helpers';

const tabsNavLinkPropDefs = {
asChild: asChildProp,
} satisfies {
asChild: typeof asChildProp;
};

export { baseTabsListPropDefs as tabsNavPropDefs } from './base-tabs-list.props';
export { tabsNavLinkPropDefs };
121 changes: 121 additions & 0 deletions packages/frosted-ui/src/components/tabs-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
'use client';

import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import classNames from 'classnames';
import * as React from 'react';
import {
GetPropDefTypes,
MarginProps,
extractMarginProps,
getSubtree,
withBreakpoints,
withMarginProps,
} from '../helpers';
import { tabsNavLinkPropDefs, tabsNavPropDefs } from './tabs-nav.props';

type TabsNavRootElement = React.ElementRef<typeof NavigationMenu.Root>;
type TabsNavOwnProps = GetPropDefTypes<typeof tabsNavPropDefs>;
interface TabsNavRootProps
extends Omit<
React.ComponentPropsWithoutRef<typeof NavigationMenu.Root>,
| 'asChild'
| 'orientation'
| 'defauValue'
| 'value'
| 'onValueChange'
| 'delayDuration'
| 'skipDelayDuration'
>,
MarginProps,
TabsNavOwnProps {}
const TabsNavRoot = React.forwardRef<TabsNavRootElement, TabsNavRootProps>(
(props, forwardedRef) => {
const { rest: marginRest, ...marginProps } = extractMarginProps(props);
const {
children,
className,
size = tabsNavPropDefs.size.default,
...rootProps
} = marginRest;

return (
<NavigationMenu.Root
className="fui-TabsNavRoot"
{...rootProps}
asChild={false}
ref={forwardedRef}
orientation="horizontal"
>
<NavigationMenu.List
className={classNames(
'fui-reset',
'fui-BaseTabsList',
'fui-TabsNavList',
className,
withMarginProps(marginProps),
withBreakpoints(size, 'fui-r-size'),
)}
>
{children}
</NavigationMenu.List>
</NavigationMenu.Root>
);
},
);
TabsNavRoot.displayName = 'TabsNavRoot';

type TabsNavLinkElement = React.ElementRef<typeof NavigationMenu.Link>;
type TabsNavLinkOwnProps = GetPropDefTypes<typeof tabsNavLinkPropDefs>;
interface TabsNavLinkProps
extends Omit<
React.ComponentPropsWithoutRef<typeof NavigationMenu.Link>,
'onSelect'
>,
TabsNavLinkOwnProps {}
const TabsNavLink = React.forwardRef<TabsNavLinkElement, TabsNavLinkProps>(
(props, forwardedRef) => {
const { asChild, children, className, ...linkProps } = props;

return (
<NavigationMenu.Item className="fui-TabsNavItem">
<NavigationMenu.Link
{...linkProps}
ref={forwardedRef}
className={classNames(
'fui-reset',
'fui-BaseTabsTrigger',
'fui-TabsNavLink',
className,
)}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onSelect={() => {}}
asChild={asChild}
>
{getSubtree({ asChild, children }, (children) => (
<>
<span className="fui-BaseTabsTriggerInner fui-TabsNavLinkInner">
{children}
</span>
<span className="fui-BaseTabsTriggerInnerHidden fui-TabsNavLinkInnerHidden">
{children}
</span>
</>
))}
</NavigationMenu.Link>
</NavigationMenu.Item>
);
},
);

TabsNavLink.displayName = 'TabsNavLink';

const TabsNav = Object.assign(
{},
{
Root: TabsNavRoot,
Link: TabsNavLink,
},
);

export { TabsNav, TabsNavLink, TabsNavRoot };
export type { TabsNavLinkProps, TabsNavRootProps };
Loading

0 comments on commit be61ab5

Please sign in to comment.