uikit-react-public 0.43.2 → 0.44.0

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 (78) hide show
  1. package/dist/components/Header/Header.d.ts +2 -1
  2. package/dist/components/Header/Header.stories.d.ts +5 -1
  3. package/dist/components/HeaderNew/Header.d.ts +2 -1
  4. package/dist/components/NavigationBar/NavigationBar.d.ts +12 -0
  5. package/dist/components/NavigationBar/NavigationBar.stories.d.ts +14 -0
  6. package/dist/components/NavigationBar/NavigationBarItem.d.ts +19 -0
  7. package/dist/components/NavigationBar/__tests__/NavigationBar.test.d.ts +1 -0
  8. package/dist/components/NavigationBar/index.d.ts +2 -0
  9. package/dist/components/Paragraph/Paragraph.d.ts +1 -1
  10. package/dist/components/index.d.ts +2 -0
  11. package/dist/index.js +5591 -5247
  12. package/lib/components/Accordion/Accordion.Heading.tsx +0 -2
  13. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +2 -2
  14. package/lib/components/Alert/__tests__/__snapshots__/Alert.test.tsx.snap +4 -4
  15. package/lib/components/Avatar/Avatar.tsx +7 -1
  16. package/lib/components/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap +12 -12
  17. package/lib/components/Badge/Badge.tsx +6 -11
  18. package/lib/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap +9 -9
  19. package/lib/components/Button/Button.tsx +11 -7
  20. package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +9 -9
  21. package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +18 -18
  22. package/lib/components/Calendar/subcomponents/AcademicWeek.tsx +7 -5
  23. package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +12 -7
  24. package/lib/components/Calendar/subcomponents/ControlsSelect.tsx +11 -6
  25. package/lib/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap +2 -2
  26. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +2 -2
  27. package/lib/components/DrawerMenu/DrawerMenu.tsx +4 -3
  28. package/lib/components/DropdownMenu/DropdownMenu.tsx +4 -3
  29. package/lib/components/DropdownUserMenu/DropdownUserMenu.tsx +8 -5
  30. package/lib/components/Field/CharacterCount.tsx +7 -6
  31. package/lib/components/Field/ErrorText.tsx +7 -6
  32. package/lib/components/FileInput/FileInput.tsx +11 -3
  33. package/lib/components/FileInput/__tests__/__snapshots__/FileInput.test.tsx.snap +4 -4
  34. package/lib/components/Footer/Footer.tsx +16 -3
  35. package/lib/components/Footer/FooterColumn.tsx +9 -4
  36. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +13 -11
  37. package/lib/components/FooterNew/BackToTop.tsx +12 -4
  38. package/lib/components/FooterNew/FooterColumn.tsx +4 -0
  39. package/lib/components/FooterNew/FooterNavLink.tsx +12 -6
  40. package/lib/components/FooterNew/LegalAndCopyright.tsx +11 -6
  41. package/lib/components/FooterNew/LogoAddressAndSocial.tsx +27 -15
  42. package/lib/components/FooterNew/__tests__/__snapshots__/Footer.test.tsx.snap +32 -26
  43. package/lib/components/Header/Header.mdx +8 -1
  44. package/lib/components/Header/Header.stories.tsx +13 -0
  45. package/lib/components/Header/Header.tsx +3 -0
  46. package/lib/components/Header/__tests__/Header.test.tsx +22 -0
  47. package/lib/components/HeaderDraft/HeaderBottomBreadcrumbs.tsx +6 -1
  48. package/lib/components/HeaderDraft/HeaderBottomHeadingAndAvatar.tsx +19 -8
  49. package/lib/components/HeaderDraft/HeaderBreadcrumb.tsx +11 -2
  50. package/lib/components/HeaderDraft/HeaderDraft.tsx +16 -5
  51. package/lib/components/HeaderDraft/__tests__/__snapshots__/Header.test.tsx.snap +7 -7
  52. package/lib/components/HeaderNew/Header.tsx +7 -1
  53. package/lib/components/HeaderNew/HeaderItem.tsx +6 -0
  54. package/lib/components/HeaderNew/__tests__/Header.test.tsx +41 -0
  55. package/lib/components/Input/__tests__/__snapshots__/Input.test.tsx.snap +4 -4
  56. package/lib/components/Menu/MenuContent.tsx +9 -1
  57. package/lib/components/NativeDatepicker/NativeDatepicker.tsx +6 -2
  58. package/lib/components/NavigationBar/NavigationBar.stories.tsx +99 -0
  59. package/lib/components/NavigationBar/NavigationBar.tsx +76 -0
  60. package/lib/components/NavigationBar/NavigationBarItem.tsx +236 -0
  61. package/lib/components/NavigationBar/__tests__/NavigationBar.test.tsx +90 -0
  62. package/lib/components/NavigationBar/index.ts +5 -0
  63. package/lib/components/Pagination/PaginationInfo.tsx +8 -6
  64. package/lib/components/Pagination/subcomponents/PaginationCompactControls.tsx +8 -2
  65. package/lib/components/Pagination/subcomponents/PaginationStandardControls.tsx +2 -1
  66. package/lib/components/Paragraph/Paragraph.tsx +6 -4
  67. package/lib/components/Paragraph/__tests__/Paragraph.test.tsx +1 -1
  68. package/lib/components/Paragraph/__tests__/__snapshots__/Paragraph.test.tsx.snap +9 -9
  69. package/lib/components/Radio/__tests__/__snapshots__/Radio.test.tsx.snap +2 -2
  70. package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +2 -2
  71. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
  72. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +2 -2
  73. package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +1 -1
  74. package/lib/components/Textarea/__tests__/__snapshots__/Textarea.test.tsx.snap +4 -4
  75. package/lib/components/Tooltip/Tooltip.tsx +5 -8
  76. package/lib/components/Tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap +2 -2
  77. package/lib/components/index.ts +6 -0
  78. package/package.json +2 -2
@@ -40,6 +40,47 @@ describe('Header', () => {
40
40
  expect(link.getAttribute('href')).toBe('/');
41
41
  });
42
42
 
43
+ test('shows the bottom border by default and can hide it', () => {
44
+ const { rerender } = render(
45
+ <ThemeContextProvider>
46
+ <Header title='LIDS' />
47
+ </ThemeContextProvider>
48
+ );
49
+
50
+ const header = screen.getByTestId('ucl-uikit-header');
51
+ const defaultClass = [...header.classList].find((className) =>
52
+ className.startsWith('css-')
53
+ );
54
+
55
+ expect(defaultClass).toBeDefined();
56
+ expect(document.head.textContent).toContain(
57
+ `.${defaultClass}{position:relative`
58
+ );
59
+ expect(document.head.textContent).toContain(
60
+ `.${defaultClass}{height:78px;padding:0 48px 0 64px;border-bottom:1px solid`
61
+ );
62
+
63
+ rerender(
64
+ <ThemeContextProvider>
65
+ <Header
66
+ title='LIDS'
67
+ showBorder={false}
68
+ />
69
+ </ThemeContextProvider>
70
+ );
71
+
72
+ const borderlessHeader = screen.getByTestId('ucl-uikit-header');
73
+ const borderlessClass = [...borderlessHeader.classList].find((className) =>
74
+ className.startsWith('css-')
75
+ );
76
+
77
+ expect(borderlessClass).toBeDefined();
78
+ expect(borderlessHeader).not.toHaveAttribute('showBorder');
79
+ expect(document.head.textContent).toContain(
80
+ `.${borderlessClass}{height:78px;padding:0 48px 0 64px;}`
81
+ );
82
+ });
83
+
43
84
  test('spaces subsequent header items on smaller screens and resets spacing on desktop', () => {
44
85
  render(
45
86
  <ThemeContextProvider>
@@ -5,7 +5,7 @@ exports[`Input > snapshot: no props 1`] = `
5
5
  class="ucl-uikit-input css-1bpb1dw"
6
6
  >
7
7
  <input
8
- class="ucl-uikit-input__input css-10ijqev"
8
+ class="ucl-uikit-input__input css-kvsjyn"
9
9
  data-testid="ucl-uikit-input"
10
10
  id=""
11
11
  />
@@ -17,7 +17,7 @@ exports[`Input > snapshot: placeholder prop 1`] = `
17
17
  class="ucl-uikit-input css-1bpb1dw"
18
18
  >
19
19
  <input
20
- class="ucl-uikit-input__input css-10ijqev"
20
+ class="ucl-uikit-input__input css-kvsjyn"
21
21
  data-testid="ucl-uikit-input"
22
22
  id=""
23
23
  placeholder="Type your value..."
@@ -30,7 +30,7 @@ exports[`Input > snapshot: testId prop 1`] = `
30
30
  class="ucl-uikit-input css-1bpb1dw"
31
31
  >
32
32
  <input
33
- class="ucl-uikit-input__input css-10ijqev"
33
+ class="ucl-uikit-input__input css-kvsjyn"
34
34
  data-testid="test123"
35
35
  id=""
36
36
  />
@@ -42,7 +42,7 @@ exports[`Input > snapshot: value prop 1`] = `
42
42
  class="ucl-uikit-input css-1bpb1dw"
43
43
  >
44
44
  <input
45
- class="ucl-uikit-input__input css-10ijqev"
45
+ class="ucl-uikit-input__input css-kvsjyn"
46
46
  data-testid="ucl-uikit-input"
47
47
  id=""
48
48
  value="abc"
@@ -5,6 +5,7 @@ import useTheme from '../../theme/useTheme';
5
5
  import useMediaQuery from '../../hooks/useMediaQuery';
6
6
  import Icon from '../Icon';
7
7
  import IconButton from '../IconButton';
8
+ import Heading from '../Heading';
8
9
  import { useAppMenu } from './Menu.context';
9
10
 
10
11
  export const NAME = 'ucl-uikit-menu__content';
@@ -113,7 +114,14 @@ const MenuContent: React.FC<MenuContentProps> = ({
113
114
  className={style}
114
115
  >
115
116
  <header className={headerStyle}>
116
- <h3 className={titleStyle}>{title}</h3>
117
+ <Heading
118
+ as='h3'
119
+ level='xs'
120
+ margins={false}
121
+ className={titleStyle}
122
+ >
123
+ {title}
124
+ </Heading>
117
125
  <IconButton
118
126
  onClick={toggleMenu}
119
127
  aria-label='Close menu'
@@ -17,6 +17,7 @@ const NativeDatepicker = ({
17
17
  ...props
18
18
  }: NativeDatepickerProps) => {
19
19
  const [theme] = useTheme();
20
+ const { md } = theme.typography.body;
20
21
 
21
22
  const baseStyle = css`
22
23
  width: 196px;
@@ -25,8 +26,11 @@ const NativeDatepicker = ({
25
26
  padding: 0 16px;
26
27
  border: 1px solid ${theme.colour.speciality.inputDefault};
27
28
  color: ${theme.colour.text.default};
28
- font-size: 16px;
29
- font-family: sans-serif;
29
+ font-family: ${md.fontFamily};
30
+ font-feature-settings: ${md.fontSettings};
31
+ font-size: ${md.fontSize}px;
32
+ font-weight: ${md.fontWeight};
33
+ line-height: ${md.lineHeight}%;
30
34
  text-transform: uppercase;
31
35
  cursor: pointer;
32
36
 
@@ -0,0 +1,99 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { css } from '@emotion/css';
3
+ import NavigationBar from './NavigationBar';
4
+ import Icon from '../Icon';
5
+
6
+ const meta = {
7
+ title: 'Components/NavigationBar',
8
+ component: NavigationBar,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ },
12
+ tags: ['autodocs'],
13
+ } satisfies Meta<typeof NavigationBar>;
14
+
15
+ export default meta;
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ const pageStyle = css`
19
+ min-height: 160px;
20
+ `;
21
+
22
+ export const Default: Story = {
23
+ render: () => (
24
+ <div className={pageStyle}>
25
+ <NavigationBar>
26
+ <NavigationBar.Item href='/courses'>Courses</NavigationBar.Item>
27
+ <NavigationBar.Item
28
+ href='/research'
29
+ active
30
+ >
31
+ Research
32
+ </NavigationBar.Item>
33
+ <NavigationBar.Item
34
+ href='/open-days'
35
+ icon={<Icon.ChevronDown />}
36
+ >
37
+ Open days
38
+ </NavigationBar.Item>
39
+ <NavigationBar.Item href='/events'>Events</NavigationBar.Item>
40
+ <NavigationBar.Item
41
+ href='/news'
42
+ disabled
43
+ >
44
+ News
45
+ </NavigationBar.Item>
46
+ </NavigationBar>
47
+ </div>
48
+ ),
49
+ };
50
+
51
+ export const Wrapped: Story = {
52
+ render: () => (
53
+ <div
54
+ className={css`
55
+ width: 720px;
56
+ `}
57
+ >
58
+ <NavigationBar>
59
+ <NavigationBar.Item
60
+ href='/courses'
61
+ active
62
+ >
63
+ Courses
64
+ </NavigationBar.Item>
65
+ <NavigationBar.Item href='/research'>Research</NavigationBar.Item>
66
+ <NavigationBar.Item href='/open-days'>Open days</NavigationBar.Item>
67
+ <NavigationBar.Item href='/events'>Events</NavigationBar.Item>
68
+ <NavigationBar.Item href='/news'>News</NavigationBar.Item>
69
+ <NavigationBar.Item href='/blogs'>IOE blogs</NavigationBar.Item>
70
+ <NavigationBar.Item href='/work-with-us'>
71
+ Work with us
72
+ </NavigationBar.Item>
73
+ <NavigationBar.Item href='/departments-and-centres'>
74
+ Departments and Centres
75
+ </NavigationBar.Item>
76
+ <NavigationBar.Item href='/about'>About IOE</NavigationBar.Item>
77
+ </NavigationBar>
78
+ </div>
79
+ ),
80
+ };
81
+
82
+ export const WithRouterLinks: Story = {
83
+ render: () => (
84
+ <NavigationBar>
85
+ <NavigationBar.Item
86
+ active
87
+ asChild
88
+ >
89
+ <a href='/overview'>Overview</a>
90
+ </NavigationBar.Item>
91
+ <NavigationBar.Item asChild>
92
+ <a href='/modules'>Modules</a>
93
+ </NavigationBar.Item>
94
+ <NavigationBar.Item asChild>
95
+ <a href='/people'>People</a>
96
+ </NavigationBar.Item>
97
+ </NavigationBar>
98
+ ),
99
+ };
@@ -0,0 +1,76 @@
1
+ import {
2
+ Children,
3
+ HTMLAttributes,
4
+ NamedExoticComponent,
5
+ isValidElement,
6
+ memo,
7
+ } from 'react';
8
+ import { css, cx } from '@emotion/css';
9
+ import { useTheme } from '../../theme';
10
+ import NavigationBarItem, { NavigationBarItemProps } from './NavigationBarItem';
11
+
12
+ export const NAME = 'ucl-uikit-navigation-bar';
13
+
14
+ export interface NavigationBarProps extends HTMLAttributes<HTMLElement> {
15
+ testId?: string;
16
+ }
17
+
18
+ const NavigationBarBase = ({
19
+ testId = NAME,
20
+ className,
21
+ children,
22
+ 'aria-label': ariaLabel = 'Primary navigation',
23
+ ...props
24
+ }: NavigationBarProps) => {
25
+ const [theme] = useTheme();
26
+ const items = Children.toArray(children).filter(isValidElement);
27
+
28
+ const baseStyle = css`
29
+ box-sizing: border-box;
30
+ display: flex;
31
+ width: 100%;
32
+ padding: 0 ${theme.padding.p16};
33
+ align-items: flex-end;
34
+ background-color: ${theme.colour.surface.primary};
35
+ border-bottom: ${theme.border.b1} solid ${theme.colour.border.subtle};
36
+
37
+ @media (min-width: ${theme.breakpoints.tablet}px) {
38
+ padding: 0 ${theme.padding.p48};
39
+ }
40
+ `;
41
+
42
+ const listStyle = css`
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ align-items: center;
46
+ gap: 2px 0;
47
+ margin: 0;
48
+ padding: 0;
49
+ list-style: none;
50
+ `;
51
+
52
+ const style = cx(NAME, baseStyle, className);
53
+
54
+ return (
55
+ <nav
56
+ className={style}
57
+ data-testid={testId}
58
+ aria-label={ariaLabel}
59
+ {...props}
60
+ >
61
+ <ul className={listStyle}>{items}</ul>
62
+ </nav>
63
+ );
64
+ };
65
+
66
+ export interface NavigationBarComponent extends NamedExoticComponent<NavigationBarProps> {
67
+ Item: typeof NavigationBarItem;
68
+ }
69
+
70
+ const NavigationBar = Object.assign(memo(NavigationBarBase), {
71
+ Item: NavigationBarItem,
72
+ }) as NavigationBarComponent;
73
+
74
+ export type { NavigationBarItemProps };
75
+
76
+ export default NavigationBar;
@@ -0,0 +1,236 @@
1
+ import {
2
+ Children,
3
+ ComponentPropsWithoutRef,
4
+ ElementType,
5
+ ReactElement,
6
+ ReactNode,
7
+ cloneElement,
8
+ forwardRef,
9
+ isValidElement,
10
+ memo,
11
+ } from 'react';
12
+ import { css, cx } from '@emotion/css';
13
+ import { useTheme } from '../../theme';
14
+ import Text from '../Text';
15
+ import BaseLink, { BaseLinkProps } from '../Link/BaseLink';
16
+ import type { IconProps } from '../Icon';
17
+
18
+ export const NAME = 'ucl-uikit-navigation-bar__item';
19
+
20
+ type NavigationBarItemOwnProps = {
21
+ active?: boolean;
22
+ disabled?: boolean;
23
+ icon?: ReactElement<IconProps>;
24
+ iconPosition?: 'left' | 'right';
25
+ testId?: string;
26
+ className?: string;
27
+ children?: ReactNode;
28
+ };
29
+
30
+ export type NavigationBarItemProps<C extends ElementType = 'a'> =
31
+ NavigationBarItemOwnProps &
32
+ BaseLinkProps<C> &
33
+ Omit<ComponentPropsWithoutRef<C>, keyof NavigationBarItemOwnProps>;
34
+
35
+ type NavigationBarItemComponent = <C extends ElementType = 'a'>(
36
+ props: NavigationBarItemProps<C> & {
37
+ ref?: React.ComponentPropsWithRef<C>['ref'];
38
+ }
39
+ ) => React.JSX.Element;
40
+
41
+ const NavigationBarItem = forwardRef(function NavigationBarItemInner(
42
+ {
43
+ active = false,
44
+ disabled = false,
45
+ icon,
46
+ iconPosition = 'right',
47
+ testId = NAME,
48
+ className,
49
+ children,
50
+ asChild = false,
51
+ ...props
52
+ }: NavigationBarItemProps<ElementType>,
53
+ ref: React.Ref<Element>
54
+ ) {
55
+ const [theme] = useTheme();
56
+
57
+ const listItemStyle = css`
58
+ display: inline-flex;
59
+ `;
60
+
61
+ const baseStyle = css`
62
+ box-sizing: border-box;
63
+ display: inline-flex;
64
+ min-height: 40px;
65
+ padding: ${theme.padding.p8} ${theme.padding.p16} 0;
66
+ align-items: stretch;
67
+ justify-content: center;
68
+ color: ${theme.colour.link.primary};
69
+ background-color: transparent;
70
+ text-decoration: none;
71
+ cursor: pointer;
72
+ outline: none;
73
+
74
+ &:visited {
75
+ color: ${theme.colour.link.primary};
76
+ }
77
+
78
+ &:hover {
79
+ color: ${theme.colour.link.primary};
80
+ background-color: ${theme.colour.fill.brandSubtleHover};
81
+ text-decoration: none;
82
+ }
83
+
84
+ &:focus-visible {
85
+ box-shadow: ${theme.boxShadow.focus};
86
+ z-index: 1;
87
+ }
88
+ `;
89
+
90
+ const activeStyle = css`
91
+ color: ${theme.colour.link.primary};
92
+
93
+ &:visited,
94
+ &:hover {
95
+ color: ${theme.colour.link.primary};
96
+ }
97
+ `;
98
+
99
+ const disabledStyle = css`
100
+ color: ${theme.colour.link.disabled};
101
+ background-color: transparent;
102
+ cursor: not-allowed;
103
+
104
+ &:visited,
105
+ &:hover {
106
+ color: ${theme.colour.link.disabled};
107
+ background-color: transparent;
108
+ }
109
+ `;
110
+
111
+ const contentStyle = css`
112
+ display: inline-flex;
113
+ flex-direction: column;
114
+ align-items: stretch;
115
+ justify-content: flex-start;
116
+ gap: 2px;
117
+ `;
118
+
119
+ const labelRowStyle = css`
120
+ display: inline-flex;
121
+ min-height: 24px;
122
+ align-items: center;
123
+ justify-content: center;
124
+ gap: ${theme.padding.p4};
125
+ color: inherit;
126
+ white-space: nowrap;
127
+ `;
128
+
129
+ const textStackStyle = css`
130
+ display: inline-grid;
131
+ align-items: center;
132
+ justify-items: start;
133
+ color: inherit;
134
+ `;
135
+
136
+ const textStyle = css`
137
+ grid-area: 1 / 1;
138
+ color: inherit;
139
+ white-space: nowrap;
140
+ `;
141
+
142
+ const hiddenTextStyle = css`
143
+ height: 0;
144
+ overflow: hidden;
145
+ visibility: hidden;
146
+ pointer-events: none;
147
+ `;
148
+
149
+ const indicatorStyle = css`
150
+ display: block;
151
+ height: 6px;
152
+ background-color: ${active ? theme.colour.fill.brand : 'transparent'};
153
+ `;
154
+
155
+ const iconStyle = css`
156
+ display: block;
157
+ flex-shrink: 0;
158
+ `;
159
+
160
+ const style = cx(
161
+ NAME,
162
+ baseStyle,
163
+ active && activeStyle,
164
+ disabled && disabledStyle,
165
+ className
166
+ );
167
+
168
+ const renderContent = (content: ReactNode) => (
169
+ <span className={contentStyle}>
170
+ <span className={labelRowStyle}>
171
+ {icon &&
172
+ iconPosition === 'left' &&
173
+ cloneElement(icon, {
174
+ size: icon.props.size ?? 20,
175
+ className: cx(iconStyle, icon.props.className),
176
+ 'aria-hidden': icon.props['aria-hidden'] ?? true,
177
+ })}
178
+ <span className={textStackStyle}>
179
+ <Text
180
+ level={active ? 'md-semibold' : 'md'}
181
+ className={textStyle}
182
+ >
183
+ {content}
184
+ </Text>
185
+ <Text
186
+ aria-hidden
187
+ level='md-semibold'
188
+ className={cx(textStyle, hiddenTextStyle)}
189
+ >
190
+ {content}
191
+ </Text>
192
+ </span>
193
+ {icon &&
194
+ iconPosition === 'right' &&
195
+ cloneElement(icon, {
196
+ size: icon.props.size ?? 20,
197
+ className: cx(iconStyle, icon.props.className),
198
+ 'aria-hidden': icon.props['aria-hidden'] ?? true,
199
+ })}
200
+ </span>
201
+ <span
202
+ className={indicatorStyle}
203
+ aria-hidden='true'
204
+ />
205
+ </span>
206
+ );
207
+
208
+ const content = asChild
209
+ ? (() => {
210
+ const child = Children.only(children);
211
+
212
+ return isValidElement<{ children?: ReactNode }>(child)
213
+ ? cloneElement(child, {}, renderContent(child.props.children))
214
+ : child;
215
+ })()
216
+ : renderContent(children);
217
+
218
+ return (
219
+ <li className={listItemStyle}>
220
+ <BaseLink
221
+ ref={ref}
222
+ className={style}
223
+ testId={testId}
224
+ disabled={disabled}
225
+ asChild={asChild}
226
+ aria-current={active ? 'page' : undefined}
227
+ data-state={active ? 'active' : 'inactive'}
228
+ {...props}
229
+ >
230
+ {content}
231
+ </BaseLink>
232
+ </li>
233
+ );
234
+ }) as unknown as NavigationBarItemComponent;
235
+
236
+ export default memo(NavigationBarItem) as NavigationBarItemComponent;
@@ -0,0 +1,90 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import NavigationBar from '../NavigationBar';
4
+ import Icon from '../../Icon';
5
+ import { ThemeContextProvider } from '../../../theme/useTheme';
6
+
7
+ const wrap = (ui: React.ReactElement) =>
8
+ render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
9
+
10
+ describe('NavigationBar', () => {
11
+ test('renders a navigation landmark with link items', () => {
12
+ wrap(
13
+ <NavigationBar aria-label='Section navigation'>
14
+ <NavigationBar.Item href='/courses'>Courses</NavigationBar.Item>
15
+ <NavigationBar.Item href='/research'>Research</NavigationBar.Item>
16
+ </NavigationBar>
17
+ );
18
+
19
+ expect(
20
+ screen.getByRole('navigation', { name: 'Section navigation' })
21
+ ).toBeInTheDocument();
22
+ expect(screen.getByRole('link', { name: 'Courses' })).toHaveAttribute(
23
+ 'href',
24
+ '/courses'
25
+ );
26
+ });
27
+
28
+ test('marks the active item and reserves bold label width', () => {
29
+ wrap(
30
+ <NavigationBar>
31
+ <NavigationBar.Item
32
+ href='/courses'
33
+ active
34
+ >
35
+ Courses
36
+ </NavigationBar.Item>
37
+ </NavigationBar>
38
+ );
39
+
40
+ const item = screen.getByRole('link', { name: 'Courses' });
41
+ const generatedClass = [...item.classList].find((className) =>
42
+ className.startsWith('css-')
43
+ );
44
+
45
+ expect(item).toHaveAttribute('aria-current', 'page');
46
+ expect(item).toHaveAttribute('data-state', 'active');
47
+ expect(screen.getAllByText('Courses')).toHaveLength(2);
48
+ expect(generatedClass).toBeDefined();
49
+ expect(document.head.textContent).toContain('visibility:hidden');
50
+ expect(document.head.textContent).toContain('white-space:nowrap');
51
+ });
52
+
53
+ test('supports disabled items', () => {
54
+ wrap(
55
+ <NavigationBar>
56
+ <NavigationBar.Item
57
+ href='/news'
58
+ disabled
59
+ >
60
+ News
61
+ </NavigationBar.Item>
62
+ </NavigationBar>
63
+ );
64
+
65
+ const item = screen.getAllByText('News')[0].closest('a');
66
+
67
+ expect(item).toBeInTheDocument();
68
+ expect(item).toHaveAttribute('aria-disabled', 'true');
69
+ expect(item).toHaveAttribute('tabindex', '-1');
70
+ expect(item).not.toHaveAttribute('href');
71
+ });
72
+
73
+ test('supports icons and asChild links', () => {
74
+ wrap(
75
+ <NavigationBar>
76
+ <NavigationBar.Item
77
+ icon={<Icon.ChevronDown />}
78
+ asChild
79
+ >
80
+ <a href='/open-days'>Open days</a>
81
+ </NavigationBar.Item>
82
+ </NavigationBar>
83
+ );
84
+
85
+ const item = screen.getByRole('link', { name: 'Open days' });
86
+
87
+ expect(item).toHaveAttribute('href', '/open-days');
88
+ expect(item.querySelector('svg')).toBeInTheDocument();
89
+ });
90
+ });
@@ -0,0 +1,5 @@
1
+ export { default } from './NavigationBar';
2
+ export type {
3
+ NavigationBarItemProps,
4
+ NavigationBarProps,
5
+ } from './NavigationBar';
@@ -1,6 +1,7 @@
1
1
  import { useContext, memo } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
3
  import useTheme from '../../theme/useTheme';
4
+ import Paragraph from '../Paragraph';
4
5
  import { PaginationContext } from './Pagination';
5
6
 
6
7
  export const NAME = 'ucl-uikit-pagination__info';
@@ -14,7 +15,7 @@ export interface PaginationInfoProps extends React.HTMLAttributes<HTMLDivElement
14
15
  const PaginationInfo = ({
15
16
  format = 'pages',
16
17
  itemsPluralName,
17
- testId,
18
+ testId = NAME,
18
19
  className,
19
20
  ...props
20
21
  }: PaginationInfoProps) => {
@@ -51,8 +52,6 @@ const PaginationInfo = ({
51
52
  margin: ${theme.margin.m16} 0;
52
53
  text-align: center;
53
54
  color: ${theme.colour.text.secondary};
54
- font-family: ${theme.font.family.primary};
55
- font-size: ${theme.font.size.f16};
56
55
 
57
56
  @media (min-width: ${theme.breakpoints.tablet}px) {
58
57
  margin: ${theme.margin.m24} 0;
@@ -62,15 +61,18 @@ const PaginationInfo = ({
62
61
  const style = cx(NAME, baseStyle, className);
63
62
 
64
63
  return (
65
- <div
64
+ <Paragraph
65
+ as='div'
66
+ level='md'
67
+ noMargins
66
68
  className={style}
67
- data-testid={testId}
69
+ testId={testId}
68
70
  aria-live='polite'
69
71
  aria-label={`Pagination info: ${text}`}
70
72
  {...props}
71
73
  >
72
74
  {text}
73
- </div>
75
+ </Paragraph>
74
76
  );
75
77
  };
76
78