Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Tabs): ✨ New Tabs Component #876

Merged
merged 39 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
00e6ea0
generate initial component
Magnusrm Sep 29, 2023
4f2387d
create basic components
Magnusrm Sep 29, 2023
12be6eb
add simple story
Magnusrm Sep 29, 2023
d9c330a
update story
Magnusrm Sep 29, 2023
c3d9cdc
add Tab item styling and `active` functionality
Magnusrm Sep 30, 2023
568bcc1
fix icon size
Magnusrm Sep 30, 2023
6d85402
render content based on selection
Magnusrm Sep 30, 2023
1f02ebe
rename copypasta
Magnusrm Sep 30, 2023
c242756
:lipstick: color changes
Magnusrm Oct 2, 2023
4111089
add sizing
Magnusrm Oct 2, 2023
8371de3
update icon sizing and add story
Magnusrm Oct 2, 2023
3494c6f
update aria roles
Magnusrm Oct 2, 2023
851d8e1
add tests
Magnusrm Oct 2, 2023
f985b11
add jsdoc and cleanup some comments
Magnusrm Oct 2, 2023
6fe7d10
Merge branch 'main' into feat/tabs-component
Magnusrm Oct 2, 2023
88edfcf
make tabs not move on hover
Magnusrm Oct 2, 2023
283847b
remove icons for preview story
Magnusrm Oct 2, 2023
d12c45c
remove unused css
Magnusrm Oct 3, 2023
b192a9a
rename TabItem to Tab
Magnusrm Oct 3, 2023
22678b4
rename TabListItem to TabList
Magnusrm Oct 3, 2023
72dacdf
rename rest of TabItemList to TabList
Magnusrm Oct 3, 2023
74d4ff1
remove `buttonProps` and update `useTab`
Magnusrm Oct 3, 2023
09fcc82
use `ReactNode` instead of `string` for `Tab` children
Magnusrm Oct 3, 2023
8144988
update jsdoc and useTab
Magnusrm Oct 3, 2023
59c8968
use Prview.args in Preview story
Magnusrm Oct 3, 2023
707902a
add padding to tab content
Magnusrm Oct 4, 2023
fb37d7d
change bottom border to pseudo element
Magnusrm Oct 4, 2023
568eb8e
update default state color
Magnusrm Oct 4, 2023
3190bc8
add content text size
Magnusrm Oct 4, 2023
7f82345
remove use of `SvgIcon` component
Magnusrm Oct 4, 2023
da5cb7b
Merge branch 'main' into feat/tabs-component
Magnusrm Oct 4, 2023
619f035
add accessible title in case only icons
Magnusrm Oct 4, 2023
5b803de
Merge branch 'main' into feat/tabs-component
Magnusrm Oct 5, 2023
830668c
Merge branch 'main' into feat/tabs-component
Magnusrm Oct 5, 2023
4b1ee56
update storybook
Magnusrm Oct 5, 2023
36f67a0
update storybook icon titles
Magnusrm Oct 5, 2023
b63a91c
remove iconTitle and add title to akselicons in storybook
Magnusrm Oct 5, 2023
2f43e94
Merge branch 'main' into feat/tabs-component
Magnusrm Oct 5, 2023
647684a
check if hover is available
Magnusrm Oct 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/Overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ V1 er klar når følgende komponenter er markert som "✅ Felles":
| Search | 🚸 Ikke påbegynt | Figma - Search | [Github - Search](https://github.com/digdir/designsystem/issues/88) |
| [Switch](/docs/felles-switch--docs) | ✅ Felles | [Figma - Switch](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?type=design&node-id=17755-6024&mode=design&t=ztPUyfbDSQjlpEzv-0) | [Github - Switch](https://github.com/digdir/designsystem/issues/89) |
| [Table](/docs/altinn-table--docs) | 🔵 Altinn | Figma - Table | [Github - Table](https://github.com/digdir/designsystem/issues/90) |
| [Tabs](/docs/avviklet-legacytabs--docs) | 🔵 Altinn | [Figma - Tabs](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?type=design&node-id=9551%3A54208&t=Rlfq5UyNZBL69dFr-1) | [Github - Tabs](https://github.com/digdir/designsystem/issues/91) |
| [Tabs](/docs/felles-tabs--docs) | ✅ Felles | [Figma - Tabs](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?type=design&node-id=9551%3A54208&t=Rlfq5UyNZBL69dFr-1) | [Github - Tabs](https://github.com/digdir/designsystem/issues/91) |
| [Tag](/docs/felles-tag--docs) | ✅ Felles | [Figma - Tag](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=10185%3A59053&t=7Q2N4sUdQGhFZrPh-1) | [Github - Tag](https://github.com/digdir/designsystem/issues/322) |
| [Textarea](/docs/felles-textarea--docs) | ✅ Felles | [Figma - Text area](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=6632%3A21873&t=7Q2N4sUdQGhFZrPh-1) | [Github - Textarea](https://github.com/digdir/designsystem/issues/323) |
| [Textfield](/docs/felles-textfield--docs) | ✅ Felles | [Figma - Text Field](https://www.figma.com/file/vpM9dqqQPHqU6ogfKp5tlr/Felles-komponenter?node-id=6632%3A22228&t=7Q2N4sUdQGhFZrPh-1) | [Github - Textfield](https://github.com/digdir/designsystem/issues/92) |
Expand Down
84 changes: 84 additions & 0 deletions packages/react/src/components/Tabs/Tab/Tab.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
.tabItem {
--fdsc-icon-size: var(--fds-sizing-4);
--fdsc-typography-font-family: inherit;
--fdsc-bottom-border-color: transparent;

display: flex;
flex-direction: row;
box-sizing: border-box;
gap: var(--fds-spacing-1);
justify-content: center;
text-align: center;
align-items: center;
padding: var(--fds-spacing-2) var(--fds-spacing-3);
border: none;
border-radius: 0;
background-color: transparent;
cursor: pointer;
color: var(--fds-semantic-text-neutral-subtle);
position: relative;
}

.icon > svg {
display: flex;
height: 1.75em;
width: auto;
}

.tabItem.small {
--fdsc-icon-size: var(--fds-sizing-5);

font: var(--fds-typography-interactive-small);
font-family: var(--fdsc-typography-font-family);
padding: var(--fds-spacing-2) var(--fds-spacing-4);
}

.tabItem.medium {
--fdsc-icon-size: var(--fds-sizing-6);

font: var(--fds-typography-interactive-medium);
font-family: var(--fdsc-typography-font-family);
padding: var(--fds-spacing-3) var(--fds-spacing-5);
}

.tabItem.large {
--fdsc-icon-size: var(--fds-sizing-7);

font: var(--fds-typography-interactive-large);
font-family: var(--fdsc-typography-font-family);
padding: var(--fds-spacing-4) var(--fds-spacing-6);
}

@media (hover: hover) and (pointer: fine) {
.tabItem:hover {
--fdsc-bottom-border-color: var(--fds-semantic-border-neutral-subtle);

color: var(--fds-semantic-text-neutral-default);
}
}

.tabItem.isActive {
--fdsc-bottom-border-color: var(--fds-semantic-border-action-default);

color: var(--fds-semantic-text-action-default);
}

.tabItem:focus-visible {
--fdsc-bottom-border-color: var(--fds-semantic-text-neutral-default);

background: var(--fds-semantic-border-focus-outline);
color: var(--fds-semantic-text-neutral-default);
outline: none;
}

.tabItem::after {
content: '';
display: block;
height: 3px;
width: 100%;
border-radius: var(--fds-border_radius-full);
background-color: var(--fdsc-bottom-border-color);
position: absolute;
bottom: 0;
left: 0;
}
28 changes: 28 additions & 0 deletions packages/react/src/components/Tabs/Tab/Tab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { TabList } from '../TabList';
import { Tabs } from '..';

import { Tab } from '.';

const user = userEvent.setup();

describe('TabItem', () => {
test('item renders with correct aria attributes', async () => {
render(
<Tabs defaultValue='value1'>
<TabList>
<Tab value='value1'>Tab 1</Tab>
<Tab value='value2'>Tab 2</Tab>
</TabList>
</Tabs>,
);

const tab = screen.getByRole('tab', { name: 'Tab 2' });
expect(tab).toHaveAttribute('aria-selected', 'false');
await user.click(tab);
expect(tab).toHaveAttribute('aria-selected', 'true');
});
});
38 changes: 38 additions & 0 deletions packages/react/src/components/Tabs/Tab/Tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { HTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import cn from 'classnames';

import { RovingTabindexItem } from '../../../utility-components/RovingTabIndex';

import classes from './Tab.module.css';
import { useTabItem } from './useTab';

export type TabProps = {
/** Value that will be set in the `Tabs` components state when the tab is activated*/
value: string;
/** Icon to display */
icon?: React.ReactNode;
} & Omit<HTMLAttributes<HTMLButtonElement>, 'value'>;

export const Tab = forwardRef<HTMLButtonElement, TabProps>((props, ref) => {
const { children, className, icon, ...rest } = props;
const { active, size = 'medium', ...useTabRest } = useTabItem(props);

return (
<RovingTabindexItem
{...rest}
{...useTabRest}
as={'button'}
className={cn(
classes.tabItem,
classes[size],
active && classes.isActive,
className,
)}
ref={ref}
>
{icon && <span className={classes.icon}>{icon}</span>}
{children}
</RovingTabindexItem>
);
});
1 change: 1 addition & 0 deletions packages/react/src/components/Tabs/Tab/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Tab';
34 changes: 34 additions & 0 deletions packages/react/src/components/Tabs/Tab/useTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { HTMLAttributes } from 'react';
import { useContext, useId } from 'react';

import { TabsContext } from '../Tabs';

import type { TabProps } from './Tab';

type UseTab = (props: TabProps) => {
active: boolean;
size?: 'small' | 'medium' | 'large';
} & Pick<
HTMLAttributes<HTMLButtonElement>,
'id' | 'aria-selected' | 'role' | 'onClick'
>;

/** Handles props for `Tab` in context with `Tabs` */
export const useTabItem: UseTab = (props: TabProps) => {
const { value, ...rest } = props;
const tabs = useContext(TabsContext);
const active = tabs.value == value;
const buttonId = `tab-${useId()}`;

return {
...rest,
active: active,
size: tabs?.size,
id: buttonId,
'aria-selected': active,
role: 'tab',
onClick: () => {
tabs.onChange?.(value);
},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
.tabContent {
--fdsc-typography-font-family: inherit;

font-family: var(--fdsc-typography-font-family);
}

.tabContent.small {
padding: var(--fds-spacing-4);
}

.tabContent.medium {
padding: var(--fds-spacing-5);
}

.tabContent.large {
padding: var(--fds-spacing-6);
}

.tabContent.onlyText.small {
font: var(--fds-typography-interactive-small);
font-family: var(--fdsc-typography-font-family);
}

.tabContent.onlyText.medium {
font: var(--fds-typography-interactive-medium);
font-family: var(--fdsc-typography-font-family);
}

.tabContent.onlyText.large {
font: var(--fds-typography-interactive-large);
font-family: var(--fdsc-typography-font-family);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { render, screen } from '@testing-library/react';

import { Tabs } from '..';

import { TabContent } from '.';

describe('TabContent', () => {
test('renders ReactNodes as children when TabContents value is selected', () => {
render(
<Tabs defaultValue='value1'>
<TabContent value='value1'>
<div>content 1</div>
</TabContent>
</Tabs>,
);

const content = screen.queryByText('content 1');
expect(content).toBeInTheDocument();
});
});
39 changes: 39 additions & 0 deletions packages/react/src/components/Tabs/TabContent/TabContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { HTMLAttributes } from 'react';
import React, { forwardRef, useContext } from 'react';
import cn from 'classnames';

import { TabsContext } from '../Tabs';

import classes from './TabContent.module.css';

export type TabContentProps = {
/** When this value is selected as the current state, render this `TabContent` component*/
value: string;
} & Omit<HTMLAttributes<HTMLDivElement>, 'value'>;

export const TabContent = forwardRef<HTMLDivElement, TabContentProps>(
({ children, value, ...rest }, ref) => {
const { value: tabsValue, size = 'medium' } = useContext(TabsContext);
const active = value == tabsValue;
const onlyText = typeof children === 'string';

return (
<>
{active && (
<div
{...rest}
className={cn(
classes[size],
classes.tabContent,
onlyText && classes.onlyText,
rest.className,
)}
ref={ref}
>
{children}
</div>
)}
</>
);
},
);
1 change: 1 addition & 0 deletions packages/react/src/components/Tabs/TabContent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TabContent';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.tabItemList {
display: flex;
flex-direction: row;
border-bottom: var(--fds-border_width-default) solid var(--fds-semantic-border-neutral-subtle);
}
29 changes: 29 additions & 0 deletions packages/react/src/components/Tabs/TabList/TabList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { Tab } from '../Tab';

import { TabList } from '.';

const user = userEvent.setup();

describe('TabItemList', () => {
test('can navigate tabs with keyboard', async () => {
render(
<TabList>
<Tab value='value1'>Tab 1</Tab>
<Tab value='value2'>Tab 2</Tab>
</TabList>,
);

const tab1 = screen.getByRole('tab', { name: 'Tab 1' });
const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
await user.tab();
expect(tab1).toHaveFocus();
await user.type(tab1, '{arrowright}');
expect(tab2).toHaveFocus();
await user.type(tab2, '{arrowleft}');
expect(tab1).toHaveFocus();
});
});
23 changes: 23 additions & 0 deletions packages/react/src/components/Tabs/TabList/TabList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { HTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import cn from 'classnames';

import { RovingTabindexRoot } from '../../../utility-components/RovingTabIndex';

import classes from './TabList.module.css';

export const TabList = forwardRef<
HTMLDivElement,
HTMLAttributes<HTMLDivElement>
>(({ children, ...rest }, ref) => {
return (
<RovingTabindexRoot
{...rest}
role='tablist'
className={cn(classes.tabItemList, rest.className)}
ref={ref}
>
{children}
</RovingTabindexRoot>
);
});
1 change: 1 addition & 0 deletions packages/react/src/components/Tabs/TabList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TabList';
Loading
Loading