Skip to content

Commit 30a3bec

Browse files
committed
consolidated component libraries
1 parent 470bcf2 commit 30a3bec

File tree

70 files changed

+4401
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+4401
-1
lines changed

.storybook/storybook.scss

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
// Allow current nhsuk styles to override legacy
2-
@import '../node_modules/nhsuk-frontend/packages/nhsuk.scss';
2+
@use '../node_modules/nhsuk-frontend/packages/nhsuk.scss';
3+
@use '../src/styles/all.scss';
4+
@use '../src/styles/components.scss';
5+
@use '../src/styles/core.scss';
6+
@use '../src/styles/variables.scss';
37

48
.tag-wrapper {
59
display: flex;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@rollup/plugin-terser": "^0.4.4",
4141
"@rollup/plugin-typescript": "^11.1.6",
4242
"@storybook/addon-docs": "9.1.3",
43+
"@storybook/addon-links": "^9.1.3",
4344
"@storybook/react-vite": "^9.1.3",
4445
"@testing-library/react": "^16.3.0",
4546
"@types/lodash": "^4",
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* eslint-disable @typescript-eslint/no-redeclare */
2+
import React, { HTMLProps } from 'react';
3+
import classNames from 'classnames';
4+
import Section, { SectionProps } from './components/Section';
5+
import Link from './components/Link';
6+
7+
interface AccordionMenu extends React.FC<HTMLProps<HTMLDivElement>> {
8+
Section: React.FC<SectionProps>;
9+
Link: React.FC<HTMLProps<HTMLAnchorElement>>;
10+
}
11+
12+
const AccordionMenu: AccordionMenu = ({ className, ...rest }) => (
13+
<div className={classNames('nhsuk-accordion-menu', className)} {...rest} />
14+
);
15+
16+
AccordionMenu.Section = Section;
17+
AccordionMenu.Link = Link;
18+
19+
export default AccordionMenu;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
@use '../../styles/variables';
2+
@use '../../../node_modules/nhsuk-frontend/packages/core/settings/all' as settings;
3+
@import '../../../node_modules/nhsuk-frontend/packages/core/all';
4+
5+
.nhsuk-accordion-menu {
6+
display: block;
7+
background-color: settings.$color_nhsuk-white;
8+
color: settings.$color_nhsuk-white;
9+
10+
&__section {
11+
border-top: 1px solid settings.$color_nhsuk-white;
12+
border-bottom: 1px solid settings.$color_nhsuk-white;
13+
background-color: settings.$color_nhsuk-white;
14+
padding: 0;
15+
16+
&:first-of-type + &:not(:focus-visible) {
17+
border-top: 4px solid variables.$color_nhsuk-dark-blue;
18+
}
19+
20+
&:last-of-type + &:not(:focus-visible) {
21+
border-bottom: 4px solid variables.$color_nhsuk-dark-blue;
22+
}
23+
24+
&-summary {
25+
@include nhsuk-responsive-padding(2, 'top');
26+
@include nhsuk-responsive-padding(2, 'bottom');
27+
@include nhsuk-responsive-padding(4, 'left');
28+
@include nhsuk-responsive-padding(4, 'right');
29+
30+
list-style: none !important;
31+
background-color: variables.$color_nhsuk-dark-blue;
32+
outline: none;
33+
border-bottom: 4px solid transparent;
34+
border-top: 4px solid transparent;
35+
display: flex;
36+
justify-content: space-between;
37+
align-items: center;
38+
cursor: pointer;
39+
40+
&::-webkit-details-marker {
41+
display: none !important;
42+
list-style: none !important;
43+
}
44+
45+
&::marker {
46+
display: none !important;
47+
list-style: none !important;
48+
}
49+
50+
&::before {
51+
display: none !important;
52+
list-style: none !important;
53+
}
54+
55+
&:hover {
56+
text-decoration: underline;
57+
}
58+
59+
&:active,
60+
&:focus {
61+
background-color: settings.$nhsuk-focus-color;
62+
color: settings.$nhsuk-focus-text-color;
63+
border-bottom: 4px solid settings.$color_nhsuk-black;
64+
}
65+
66+
&:active > .nhsuk-accordion-menu__icon,
67+
&:focus > .nhsuk-accordion-menu__icon {
68+
> path {
69+
stroke: settings.$color_nhsuk-white;
70+
}
71+
72+
> circle {
73+
fill: settings.$color_nhsuk-black;
74+
}
75+
}
76+
77+
&-text {
78+
@include nhsuk-typography-responsive(22);
79+
display: block;
80+
position: relative;
81+
}
82+
}
83+
}
84+
85+
&__subsection {
86+
87+
@include nhsuk-responsive-margin(2, 'top');
88+
@include nhsuk-responsive-margin(2, 'bottom');
89+
@include nhsuk-responsive-margin(2, 'right');
90+
@include nhsuk-responsive-margin(2, 'left');
91+
92+
display: block;
93+
cursor: pointer;
94+
95+
&-link {
96+
97+
@include nhsuk-responsive-padding(3, 'right');
98+
@include nhsuk-responsive-padding(3, 'left');
99+
100+
@include nhsuk-typography-responsive(19);
101+
display: block;
102+
border-bottom: 3px solid transparent;
103+
border-top: 3px solid transparent;
104+
105+
&:hover {
106+
color: settings.$nhsuk-focus-text-color;
107+
background-color: settings.$nhsuk-focus-color;
108+
}
109+
110+
&:active {
111+
color: settings.$nhsuk-focus-text-color;
112+
background-color: settings.$nhsuk-focus-color;
113+
padding-bottom: 0;
114+
border-bottom: 3px solid settings.$color_nhsuk-black;
115+
}
116+
}
117+
}
118+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import {render} from '@testing-library/react'
3+
import AccordionMenu from '..';
4+
5+
describe('AccordionMenu', () => {
6+
it('matches snapshot', () => {
7+
const component = render(<AccordionMenu />);
8+
expect(component.container).toMatchSnapshot();
9+
});
10+
11+
describe('Section', () => {
12+
it('matches snapshot', () => {
13+
const component = render(<AccordionMenu.Section heading="TestHeading" />);
14+
expect(component.container).toMatchSnapshot();
15+
});
16+
17+
it('passes through text', () => {
18+
const component = render(<AccordionMenu.Section heading="TestHeading" />);
19+
expect(component.container.textContent).toBe(
20+
'TestHeading',
21+
);
22+
});
23+
24+
it('handles defaultOpen', () => {
25+
const component = render(
26+
<AccordionMenu.Section id="accordion" heading="Heading" defaultOpen />,
27+
);
28+
29+
30+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',true)
31+
32+
component.getByText("Heading").click()
33+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',false)
34+
35+
component.getByText("Heading").click()
36+
37+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',true)
38+
});
39+
40+
it('handles open={true}', () => {
41+
const component = render(
42+
<AccordionMenu.Section id="accordion" heading="Heading" open />,
43+
);
44+
45+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',true)
46+
47+
component.getByText("Heading").click()
48+
49+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',true)
50+
});
51+
52+
it('handles open={false}', () => {
53+
const component = render(
54+
<AccordionMenu.Section id="accordion" heading="Heading" open={false} />,
55+
);
56+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',false)
57+
58+
component.getByText("Heading").click()
59+
60+
expect(component.container.querySelector('#accordion')).toHaveProperty('open',false)
61+
});
62+
63+
});
64+
describe('Link', () => {
65+
it('matches snapshot', () => {
66+
const component = render(<AccordionMenu.Link>Test</AccordionMenu.Link>);
67+
expect(component.container).toMatchSnapshot();
68+
expect(component.container.textContent).toBe('Test');
69+
});
70+
});
71+
});
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`AccordionMenu Link matches snapshot 1`] = `
4+
<div>
5+
<div
6+
class="nhsuk-accordion-menu__subsection"
7+
>
8+
<a
9+
class="nhsuk-accordion-menu__subsection-link"
10+
>
11+
Test
12+
</a>
13+
</div>
14+
</div>
15+
`;
16+
17+
exports[`AccordionMenu Section matches snapshot 1`] = `
18+
<div>
19+
<details
20+
class="nhsuk-accordion-menu__section"
21+
>
22+
<summary
23+
class="nhsuk-accordion-menu__section-summary"
24+
role="tab"
25+
tabindex="1"
26+
>
27+
<span
28+
class="nhsuk-accordion-menu__section-summary-text"
29+
>
30+
TestHeading
31+
</span>
32+
<svg
33+
aria-hidden="true"
34+
class="nhsuk-accordion-menu__icon"
35+
height="32"
36+
viewBox="0 0 32 32"
37+
width="32"
38+
xmlns="http://www.w3.org/2000/svg"
39+
>
40+
<circle
41+
cx="16"
42+
cy="16"
43+
fill="#ffffff"
44+
r="16"
45+
/>
46+
<path
47+
d="M16 8v16M8 16h16"
48+
fill="none"
49+
stroke="#003087"
50+
stroke-linecap="round"
51+
stroke-miterlimit="10"
52+
stroke-width="4"
53+
/>
54+
</svg>
55+
</summary>
56+
</details>
57+
</div>
58+
`;
59+
60+
exports[`AccordionMenu matches snapshot 1`] = `
61+
<div>
62+
<div
63+
class="nhsuk-accordion-menu"
64+
/>
65+
</div>
66+
`;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React, { HTMLProps } from 'react';
2+
import classNames from 'classnames';
3+
4+
const Link: React.FC<HTMLProps<HTMLAnchorElement>> = ({ className, ...rest }) => (
5+
<div className="nhsuk-accordion-menu__subsection">
6+
<a className={classNames('nhsuk-accordion-menu__subsection-link', className)} {...rest} />
7+
</div>
8+
);
9+
10+
export default Link;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import React, { HTMLProps, ReactNode, useState, MouseEvent, useEffect } from 'react';
2+
import classNames from 'classnames';
3+
4+
export interface SectionProps extends HTMLProps<HTMLDetailsElement> {
5+
heading: ReactNode;
6+
defaultOpen?: boolean;
7+
}
8+
9+
const ToggleIcon: React.FC<{ open: boolean }> = ({ open }) => (
10+
<svg
11+
className="nhsuk-accordion-menu__icon"
12+
xmlns="http://www.w3.org/2000/svg"
13+
viewBox="0 0 32 32"
14+
width={32}
15+
height={32}
16+
aria-hidden="true"
17+
>
18+
<circle cx="16" cy="16" r="16" fill="#ffffff" />
19+
<path
20+
fill="none"
21+
stroke="#003087"
22+
strokeLinecap="round"
23+
strokeMiterlimit="10"
24+
strokeWidth="4"
25+
d={open ? 'M8,16 h16' : 'M16 8v16M8 16h16'}
26+
/>
27+
</svg>
28+
);
29+
30+
const Section: React.FC<SectionProps> = ({
31+
children,
32+
className,
33+
heading,
34+
open,
35+
defaultOpen,
36+
tabIndex,
37+
...rest
38+
}) => {
39+
const [isOpen, setIsOpen] = useState<boolean>(open === undefined ? Boolean(defaultOpen) : open);
40+
41+
const onSummaryClick = (event: MouseEvent<HTMLDetailsElement>) => {
42+
event.preventDefault();
43+
if (open === undefined) {
44+
setIsOpen(!isOpen);
45+
}
46+
};
47+
48+
useEffect(() => {
49+
if (open !== undefined && isOpen !== open) {
50+
setIsOpen(open);
51+
}
52+
}, [isOpen, open]);
53+
54+
return (
55+
<details
56+
className={classNames('nhsuk-accordion-menu__section', className)}
57+
open={isOpen}
58+
{...rest}
59+
>
60+
<summary
61+
className="nhsuk-accordion-menu__section-summary"
62+
role="tab"
63+
tabIndex={tabIndex || 1}
64+
onClick={onSummaryClick}
65+
>
66+
<span className="nhsuk-accordion-menu__section-summary-text">{heading}</span>
67+
<ToggleIcon open={isOpen} />
68+
</summary>
69+
{children}
70+
</details>
71+
);
72+
};
73+
74+
export default Section;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import AccordionMenu from './AccordionMenu';
2+
3+
export default AccordionMenu;

0 commit comments

Comments
 (0)