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.
- package/dist/components/Header/Header.d.ts +2 -1
- package/dist/components/Header/Header.stories.d.ts +5 -1
- package/dist/components/HeaderNew/Header.d.ts +2 -1
- package/dist/components/NavigationBar/NavigationBar.d.ts +12 -0
- package/dist/components/NavigationBar/NavigationBar.stories.d.ts +14 -0
- package/dist/components/NavigationBar/NavigationBarItem.d.ts +19 -0
- package/dist/components/NavigationBar/__tests__/NavigationBar.test.d.ts +1 -0
- package/dist/components/NavigationBar/index.d.ts +2 -0
- package/dist/components/Paragraph/Paragraph.d.ts +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/index.js +5591 -5247
- package/lib/components/Accordion/Accordion.Heading.tsx +0 -2
- package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +2 -2
- package/lib/components/Alert/__tests__/__snapshots__/Alert.test.tsx.snap +4 -4
- package/lib/components/Avatar/Avatar.tsx +7 -1
- package/lib/components/Avatar/__tests__/__snapshots__/Avatar.test.tsx.snap +12 -12
- package/lib/components/Badge/Badge.tsx +6 -11
- package/lib/components/Badge/__tests__/__snapshots__/Badge.test.tsx.snap +9 -9
- package/lib/components/Button/Button.tsx +11 -7
- package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +9 -9
- package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +18 -18
- package/lib/components/Calendar/subcomponents/AcademicWeek.tsx +7 -5
- package/lib/components/Calendar/subcomponents/ColumnHeading.tsx +12 -7
- package/lib/components/Calendar/subcomponents/ControlsSelect.tsx +11 -6
- package/lib/components/Checkbox/__tests__/__snapshots__/Checkbox.test.tsx.snap +2 -2
- package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +2 -2
- package/lib/components/DrawerMenu/DrawerMenu.tsx +4 -3
- package/lib/components/DropdownMenu/DropdownMenu.tsx +4 -3
- package/lib/components/DropdownUserMenu/DropdownUserMenu.tsx +8 -5
- package/lib/components/Field/CharacterCount.tsx +7 -6
- package/lib/components/Field/ErrorText.tsx +7 -6
- package/lib/components/FileInput/FileInput.tsx +11 -3
- package/lib/components/FileInput/__tests__/__snapshots__/FileInput.test.tsx.snap +4 -4
- package/lib/components/Footer/Footer.tsx +16 -3
- package/lib/components/Footer/FooterColumn.tsx +9 -4
- package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +13 -11
- package/lib/components/FooterNew/BackToTop.tsx +12 -4
- package/lib/components/FooterNew/FooterColumn.tsx +4 -0
- package/lib/components/FooterNew/FooterNavLink.tsx +12 -6
- package/lib/components/FooterNew/LegalAndCopyright.tsx +11 -6
- package/lib/components/FooterNew/LogoAddressAndSocial.tsx +27 -15
- package/lib/components/FooterNew/__tests__/__snapshots__/Footer.test.tsx.snap +32 -26
- package/lib/components/Header/Header.mdx +8 -1
- package/lib/components/Header/Header.stories.tsx +13 -0
- package/lib/components/Header/Header.tsx +3 -0
- package/lib/components/Header/__tests__/Header.test.tsx +22 -0
- package/lib/components/HeaderDraft/HeaderBottomBreadcrumbs.tsx +6 -1
- package/lib/components/HeaderDraft/HeaderBottomHeadingAndAvatar.tsx +19 -8
- package/lib/components/HeaderDraft/HeaderBreadcrumb.tsx +11 -2
- package/lib/components/HeaderDraft/HeaderDraft.tsx +16 -5
- package/lib/components/HeaderDraft/__tests__/__snapshots__/Header.test.tsx.snap +7 -7
- package/lib/components/HeaderNew/Header.tsx +7 -1
- package/lib/components/HeaderNew/HeaderItem.tsx +6 -0
- package/lib/components/HeaderNew/__tests__/Header.test.tsx +41 -0
- package/lib/components/Input/__tests__/__snapshots__/Input.test.tsx.snap +4 -4
- package/lib/components/Menu/MenuContent.tsx +9 -1
- package/lib/components/NativeDatepicker/NativeDatepicker.tsx +6 -2
- package/lib/components/NavigationBar/NavigationBar.stories.tsx +99 -0
- package/lib/components/NavigationBar/NavigationBar.tsx +76 -0
- package/lib/components/NavigationBar/NavigationBarItem.tsx +236 -0
- package/lib/components/NavigationBar/__tests__/NavigationBar.test.tsx +90 -0
- package/lib/components/NavigationBar/index.ts +5 -0
- package/lib/components/Pagination/PaginationInfo.tsx +8 -6
- package/lib/components/Pagination/subcomponents/PaginationCompactControls.tsx +8 -2
- package/lib/components/Pagination/subcomponents/PaginationStandardControls.tsx +2 -1
- package/lib/components/Paragraph/Paragraph.tsx +6 -4
- package/lib/components/Paragraph/__tests__/Paragraph.test.tsx +1 -1
- package/lib/components/Paragraph/__tests__/__snapshots__/Paragraph.test.tsx.snap +9 -9
- package/lib/components/Radio/__tests__/__snapshots__/Radio.test.tsx.snap +2 -2
- package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +2 -2
- package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +1 -1
- package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +2 -2
- package/lib/components/Table/subcomponents/HeadCell/__tests__/__snapshots__/HeadCell.test.tsx.snap +1 -1
- package/lib/components/Textarea/__tests__/__snapshots__/Textarea.test.tsx.snap +4 -4
- package/lib/components/Tooltip/Tooltip.tsx +5 -8
- package/lib/components/Tooltip/__tests__/__snapshots__/tooltip.test.tsx.snap +2 -2
- package/lib/components/index.ts +6 -0
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
<
|
|
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-
|
|
29
|
-
font-
|
|
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
|
+
});
|
|
@@ -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
|
-
<
|
|
64
|
+
<Paragraph
|
|
65
|
+
as='div'
|
|
66
|
+
level='md'
|
|
67
|
+
noMargins
|
|
66
68
|
className={style}
|
|
67
|
-
|
|
69
|
+
testId={testId}
|
|
68
70
|
aria-live='polite'
|
|
69
71
|
aria-label={`Pagination info: ${text}`}
|
|
70
72
|
{...props}
|
|
71
73
|
>
|
|
72
74
|
{text}
|
|
73
|
-
</
|
|
75
|
+
</Paragraph>
|
|
74
76
|
);
|
|
75
77
|
};
|
|
76
78
|
|