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.
Files changed (158) hide show
  1. package/README.md +4 -2
  2. package/dist/components/Accordion/Accordion.Heading.d.ts +4 -4
  3. package/dist/components/Accordion/Accordion.Panel.d.ts +2 -2
  4. package/dist/components/Accordion/Accordion.d.ts +1 -1
  5. package/dist/components/Accordion/Accordion.stories.d.ts +57 -0
  6. package/dist/components/Accordion/index.d.ts +2 -0
  7. package/dist/components/Avatar/Avatar.stories.d.ts +107 -1
  8. package/dist/components/Button/Button.d.ts +1 -0
  9. package/dist/components/Calendar/index.d.ts +1 -1
  10. package/dist/components/Datepicker/Datepicker.d.ts +1 -1
  11. package/dist/components/Datepicker/Datepicker.stories.d.ts +4 -3
  12. package/dist/components/Datepicker/Datepicker.types.d.ts +4 -5
  13. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +4 -1
  14. package/dist/components/Datepicker/subcomponents/DatepickerInput.d.ts +15 -2
  15. package/dist/components/Datepicker/subcomponents/Panel.d.ts +1 -1
  16. package/dist/components/Datepicker/subcomponents/VisibleField.d.ts +6 -1
  17. package/dist/components/Datepicker/subcomponents/index.d.ts +0 -1
  18. package/dist/components/Datepicker/utils/index.d.ts +0 -1
  19. package/dist/components/Dialog/BaseDialog.d.ts +2 -1
  20. package/dist/components/Dialog/Dialog.d.ts +2 -0
  21. package/dist/components/Header/Header.d.ts +4 -1
  22. package/dist/components/Header/Header.stories.d.ts +40 -0
  23. package/dist/components/Main/Main.d.ts +21 -0
  24. package/dist/components/Main/Main.stories.d.ts +15 -0
  25. package/dist/components/Main/index.d.ts +2 -0
  26. package/dist/components/NativeDatepicker/NativeDatepicker.d.ts +3 -0
  27. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +36 -0
  28. package/dist/components/NativeDatepicker/NativeDatepicker.types.d.ts +10 -0
  29. package/dist/components/NativeDatepicker/index.d.ts +2 -0
  30. package/dist/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.d.ts +1 -1
  31. package/dist/components/NativeDatepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts +1 -0
  32. package/dist/components/NativeDatepicker/utils/index.d.ts +1 -0
  33. package/dist/components/Select/Select.stories.d.ts +154 -2
  34. package/dist/components/Select/Select.types.d.ts +51 -22
  35. package/dist/components/Select/subcomponents/CustomOption.d.ts +1 -1
  36. package/dist/components/Select/subcomponents/CustomSelect.d.ts +3 -2
  37. package/dist/components/Select/subcomponents/FilterInput.d.ts +14 -0
  38. package/dist/components/Select/subcomponents/NativeSelect.d.ts +5 -1
  39. package/dist/components/Select/subcomponents/VisibleField.d.ts +3 -1
  40. package/dist/components/Select/subcomponents/index.d.ts +1 -0
  41. package/dist/components/WeekPicker/WeekPicker.d.ts +2 -2
  42. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +41 -0
  43. package/dist/components/WeekPicker/WeekPicker.types.d.ts +16 -0
  44. package/dist/components/WeekPicker/index.d.ts +1 -0
  45. package/dist/components/WeekPicker/subcomponents/CustomDatepicker.d.ts +1 -1
  46. package/dist/components/index.d.ts +8 -0
  47. package/dist/hooks/useFocusTrap.d.ts +2 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.js +4366 -3768
  50. package/dist/utils/__tests__/announce.test.d.ts +1 -0
  51. package/dist/utils/announce.d.ts +6 -0
  52. package/dist/utils/index.d.ts +1 -0
  53. package/lib/components/Accordion/Accordion.Heading.tsx +27 -8
  54. package/lib/components/Accordion/Accordion.Panel.tsx +11 -3
  55. package/lib/components/Accordion/Accordion.stories.tsx +139 -0
  56. package/lib/components/Accordion/Accordion.tsx +10 -8
  57. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +7 -7
  58. package/lib/components/Accordion/index.ts +2 -0
  59. package/lib/components/Alert/Alert.stories.tsx +1 -1
  60. package/lib/components/Avatar/Avatar.mdx +117 -0
  61. package/lib/components/Avatar/Avatar.stories.tsx +110 -2
  62. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  63. package/lib/components/Button/Button.stories.tsx +1 -1
  64. package/lib/components/Button/Button.tsx +1 -0
  65. package/lib/components/Calendar/Calendar.stories.tsx +12 -32
  66. package/lib/components/Calendar/__tests__/Calendar.test.tsx +23 -15
  67. package/lib/components/Calendar/index.ts +1 -5
  68. package/lib/components/Calendar/subcomponents/AcademicWeeks.tsx +2 -1
  69. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +5 -1
  70. package/lib/components/Calendar/subcomponents/EventDot.tsx +2 -1
  71. package/lib/components/Calendar/subcomponents/index.ts +1 -1
  72. package/lib/components/Calendar/utils/getDatesForCalendarGrid/getDatesForCalendarGrid.ts +43 -11
  73. package/lib/components/Calendar/utils/normaliseMonth/normaliseMonth.test.ts +5 -5
  74. package/lib/components/Datepicker/Datepicker.lld.md +108 -0
  75. package/lib/components/Datepicker/Datepicker.stories.tsx +44 -5
  76. package/lib/components/Datepicker/Datepicker.tsx +14 -36
  77. package/lib/components/Datepicker/Datepicker.types.ts +5 -14
  78. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +150 -8
  79. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +10 -4
  80. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +39 -5
  81. package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +30 -17
  82. package/lib/components/Datepicker/subcomponents/Panel.tsx +6 -2
  83. package/lib/components/Datepicker/subcomponents/VisibleField.tsx +40 -3
  84. package/lib/components/Datepicker/subcomponents/index.ts +0 -1
  85. package/lib/components/Datepicker/utils/index.ts +0 -1
  86. package/lib/components/Dialog/BaseDialog.tsx +11 -0
  87. package/lib/components/Dialog/Dialog.tsx +8 -1
  88. package/lib/components/Dialog/DialogBody.tsx +5 -1
  89. package/lib/components/Dialog/DialogHeader.tsx +2 -1
  90. package/lib/components/Divider/Divider.stories.tsx +1 -1
  91. package/lib/components/Field/ErrorText.tsx +1 -0
  92. package/lib/components/Field/Field.stories.tsx +1 -1
  93. package/lib/components/Field/__tests__/Field.test.tsx +13 -0
  94. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  95. package/lib/components/Footer/Footer.stories.tsx +1 -1
  96. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
  97. package/lib/components/Header/Header.mdx +52 -0
  98. package/lib/components/Header/Header.stories.tsx +98 -0
  99. package/lib/components/Header/Header.tsx +51 -6
  100. package/lib/components/Header/__tests__/Header.test.tsx +17 -1
  101. package/lib/components/Heading/Heading.stories.tsx +1 -1
  102. package/lib/components/Icon/Icon.stories.tsx +1 -1
  103. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  104. package/lib/components/Input/Input.stories.tsx +1 -1
  105. package/lib/components/Label/Label.stories.tsx +1 -1
  106. package/lib/components/Main/Main.stories.tsx +36 -0
  107. package/lib/components/Main/Main.tsx +46 -0
  108. package/lib/components/Main/__tests__/Main.test.tsx +80 -0
  109. package/lib/components/Main/__tests__/__snapshots__/Main.test.tsx.snap +33 -0
  110. package/lib/components/Main/index.ts +2 -0
  111. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +100 -0
  112. package/lib/components/{Datepicker/subcomponents → NativeDatepicker}/NativeDatepicker.tsx +14 -15
  113. package/lib/components/NativeDatepicker/NativeDatepicker.types.ts +19 -0
  114. package/lib/components/NativeDatepicker/index.ts +2 -0
  115. package/lib/components/{Datepicker → NativeDatepicker}/utils/dateToLocaleISOString/dateToLocaleISOString.ts +1 -1
  116. package/lib/components/NativeDatepicker/utils/index.ts +1 -0
  117. package/lib/components/Pagination/PaginationControls.tsx +55 -12
  118. package/lib/components/Pagination/PaginationInfo.tsx +5 -1
  119. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  120. package/lib/components/Search/Search.stories.tsx +1 -1
  121. package/lib/components/Search/Search.tsx +4 -1
  122. package/lib/components/Search/__tests__/Search.test.tsx +19 -1
  123. package/lib/components/Select/Select.mdx +169 -0
  124. package/lib/components/Select/Select.stories.tsx +191 -43
  125. package/lib/components/Select/Select.tsx +36 -12
  126. package/lib/components/Select/Select.types.ts +66 -48
  127. package/lib/components/Select/__tests__/Select.test.tsx +448 -7
  128. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
  129. package/lib/components/Select/subcomponents/CustomOption.tsx +2 -1
  130. package/lib/components/Select/subcomponents/CustomSelect.tsx +303 -33
  131. package/lib/components/Select/subcomponents/FilterInput.tsx +80 -0
  132. package/lib/components/Select/subcomponents/NativeSelect.tsx +13 -1
  133. package/lib/components/Select/subcomponents/VisibleField.tsx +11 -3
  134. package/lib/components/Select/subcomponents/index.tsx +1 -0
  135. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  136. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  137. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  138. package/lib/components/Timepicker/Timepicker.tsx +4 -0
  139. package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
  140. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  141. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  142. package/lib/components/WeekPicker/WeekPicker.stories.tsx +147 -0
  143. package/lib/components/WeekPicker/WeekPicker.tsx +2 -2
  144. package/lib/components/WeekPicker/WeekPicker.types.ts +21 -0
  145. package/lib/components/WeekPicker/index.ts +1 -0
  146. package/lib/components/WeekPicker/subcomponents/CustomDatepicker.tsx +1 -1
  147. package/lib/components/common/Common.mdx +1 -1
  148. package/lib/components/index.ts +11 -2
  149. package/lib/hooks/useFocusTrap.ts +40 -4
  150. package/lib/index.ts +1 -0
  151. package/lib/utils/__tests__/announce.test.ts +121 -0
  152. package/lib/utils/announce.ts +134 -0
  153. package/lib/utils/index.ts +1 -0
  154. package/package.json +3 -6
  155. package/dist/components/Datepicker/subcomponents/NativeDatepicker.d.ts +0 -6
  156. package/lib/components/Accordion/Accordion.stories.tsx.NOT_READY +0 -93
  157. /package/dist/components/{Datepicker/utils/dateToLocaleISOString/dateToLocaleISOString.test.d.ts → Main/__tests__/Main.test.d.ts} +0 -0
  158. /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>
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
3
3
  import Divider from './Divider';
4
4
 
5
5
  const meta = {
6
- title: 'Components/Ready to use/Divider',
6
+ title: 'Components/Divider',
7
7
  component: Divider,
8
8
  parameters: {
9
9
  layout: 'padded',
@@ -45,6 +45,7 @@ const ErrorText = ({
45
45
  className={style}
46
46
  data-testid={testId}
47
47
  {...props}
48
+ role='alert'
48
49
  >
49
50
  <Icon.Info className={iconStyle} />
50
51
  {children}
@@ -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/Ready to use/Field',
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/WorkInProgress/FileInput',
5
+ title: 'Components/Work in progress/FileInput',
6
6
  component: FileInput,
7
7
  parameters: {
8
8
  layout: 'centered',
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
3
3
  import Footer from './Footer';
4
4
 
5
5
  const meta = {
6
- title: 'Components/Ready to use/Footer',
6
+ title: 'Components/Footer',
7
7
  component: Footer,
8
8
  parameters: {
9
9
  layout: 'fullscreen',
@@ -247,7 +247,7 @@ exports[`Footer > snapshot: footer links provided 1`] = `
247
247
  class="css-bq4k8p"
248
248
  >
249
249
  ©
250
- 2025
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
- 2025
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
- 2025
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
- return (
94
- <header
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
  });
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
3
3
  import Heading from './Heading';
4
4
 
5
5
  const meta = {
6
- title: 'Components/Ready to use/Heading',
6
+ title: 'Components/Heading',
7
7
  component: Heading,
8
8
  parameters: {
9
9
  layout: 'centered',
@@ -3,7 +3,7 @@ import Icon from './Icon';
3
3
  import { ComponentType } from 'react';
4
4
 
5
5
  const meta: Meta = {
6
- title: 'Components/Ready to use/Icon',
6
+ title: 'Components/Icon',
7
7
  component: Icon,
8
8
  parameters: {
9
9
  // Appearing automatically, due to composition
@@ -4,7 +4,7 @@ import IconButton from './IconButton';
4
4
  import Icon from '../Icon/Icon';
5
5
 
6
6
  const meta: Meta = {
7
- title: 'Components/Ready to use/IconButton',
7
+ title: 'Components/IconButton',
8
8
  component: IconButton,
9
9
  argTypes: {
10
10
  icon: {
@@ -3,7 +3,7 @@ import { Input } from '../../../lib';
3
3
  import { Icon } from '../../../lib';
4
4
 
5
5
  const meta: Meta<typeof Input> = {
6
- title: 'Components/Ready to use/Input',
6
+ title: 'Components/Input',
7
7
  component: Input,
8
8
  parameters: {
9
9
  layout: 'centered',
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
3
3
  import Label from './Label';
4
4
 
5
5
  const meta = {
6
- title: 'Components/Ready to use/Label',
6
+ title: 'Components/Label',
7
7
  component: Label,
8
8
  } satisfies Meta<typeof Label>;
9
9
 
@@ -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
+ `;
@@ -0,0 +1,2 @@
1
+ export { default } from './Main';
2
+ export type { MainProps } from './Main';