uikit-react-public 0.14.21 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
- package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
- package/dist/components/Accordion/Accordion.d.ts +1 -1
- package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
- package/dist/components/Accordion/index.d.ts +2 -0
- package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
- package/dist/components/Button/Button.d.ts +1 -0
- package/dist/components/Calendar/index.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.d.ts +1 -1
- package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
- package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
- package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
- package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
- package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
- package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
- package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
- package/dist/components/Datepicker/utils/index.d.ts +0 -1
- package/dist/components/Dialog/BaseDialog.d.ts +2 -1
- package/dist/components/Dialog/Dialog.d.ts +2 -0
- package/dist/components/Header/Header.d.ts +4 -1
- package/dist/components/Header/Header.stories.d.ts +40 -0
- package/dist/components/Main/Main.d.ts +21 -0
- package/dist/components/Main/Main.stories.d.ts +15 -0
- package/dist/components/Main/index.d.ts +2 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
- package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
- package/dist/components/NativeDatepicker/index.d.ts +2 -0
- package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
- package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
- package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
- package/dist/components/Select/Select.stories.d.ts +154 -2
- package/dist/components/Select/Select.types.d.ts +51 -22
- package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
- package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
- package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
- package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
- package/dist/components/Select/subcomponents/index.d.ts +1 -0
- package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
- package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
- package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
- package/dist/components/WeekPicker/index.d.ts +1 -0
- package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
- package/dist/components/index.d.ts +8 -0
- package/dist/hooks/useFocusTrap.d.ts +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4366 -3768
- package/dist/utils/__tests__/announce.test.d.ts +1 -0
- package/dist/utils/announce.d.ts +6 -0
- package/dist/utils/index.d.ts +1 -0
- package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
- package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
- package/lib/components/Accordion/Accordion.stories.tsx +139 -0
- package/lib/components/Accordion/Accordion.tsx +10 -8
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
- package/lib/components/Accordion/index.ts +2 -0
- package/lib/components/Alert/Alert.stories.tsx +1 -1
- package/lib/components/Avatar/Avatar.mdx +117 -0
- package/lib/components/Avatar/Avatar.stories.tsx +110 -2
- package/lib/components/Blanket/Blanket.stories.tsx +1 -1
- package/lib/components/Button/Button.stories.tsx +1 -1
- package/lib/components/Button/Button.tsx +1 -0
- package/lib/components/Calendar/Calendar.stories.tsx +12 -32
- package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
- package/lib/components/Calendar/index.ts +1 -5
- package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
- package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
- package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
- package/lib/components/Calendar/subcomponents/index.ts +1 -1
- package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
- package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
- package/lib/components/Datepicker/Datepicker.lld.md +108 -0
- package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
- package/lib/components/Datepicker/Datepicker.tsx +14 -36
- package/lib/components/Datepicker/Datepicker.types.ts +5 -14
- package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
- package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
- package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
- package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
- package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
- package/lib/components/Datepicker/subcomponents/index.ts +0 -1
- package/lib/components/Datepicker/utils/index.ts +0 -1
- package/lib/components/Dialog/BaseDialog.tsx +11 -0
- package/lib/components/Dialog/Dialog.tsx +8 -1
- package/lib/components/Dialog/DialogBody.tsx +5 -1
- package/lib/components/Dialog/DialogHeader.tsx +2 -1
- package/lib/components/Divider/Divider.stories.tsx +1 -1
- package/lib/components/Field/ErrorText.tsx +1 -0
- package/lib/components/Field/Field.stories.tsx +1 -1
- package/lib/components/Field/__tests__/Field.test.tsx +13 -0
- package/lib/components/FileInput/FileInput.stories.tsx +1 -1
- package/lib/components/Footer/Footer.stories.tsx +1 -1
- package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
- package/lib/components/Header/Header.mdx +52 -0
- package/lib/components/Header/Header.stories.tsx +98 -0
- package/lib/components/Header/Header.tsx +51 -6
- package/lib/components/Header/__tests__/Header.test.tsx +17 -1
- package/lib/components/Heading/Heading.stories.tsx +1 -1
- package/lib/components/Icon/Icon.stories.tsx +1 -1
- package/lib/components/IconButton/IconButton.stories.tsx +1 -1
- package/lib/components/Input/Input.stories.tsx +1 -1
- package/lib/components/Label/Label.stories.tsx +1 -1
- package/lib/components/Main/Main.stories.tsx +36 -0
- package/lib/components/Main/Main.tsx +46 -0
- package/lib/components/Main/__tests__/Main.test.tsx +80 -0
- package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
- package/lib/components/Main/index.ts +2 -0
- package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
- package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
- package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
- package/lib/components/NativeDatepicker/index.ts +2 -0
- package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
- package/lib/components/NativeDatepicker/utils/index.ts +1 -0
- package/lib/components/Pagination/PaginationControls.tsx +55 -12
- package/lib/components/Pagination/PaginationInfo.tsx +5 -1
- package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
- package/lib/components/Search/Search.stories.tsx +1 -1
- package/lib/components/Search/Search.tsx +4 -1
- package/lib/components/Search/__tests__/Search.test.tsx +19 -1
- package/lib/components/Select/Select.mdx +169 -0
- package/lib/components/Select/Select.stories.tsx +191 -43
- package/lib/components/Select/Select.tsx +36 -12
- package/lib/components/Select/Select.types.ts +66 -48
- package/lib/components/Select/__tests__/Select.test.tsx +448 -7
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
- package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
- package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
- package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
- package/lib/components/Select/subcomponents/index.tsx +1 -0
- package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
- package/lib/components/Spinner/Spinner.stories.tsx +1 -1
- package/lib/components/Textarea/Textarea.stories.tsx +1 -1
- package/lib/components/Timepicker/Timepicker.tsx +4 -0
- package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
- package/lib/components/Toggle/Toggle.stories.tsx +1 -1
- package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
- package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
- package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
- package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
- package/lib/components/WeekPicker/index.ts +1 -0
- package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
- package/lib/components/common/Common.mdx +1 -1
- package/lib/components/index.ts +11 -2
- package/lib/hooks/useFocusTrap.ts +40 -4
- package/lib/index.ts +1 -0
- package/lib/utils/__tests__/announce.test.ts +121 -0
- package/lib/utils/announce.ts +134 -0
- package/lib/utils/index.ts +1 -0
- package/package.json +3 -6
- package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
- package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
- /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
- /package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.test.ts +0 -0
|
@@ -23,7 +23,7 @@ const DialogHeader = ({
|
|
|
23
23
|
testId = NAME,
|
|
24
24
|
className,
|
|
25
25
|
}: DialogHeaderProps) => {
|
|
26
|
-
const { onClose } = useContext(DialogContext);
|
|
26
|
+
const { onClose, dialogHeaderId } = useContext(DialogContext);
|
|
27
27
|
|
|
28
28
|
const [theme] = useTheme();
|
|
29
29
|
|
|
@@ -73,6 +73,7 @@ const DialogHeader = ({
|
|
|
73
73
|
level={3}
|
|
74
74
|
margins={false}
|
|
75
75
|
{...headingProps}
|
|
76
|
+
id={dialogHeaderId}
|
|
76
77
|
>
|
|
77
78
|
{children}
|
|
78
79
|
</Heading>
|
|
@@ -9,7 +9,7 @@ import Input from '../Input/Input';
|
|
|
9
9
|
import Textarea from '../Textarea/Textarea';
|
|
10
10
|
|
|
11
11
|
const meta = {
|
|
12
|
-
title: 'Components/
|
|
12
|
+
title: 'Components/Field',
|
|
13
13
|
component: Field,
|
|
14
14
|
parameters: { layout: 'padded' },
|
|
15
15
|
} satisfies Meta<typeof Field>;
|
|
@@ -507,4 +507,17 @@ describe('Field', () => {
|
|
|
507
507
|
expect(label2).not.toHaveAttribute('for', idForField2);
|
|
508
508
|
expect(textarea1).not.toHaveAttribute('id', idForField1);
|
|
509
509
|
});
|
|
510
|
+
|
|
511
|
+
test('Field.ErrorText has role alert', () => {
|
|
512
|
+
const testId = 'error-text-01';
|
|
513
|
+
render(
|
|
514
|
+
<ThemeContextProvider>
|
|
515
|
+
<Field error>
|
|
516
|
+
<Field.ErrorText testId={testId}> Error text </Field.ErrorText>
|
|
517
|
+
</Field>
|
|
518
|
+
</ThemeContextProvider>
|
|
519
|
+
);
|
|
520
|
+
const errorText = screen.getByTestId(testId);
|
|
521
|
+
expect(errorText).toHaveAttribute('role', 'alert');
|
|
522
|
+
});
|
|
510
523
|
});
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
import FileInput from './FileInput';
|
|
3
3
|
|
|
4
4
|
const meta = {
|
|
5
|
-
title: 'Components/
|
|
5
|
+
title: 'Components/Work in progress/FileInput',
|
|
6
6
|
component: FileInput,
|
|
7
7
|
parameters: {
|
|
8
8
|
layout: 'centered',
|
|
@@ -247,7 +247,7 @@ exports[`Footer > snapshot: footer links provided 1`] = `
|
|
|
247
247
|
class="css-bq4k8p"
|
|
248
248
|
>
|
|
249
249
|
©
|
|
250
|
-
|
|
250
|
+
2026
|
|
251
251
|
UCL
|
|
252
252
|
</span>
|
|
253
253
|
</div>
|
|
@@ -591,7 +591,7 @@ exports[`Footer > snapshot: nav links 1`] = `
|
|
|
591
591
|
class="css-bq4k8p"
|
|
592
592
|
>
|
|
593
593
|
©
|
|
594
|
-
|
|
594
|
+
2026
|
|
595
595
|
UCL
|
|
596
596
|
</span>
|
|
597
597
|
</div>
|
|
@@ -886,7 +886,7 @@ exports[`Footer > snapshot: no nav links 1`] = `
|
|
|
886
886
|
class="css-bq4k8p"
|
|
887
887
|
>
|
|
888
888
|
©
|
|
889
|
-
|
|
889
|
+
2026
|
|
890
890
|
UCL
|
|
891
891
|
</span>
|
|
892
892
|
</div>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as HeaderStories from "./Header.stories";
|
|
2
|
+
import { Meta, Title, Subtitle, Canvas, Controls } from "@storybook/blocks";
|
|
3
|
+
|
|
4
|
+
export const usage = {
|
|
5
|
+
default: `<Header title='App Name' />`,
|
|
6
|
+
homeLink: `<Header title='App Name' homeLinkHref='/' homeLinkAriaLabel='Go to home' />`,
|
|
7
|
+
menu: `<Header title='App Name' homeLinkHref='/'>
|
|
8
|
+
<Header.Menu>
|
|
9
|
+
<Menu title='App Name'>
|
|
10
|
+
<Menu.Section>
|
|
11
|
+
<Menu.Item icon={<Icon.Home size={20} />}>Home Page</Menu.Item>
|
|
12
|
+
<Menu.Item icon={<Icon.Tool size={20} />}>Tools</Menu.Item>
|
|
13
|
+
</Menu.Section>
|
|
14
|
+
</Menu>
|
|
15
|
+
</Header.Menu>
|
|
16
|
+
</Header>`,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
<Meta of={HeaderStories} />
|
|
20
|
+
<Title />
|
|
21
|
+
<Subtitle>A simple header with optional menu and home link support.</Subtitle>
|
|
22
|
+
|
|
23
|
+
Use `<Header>` to display the UCL logo and app title at the top of a page. Pass
|
|
24
|
+
`homeLinkHref` to make the logo/title clickable, and wrap a menu in `<Header.Menu>`
|
|
25
|
+
to add a menu button on the right.
|
|
26
|
+
|
|
27
|
+
<Canvas source={{ code: usage.default }} />
|
|
28
|
+
|
|
29
|
+
## Home link
|
|
30
|
+
|
|
31
|
+
Add `homeLinkHref` (and optionally `homeLinkAriaLabel`) to make the logo/title act as a home link.
|
|
32
|
+
For client routing, pass your router link through `homeLinkProps` (e.g. `homeLinkProps={{ component: RouterLink }}`, with `import { Link as RouterLink } from 'react-router-dom'` or `'wouter'`).
|
|
33
|
+
|
|
34
|
+
<Canvas of={HeaderStories.WithHomeLink} source={{ code: usage.homeLink }} />
|
|
35
|
+
|
|
36
|
+
## Menu
|
|
37
|
+
|
|
38
|
+
Wrap a `Menu` inside `Header.Menu` to show a menu button on the right.
|
|
39
|
+
|
|
40
|
+
<Canvas of={HeaderStories.WithMenu} source={{ code: usage.menu }} />
|
|
41
|
+
|
|
42
|
+
## Fixed header
|
|
43
|
+
|
|
44
|
+
Use `fixed` to pin the header to the top of the viewport.
|
|
45
|
+
|
|
46
|
+
<Canvas of={HeaderStories.Fixed} />
|
|
47
|
+
|
|
48
|
+
## Props
|
|
49
|
+
Full props specification for `<Header>` is below.
|
|
50
|
+
<Canvas source={{ code: usage.default }} />
|
|
51
|
+
You can use the controls below to manipulate the `<Header>` component above.
|
|
52
|
+
<Controls />
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import Header from './Header';
|
|
3
|
+
import Menu from '../Menu';
|
|
4
|
+
import Icon from '../Icon';
|
|
5
|
+
|
|
6
|
+
const storyWrapperStyle: React.CSSProperties = { minHeight: 400 };
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/Header',
|
|
10
|
+
component: Header,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'fullscreen',
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
title: { control: 'text' },
|
|
16
|
+
fixed: { control: 'boolean' },
|
|
17
|
+
homeLinkHref: { control: 'text' },
|
|
18
|
+
homeLinkAriaLabel: { control: 'text' },
|
|
19
|
+
className: { control: 'text' },
|
|
20
|
+
children: { control: false },
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
title: 'App Name',
|
|
24
|
+
},
|
|
25
|
+
tags: ['autodocs'],
|
|
26
|
+
} satisfies Meta<typeof Header>;
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof meta>;
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
render: (args) => (
|
|
33
|
+
<div style={{ overflow: 'hidden' }}>
|
|
34
|
+
<Header {...args} />
|
|
35
|
+
</div>
|
|
36
|
+
),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Fixed: Story = {
|
|
40
|
+
render: (args) => (
|
|
41
|
+
<div
|
|
42
|
+
style={{
|
|
43
|
+
height: '240px',
|
|
44
|
+
overflowY: 'auto',
|
|
45
|
+
position: 'relative',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<Header
|
|
49
|
+
{...args}
|
|
50
|
+
fixed
|
|
51
|
+
title='App Name'
|
|
52
|
+
/>
|
|
53
|
+
<div
|
|
54
|
+
style={{
|
|
55
|
+
paddingTop: '88px', // keep content below the fixed header
|
|
56
|
+
height: '600px',
|
|
57
|
+
margin: '0 16px',
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
Scroll inside this panel; the header should stay fixed to the top of the
|
|
61
|
+
viewport.
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithHomeLink: Story = {
|
|
68
|
+
name: 'With home link',
|
|
69
|
+
render: (args) => (
|
|
70
|
+
<div style={{ overflow: 'hidden' }}>
|
|
71
|
+
<Header {...args} />
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
args: {
|
|
75
|
+
homeLinkHref: '/',
|
|
76
|
+
homeLinkAriaLabel: 'Go to home',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const WithMenu: Story = {
|
|
81
|
+
render: (args) => (
|
|
82
|
+
<div style={storyWrapperStyle}>
|
|
83
|
+
<Header {...args}>
|
|
84
|
+
<Header.Menu>
|
|
85
|
+
<Menu title='App Name'>
|
|
86
|
+
<Menu.Section>
|
|
87
|
+
<Menu.Item icon={<Icon.Home size={20} />}>Home Page</Menu.Item>
|
|
88
|
+
<Menu.Item icon={<Icon.Tool size={20} />}>Tools</Menu.Item>
|
|
89
|
+
</Menu.Section>
|
|
90
|
+
<Menu.Section>
|
|
91
|
+
<Menu.Item icon={<Icon.Settings size={20} />}>Settings</Menu.Item>
|
|
92
|
+
</Menu.Section>
|
|
93
|
+
</Menu>
|
|
94
|
+
</Header.Menu>
|
|
95
|
+
</Header>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
};
|
|
@@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css';
|
|
|
3
3
|
import { useTheme } from '../..';
|
|
4
4
|
import UclLogo from '../UclLogo/UclLogo';
|
|
5
5
|
import HeaderMenu from './HeaderMenu';
|
|
6
|
+
import Link from '../Link';
|
|
6
7
|
|
|
7
8
|
export const NAME = 'ucl-uikit-header';
|
|
8
9
|
export const HEADER_DESKTOP_HEIGHT_PX = 72;
|
|
@@ -15,6 +16,9 @@ export interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
|
|
15
16
|
titleAs?: React.ElementType<React.HTMLAttributes<HTMLElement>>;
|
|
16
17
|
titleClassName?: string;
|
|
17
18
|
titleProps?: Record<string, unknown>;
|
|
19
|
+
homeLinkHref?: string;
|
|
20
|
+
homeLinkProps?: Record<string, unknown>;
|
|
21
|
+
homeLinkAriaLabel?: string;
|
|
18
22
|
testId?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -24,6 +28,9 @@ const Header = ({
|
|
|
24
28
|
titleAs = 'div',
|
|
25
29
|
titleClassName,
|
|
26
30
|
titleProps,
|
|
31
|
+
homeLinkHref,
|
|
32
|
+
homeLinkProps,
|
|
33
|
+
homeLinkAriaLabel = 'Go to homepage',
|
|
27
34
|
testId = NAME,
|
|
28
35
|
className,
|
|
29
36
|
children,
|
|
@@ -86,16 +93,33 @@ const Header = ({
|
|
|
86
93
|
}
|
|
87
94
|
`;
|
|
88
95
|
|
|
96
|
+
const linkStyle = css`
|
|
97
|
+
color: inherit;
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
&:visited {
|
|
103
|
+
color: inherit;
|
|
104
|
+
}
|
|
105
|
+
&:hover {
|
|
106
|
+
color: inherit;
|
|
107
|
+
}
|
|
108
|
+
&:active {
|
|
109
|
+
color: inherit;
|
|
110
|
+
}
|
|
111
|
+
&:focus-visible {
|
|
112
|
+
outline: none;
|
|
113
|
+
box-shadow: ${theme.boxShadow.focus};
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
|
|
89
117
|
const titleStyle = cx(titleBaseStyle, titleClassName);
|
|
90
118
|
|
|
91
119
|
const TitleComponent = titleAs;
|
|
92
120
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
className={style}
|
|
96
|
-
data-testid={testId}
|
|
97
|
-
{...props}
|
|
98
|
-
>
|
|
121
|
+
const headerContent = (
|
|
122
|
+
<>
|
|
99
123
|
<UclLogo className={uclLogoStyle} />
|
|
100
124
|
{title && (
|
|
101
125
|
<TitleComponent
|
|
@@ -105,6 +129,27 @@ const Header = ({
|
|
|
105
129
|
{title}
|
|
106
130
|
</TitleComponent>
|
|
107
131
|
)}
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<header
|
|
137
|
+
className={style}
|
|
138
|
+
data-testid={testId}
|
|
139
|
+
{...props}
|
|
140
|
+
>
|
|
141
|
+
{homeLinkHref ? (
|
|
142
|
+
<Link
|
|
143
|
+
href={homeLinkHref}
|
|
144
|
+
aria-label={homeLinkAriaLabel}
|
|
145
|
+
className={linkStyle}
|
|
146
|
+
{...homeLinkProps}
|
|
147
|
+
>
|
|
148
|
+
{headerContent}
|
|
149
|
+
</Link>
|
|
150
|
+
) : (
|
|
151
|
+
headerContent
|
|
152
|
+
)}
|
|
108
153
|
|
|
109
154
|
{children}
|
|
110
155
|
</header>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import Header from '../Header';
|
|
4
4
|
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
5
5
|
|
|
@@ -23,4 +23,20 @@ describe('Header', () => {
|
|
|
23
23
|
);
|
|
24
24
|
expect(renderResult.container.firstChild).toMatchSnapshot();
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
test('wraps logo/title in a home link when homeLinkHref is provided', () => {
|
|
28
|
+
render(
|
|
29
|
+
<ThemeContextProvider>
|
|
30
|
+
<Header
|
|
31
|
+
title='LIDS'
|
|
32
|
+
homeLinkHref='/'
|
|
33
|
+
homeLinkAriaLabel='Go home'
|
|
34
|
+
/>
|
|
35
|
+
</ThemeContextProvider>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const link = screen.getByLabelText('Go home');
|
|
39
|
+
expect(link).toBeInTheDocument();
|
|
40
|
+
expect(link.getAttribute('href')).toBe('/');
|
|
41
|
+
});
|
|
26
42
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import Main from './Main';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/Utilities/Main',
|
|
6
|
+
component: Main,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'fullscreen',
|
|
9
|
+
},
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
} satisfies Meta<typeof Main>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof Main>;
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const WithLayout: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
layout: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const WithChildren: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
children: (
|
|
30
|
+
<>
|
|
31
|
+
<h1>Main Content</h1>
|
|
32
|
+
<p>This is some content inside the Main component.</p>
|
|
33
|
+
</>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { cx } from '@emotion/css';
|
|
2
|
+
import Layout from '../Layout/Layout';
|
|
3
|
+
|
|
4
|
+
export interface MainProps extends React.HTMLAttributes<HTMLElement> {
|
|
5
|
+
/**
|
|
6
|
+
* Determines whether to wrap children in a `<Layout>` component,
|
|
7
|
+
* to apply the grid layout from the UCL Design System.
|
|
8
|
+
*/
|
|
9
|
+
layout?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* ID for testing purposes.
|
|
12
|
+
* Added to the `data-testid` attribute of top-level `<main>` element.
|
|
13
|
+
* Default is `'ucl-uikit-main'`.
|
|
14
|
+
*/
|
|
15
|
+
testId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const NAME = 'ucl-uikit-main';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Semantic wrapper around HTML `<main>` element
|
|
22
|
+
*
|
|
23
|
+
* Optionally wraps children in the UCL Design System `Layout` grid when `layout` is true
|
|
24
|
+
*/
|
|
25
|
+
const Main = ({
|
|
26
|
+
layout = false,
|
|
27
|
+
testId = NAME,
|
|
28
|
+
className,
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}: MainProps) => {
|
|
32
|
+
// No need for any additional styling -- just a semantic wrapper
|
|
33
|
+
const style = cx(NAME, className);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<main
|
|
37
|
+
className={style}
|
|
38
|
+
data-testid={testId}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{layout ? <Layout>{children}</Layout> : children}
|
|
42
|
+
</main>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default Main;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import Main from '../Main';
|
|
4
|
+
import { ThemeContextProvider } from '../../../theme/useTheme';
|
|
5
|
+
|
|
6
|
+
describe('Main', () => {
|
|
7
|
+
// Snapshot tests
|
|
8
|
+
|
|
9
|
+
test('snapshot: no props', () => {
|
|
10
|
+
const renderResult = render(
|
|
11
|
+
<ThemeContextProvider>
|
|
12
|
+
<Main>Content</Main>
|
|
13
|
+
</ThemeContextProvider>
|
|
14
|
+
);
|
|
15
|
+
expect(renderResult.container.firstChild).toMatchSnapshot();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('snapshot: layout prop true', () => {
|
|
19
|
+
const renderResult = render(
|
|
20
|
+
<ThemeContextProvider>
|
|
21
|
+
<Main layout>Content</Main>
|
|
22
|
+
</ThemeContextProvider>
|
|
23
|
+
);
|
|
24
|
+
expect(renderResult.container.firstChild).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('snapshot: testId prop', () => {
|
|
28
|
+
const renderResult = render(
|
|
29
|
+
<ThemeContextProvider>
|
|
30
|
+
<Main testId='test123'>Content</Main>
|
|
31
|
+
</ThemeContextProvider>
|
|
32
|
+
);
|
|
33
|
+
expect(renderResult.container.firstChild).toMatchSnapshot();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Interaction tests
|
|
37
|
+
|
|
38
|
+
test('Can find by default testId', () => {
|
|
39
|
+
render(
|
|
40
|
+
<ThemeContextProvider>
|
|
41
|
+
<Main>Content</Main>
|
|
42
|
+
</ThemeContextProvider>
|
|
43
|
+
);
|
|
44
|
+
const main = screen.getByTestId('ucl-uikit-main');
|
|
45
|
+
expect(main).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('Can find by custom testId', () => {
|
|
49
|
+
render(
|
|
50
|
+
<ThemeContextProvider>
|
|
51
|
+
<Main testId='custom-test-id'>Content</Main>
|
|
52
|
+
</ThemeContextProvider>
|
|
53
|
+
);
|
|
54
|
+
const main = screen.getByTestId('custom-test-id');
|
|
55
|
+
expect(main).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('Renders children correctly', () => {
|
|
59
|
+
render(
|
|
60
|
+
<ThemeContextProvider>
|
|
61
|
+
<Main>
|
|
62
|
+
<div data-testid='child-element'>Child Content</div>
|
|
63
|
+
</Main>
|
|
64
|
+
</ThemeContextProvider>
|
|
65
|
+
);
|
|
66
|
+
const child = screen.getByTestId('child-element');
|
|
67
|
+
expect(child).toBeInTheDocument();
|
|
68
|
+
expect(child).toHaveTextContent('Child Content');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('Applies layout when layout prop is true', () => {
|
|
72
|
+
render(
|
|
73
|
+
<ThemeContextProvider>
|
|
74
|
+
<Main layout>Content</Main>
|
|
75
|
+
</ThemeContextProvider>
|
|
76
|
+
);
|
|
77
|
+
const layoutComponent = screen.getByTestId('ucl-uikit-layout');
|
|
78
|
+
expect(layoutComponent).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`Main > snapshot: layout prop true 1`] = `
|
|
4
|
+
<main
|
|
5
|
+
class="ucl-uikit-main"
|
|
6
|
+
data-testid="ucl-uikit-main"
|
|
7
|
+
>
|
|
8
|
+
<div
|
|
9
|
+
class="css-3reham"
|
|
10
|
+
data-testid="ucl-uikit-layout"
|
|
11
|
+
>
|
|
12
|
+
Content
|
|
13
|
+
</div>
|
|
14
|
+
</main>
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
exports[`Main > snapshot: no props 1`] = `
|
|
18
|
+
<main
|
|
19
|
+
class="ucl-uikit-main"
|
|
20
|
+
data-testid="ucl-uikit-main"
|
|
21
|
+
>
|
|
22
|
+
Content
|
|
23
|
+
</main>
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
exports[`Main > snapshot: testId prop 1`] = `
|
|
27
|
+
<main
|
|
28
|
+
class="ucl-uikit-main"
|
|
29
|
+
data-testid="test123"
|
|
30
|
+
>
|
|
31
|
+
Content
|
|
32
|
+
</main>
|
|
33
|
+
`;
|