Skip to content

Commit

Permalink
WIP - tabs
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeb-nhs committed Mar 19, 2024
1 parent 1471655 commit 25cb443
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 3 deletions.
72 changes: 72 additions & 0 deletions src/components/content-presentation/tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import classNames from 'classnames';
import React, { HTMLAttributes, useEffect } from 'react';
import HeadingLevel, { HeadingLevelType } from '../../../util/HeadingLevel';
import TabsJs from 'nhsuk-frontend/packages/components/tabs/tabs.js';

type TabsProps = HTMLAttributes<HTMLDivElement>;

type TabTitleProps = { children: React.ReactNode; headingLevel?: HeadingLevelType };

type TabListProps = {
children: React.ReactNode;
};

type TabListItemProps = {
id: string;
children: React.ReactNode;
};

type TabContentsProps = {
id: string;
children: React.ReactNode;
};

const TabTitle: React.FC<TabTitleProps> = ({ children, headingLevel = 'h2' }) => (
<HeadingLevel className="nhsuk-tabs__title" headingLevel={headingLevel}>
{children}
</HeadingLevel>
);

const TabList: React.FC<TabListProps> = ({ children }) => (
<ul className="nhsuk-tabs__list">{children}</ul>
);

const TabListItem: React.FC<TabListItemProps> = ({ id, children }) => (
<li className="nhsuk-tabs__list-item">
<a className="nhsuk-tabs__tab" href={`#${id}`}>
{children}
</a>
</li>
);

const TabContents: React.FC<TabContentsProps> = ({ id, children }) => (
<div className="nhsuk-tabs__panel" id={id}>
{children}
</div>
);

interface Tabs extends React.FC<TabsProps> {
Title: React.FC<TabTitleProps>;
List: React.FC<TabListProps>;
ListItem: React.FC<TabListItemProps>;
Contents: React.FC<TabContentsProps>;
}

const Tabs: Tabs = ({ className, children, ...rest }) => {
useEffect(() => {
TabsJs();
}, []);

return (
<div className={classNames('nhsuk-tabs', className)} data-module="nhsuk-tabs" {...rest}>
{children}
</div>
);
};

Tabs.Title = TabTitle;
Tabs.List = TabList;
Tabs.ListItem = TabListItem;
Tabs.Contents = TabContents;

export default Tabs;
32 changes: 32 additions & 0 deletions src/components/content-presentation/tabs/__tests__/Tabs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { render } from '@testing-library/react';
import Tabs from '../Tabs';

describe('The tabs component', () => {
it('Matches the snapshot', () => {
const { container } = render(
<Tabs>
<Tabs.Title>Contents</Tabs.Title>
<Tabs.List>
<Tabs.ListItem id="past-day">Past day</Tabs.ListItem>
<Tabs.ListItem id="past-week">Past week</Tabs.ListItem>
<Tabs.ListItem id="past-month">Past month</Tabs.ListItem>
</Tabs.List>

<Tabs.Contents id="past-day">
<div>Past day contents go here</div>
</Tabs.Contents>

<Tabs.Contents id="past-week">
<div>Past week contents go here</div>
</Tabs.Contents>

<Tabs.Contents id="past-month">
<div>Past month contents go here</div>
</Tabs.Contents>
</Tabs>,
);

expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The tabs component Matches the snapshot 1`] = `
<div>
<div
class="nhsuk-tabs"
data-module="nhsuk-tabs"
>
<h2
class="nhsuk-tabs__title"
>
Contents
</h2>
<ul
class="nhsuk-tabs__list"
role="tablist"
>
<li
class="nhsuk-tabs__list-item nhsuk-tabs__list-item--selected"
role="presentation"
>
<a
aria-controls="past-day"
aria-selected="true"
class="nhsuk-tabs__tab"
href="#past-day"
id="tab_past-day"
role="tab"
tabindex="0"
>
Past day
</a>
</li>
<li
class="nhsuk-tabs__list-item"
role="presentation"
>
<a
aria-controls="past-week"
aria-selected="false"
class="nhsuk-tabs__tab"
href="#past-week"
id="tab_past-week"
role="tab"
tabindex="-1"
>
Past week
</a>
</li>
<li
class="nhsuk-tabs__list-item"
role="presentation"
>
<a
aria-controls="past-month"
aria-selected="false"
class="nhsuk-tabs__tab"
href="#past-month"
id="tab_past-month"
role="tab"
tabindex="-1"
>
Past month
</a>
</li>
</ul>
<div
aria-labelledby="tab_past-day"
class="nhsuk-tabs__panel"
id="past-day"
role="tabpanel"
>
<div>
Past day contents go here
</div>
</div>
<div
aria-labelledby="tab_past-week"
class="nhsuk-tabs__panel nhsuk-tabs__panel--hidden"
id="past-week"
role="tabpanel"
>
<div>
Past week contents go here
</div>
</div>
<div
aria-labelledby="tab_past-month"
class="nhsuk-tabs__panel nhsuk-tabs__panel--hidden"
id="past-month"
role="tabpanel"
>
<div>
Past month contents go here
</div>
</div>
</div>
</div>
`;
3 changes: 3 additions & 0 deletions src/components/content-presentation/tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Tabs from './Tabs';

export default Tabs;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export enum CharacterCountType {
Words,
}

type CharacterCountProps = {
type CharacterCountProps = React.HTMLAttributes<HTMLDivElement> & {
children: React.ReactNode;
maxLength: number;
countType: CharacterCountType;
Expand All @@ -21,15 +21,16 @@ const CharacterCount: React.FC<CharacterCountProps> = ({
countType,
textAreaId,
thresholdPercent,
...rest
}) => {
useEffect(() => {
CharacterCountJs();
}, []);

const characterCountProps: HTMLAttributesWithData<HTMLDivElement> =
countType === CharacterCountType.Characters
? { ['data-maxlength']: maxLength }
: { ['data-maxwords']: maxLength };
? { ...rest, ['data-maxlength']: maxLength }
: { ...rest, ['data-maxwords']: maxLength };

if (thresholdPercent) {
characterCountProps['data-threshold'] = thresholdPercent;
Expand Down
1 change: 1 addition & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
declare module 'nhsuk-frontend/packages/components/header/header.js';
declare module 'nhsuk-frontend/packages/components/checkboxes/checkboxes.js';
declare module 'nhsuk-frontend/packages/components/character-count/character-count.js';
declare module 'nhsuk-frontend/packages/components/tabs/tabs.js';
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export { default as Select } from './components/form-elements/select';
export { default as SkipLink } from './components/navigation/skip-link';
export { default as SummaryList } from './components/content-presentation/summary-list';
export { default as Table } from './components/content-presentation/table';
export { default as Tabs } from './components/content-presentation/tabs';
export { default as Tag } from './components/content-presentation/tag';
export { default as Textarea } from './components/form-elements/textarea';
export { LedeText, BodyText } from './components/typography';
Expand Down
74 changes: 74 additions & 0 deletions stories/Content Presentation/Tabs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import { Tabs } from '../../src';
import { Meta, StoryObj } from '@storybook/react';

/**
* The tabs component lets users navigate between related sections of content, displaying 1 section at a time.
*
* This component can be found in the `nhsuk-frontend` repository <a href="https://github.com/nhsuk/nhsuk-frontend/tree/main/packages/components/tabs" target="_blank" rel="noopener noreferrer">here</a>.
*
* Further information about this component can be found in the <a href='https://service-manual.nhs.uk/design-system/components/tabs'>NHS digital service manual.</a>
*/

const meta: Meta<typeof Tabs> = {
title: 'Form Elements/Tabs',
component: Tabs,
};

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

export const Standard: Story = {
render: () => (
<Tabs>
<Tabs.Title>Contents</Tabs.Title>
<Tabs.List>
<Tabs.ListItem id="past-day">Past day</Tabs.ListItem>
<Tabs.ListItem id="past-week">Past week</Tabs.ListItem>
<Tabs.ListItem id="past-month">Past month</Tabs.ListItem>
</Tabs.List>

<Tabs.Contents id="past-day">
<div>Past day contents go here</div>
</Tabs.Contents>

<Tabs.Contents id="past-week">
<div>Past week contents go here</div>
</Tabs.Contents>

<Tabs.Contents id="past-month">
<div>Past month contents go here</div>
</Tabs.Contents>
</Tabs>
),
};

/**
* There is a hidden heading which is useful for accessibility concerns and screen readers.
*
* This heading is also visible on small screens and if the user has JavaScript disabled.
*/
export const DifferentAccessibleHeading: Story = {
render: () => (
<Tabs>
<Tabs.Title headingLevel="h1">Tabs title</Tabs.Title>
<Tabs.List>
<Tabs.ListItem id="past-day-2">Past day</Tabs.ListItem>
<Tabs.ListItem id="past-week-2">Past week</Tabs.ListItem>
<Tabs.ListItem id="past-month-2">Past month</Tabs.ListItem>
</Tabs.List>

<Tabs.Contents id="past-day-2">
<div>Past day contents go here</div>
</Tabs.Contents>

<Tabs.Contents id="past-week-2">
<div>Past week contents go here</div>
</Tabs.Contents>

<Tabs.Contents id="past-month-2">
<div>Past month contents go here</div>
</Tabs.Contents>
</Tabs>
),
};
18 changes: 18 additions & 0 deletions stories/Form Elements/CharacterCount.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import React from 'react';
import { CharacterCount, CharacterCountType, HintText, Label, Textarea } from '../../src';
import { Meta, StoryObj } from '@storybook/react';

/**
* Help users know how much text they can enter when there is a limit on the number of characters.
*
* This component can be found in the `nhsuk-frontend` repository <a href="https://github.com/nhsuk/nhsuk-frontend/tree/main/packages/components/character-count" target="_blank" rel="noopener noreferrer">here</a>.
*
* Further information about this component can be found in the <a href='https://service-manual.nhs.uk/design-system/components/character-count'>NHS digital service manual.</a>
*/

const meta: Meta<typeof CharacterCount> = {
title: 'Form Elements/CharacterCount',
component: CharacterCount,
Expand Down Expand Up @@ -32,6 +40,11 @@ export const Standard: Story = {
),
};

/**
* Sometimes, rather than counting the number of characters, it is useful to count the number of words instead.
*
* Use the `countType` prop to vary this behaviour.
*/
export const WordCountLimit: Story = {
render: () => (
<CharacterCount
Expand All @@ -52,6 +65,11 @@ export const WordCountLimit: Story = {
),
};

/**
* If the limit is much higher than most users are likely to reach, you can choose to only display the message after a user has entered a certain amount.
*
* Use the `thresholdPercent` prop to only show the count message when users have reached that percentage of the limit.
*/
export const MessageThresholdPercentage: Story = {
render: () => (
<CharacterCount
Expand Down

0 comments on commit 25cb443

Please sign in to comment.