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 17 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tabContent {
/* placeholder */
}
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
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();
});
});
33 changes: 33 additions & 0 deletions packages/react/src/components/Tabs/TabContent/TabContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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 = {
/** Value of the content to be dislpayed */
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
value?: string;
} & Omit<HTMLAttributes<HTMLDivElement>, 'value'>;

export const TabContent = forwardRef<HTMLDivElement, TabContentProps>(
({ children, value, ...rest }, ref) => {
const tabs = useContext(TabsContext);
const active = value == tabs.value;

return (
<>
{active && (
<div
{...rest}
className={cn(classes.tabContent, 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';
70 changes: 70 additions & 0 deletions packages/react/src/components/Tabs/TabItem/TabItem.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.tabItem {
--fdsc-icon-size: var(--fds-sizing-4);
--fdsc-typography-font-family: inherit;

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-bottom: var(--fds-border_width-active) solid transparent;
border-radius: 0;
background-color: transparent;
cursor: pointer;
color: var(--fds-semantic-text-neutral-default);
letter-spacing: var(--typography-default-letter-spacing);
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
}

.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);
}

.tabItem svg {
overflow: visible;
}

.icon {
display: inline-block;
height: var(--fdsc-icon-size);
width: var(--fdsc-icon-size);
}

.tabItem:hover {
border-bottom: var(--fds-border_width-active) solid var(--fds-semantic-border-neutral-subtle);
}

.tabItem.isActive {
color: var(--fds-semantic-text-action-default);
border-bottom: var(--fds-border_width-active) solid var(--fds-semantic-border-action-default);
}

.tabItem:focus-visible {
background: var(--fds-semantic-border-focus-outline);
color: var(--fds-semantic-text-neutral-default);
border-bottom: var(--fds-border_width-active) solid var(--fds-semantic-border-focus-boxshadow);
outline: none;
}
28 changes: 28 additions & 0 deletions packages/react/src/components/Tabs/TabItem/TabItem.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 { TabItemList } from '../TabItemList';
import { Tabs } from '..';

import { TabItem } from '.';

const user = userEvent.setup();

describe('TabItem', () => {
test('item renders with correct aria attributes', async () => {
render(
<Tabs defaultValue='value1'>
<TabItemList>
<TabItem value='value1'>Tab 1</TabItem>
<TabItem value='value2'>Tab 2</TabItem>
</TabItemList>
</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');
});
});
48 changes: 48 additions & 0 deletions packages/react/src/components/Tabs/TabItem/TabItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { HTMLAttributes } from 'react';
import React, { forwardRef } from 'react';
import cn from 'classnames';

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

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

export type TabItemProps = {
/** Value of the TabItem */
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
value: string;
/** The Children */
children?: string;
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
/** Icon to display */
icon?: React.ReactNode;
} & Omit<HTMLAttributes<HTMLButtonElement>, 'children' | 'value'>;

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

return (
<RovingTabindexItem
{...rest}
{...buttonProps}
as={'button'}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also just pass our own Button into the as prop here. It would save us for a lot of repitition. But maybe save that for later as we have planned to do changes to the Button and its api now 😅

className={cn(
classes.tabItem,
classes[size],
active && classes.isActive,
className,
)}
ref={ref}
>
{icon && (
<SvgIcon
svgIconComponent={icon}
className={classes.icon}
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
/>
)}
{children}
</RovingTabindexItem>
);
},
);
1 change: 1 addition & 0 deletions packages/react/src/components/Tabs/TabItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TabItem';
39 changes: 39 additions & 0 deletions packages/react/src/components/Tabs/TabItem/useTabItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useContext, useId } from 'react';

import { TabsContext } from '../Tabs';
import type { ButtonProps } from '../../Button';

import type { TabItemProps } from './TabItem';

type UseTabItem = (props: TabItemProps) => {
active: boolean;
size?: 'small' | 'medium' | 'large';
buttonProps?: Pick<
ButtonProps,
'id' | 'onClick' | 'aria-checked' | 'aria-current'
>;
};

/** Handles props for `ToggleGroup.Item` in context with `ToggleGroup` and `RovingTabIndex` */
export const useTabItem: UseTabItem = (props: TabItemProps) => {
const { ...rest } = props;
const tabs = useContext(TabsContext);
const itemValue =
props.value ?? (typeof props.children === 'string' ? props.children : '');
const active = tabs.value == itemValue;
const buttonId = `togglegroup-item-${useId()}`;

return {
...rest,
active: active,
size: tabs?.size,
buttonProps: {
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
id: buttonId,
'aria-selected': active,
role: 'tab',
onClick: () => {
tabs.onChange?.(itemValue);
},
},
};
};
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);
}
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 { TabItem } from '../TabItem';

import { TabItemList } from '.';

const user = userEvent.setup();

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

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/TabItemList/TabItemList.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 './TabItemList.module.css';

export const TabItemList = 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/TabItemList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './TabItemList';
44 changes: 44 additions & 0 deletions packages/react/src/components/Tabs/Tabs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Meta, Canvas, Story, Controls, Primary } from '@storybook/blocks';
import { Information } from '../../../../../docs-components';
import * as TabsStories from './Tabs.stories';

<Meta of={TabsStories} />

# Tabs

<Information text='development' />

Description of the Tabs component.

<Primary />
<Controls />

## Bruk

<Information text='token' />

```tsx
import '@digdir/design-system-tokens/brand/altinn/tokens.css'; // Importeres kun en gang i appen din.
import { Tabs } from '@digdir/design-system-react';

<Tabs
defaultValue='value1'
>
<Tabs.List>
<Tabs.Tab value='value1' >Tab 1</Tabs.Tab>
<Tabs.Tab value='value2' >Tab 2</Tabs.Tab>
<Tabs.Tab value='value3' >Tab 3</Tabs.Tab>
</Tabs.List>
<Tabs.Content value='value1'>content 1</Tabs.Content>
<Tabs.Content value='value2'>content 2</Tabs.Content>
<Tabs.Content value='value3'>content 3</Tabs.Content>
</Tabs>
```

## Icons only

<Canvas of={TabsStories.IconsOnly} />

## Controlled

<Canvas of={TabsStories.Controlled} />
3 changes: 3 additions & 0 deletions packages/react/src/components/Tabs/Tabs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tabs {
/* color: red; */
}
Magnusrm marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading