uikit-react-public 0.43.4 → 0.44.2

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 (37) hide show
  1. package/dist/components/Calendar/Calendar.d.ts +1 -1
  2. package/dist/components/Calendar/Calendar.stories.d.ts +6 -1
  3. package/dist/components/Calendar/Calendar.types.d.ts +1 -0
  4. package/dist/components/Datepicker/Datepicker.stories.d.ts +5 -0
  5. package/dist/components/Datepicker/Datepicker.types.d.ts +1 -0
  6. package/dist/components/Datepicker/subcomponents/CustomDatepicker.d.ts +2 -1
  7. package/dist/components/Header/Header.d.ts +2 -1
  8. package/dist/components/Header/Header.stories.d.ts +5 -1
  9. package/dist/components/HeaderNew/Header.d.ts +2 -1
  10. package/dist/components/NavigationBar/NavigationBar.d.ts +12 -0
  11. package/dist/components/NavigationBar/NavigationBar.stories.d.ts +14 -0
  12. package/dist/components/NavigationBar/NavigationBarItem.d.ts +19 -0
  13. package/dist/components/NavigationBar/__tests__/NavigationBar.test.d.ts +1 -0
  14. package/dist/components/NavigationBar/index.d.ts +2 -0
  15. package/dist/components/index.d.ts +2 -0
  16. package/dist/index.js +5645 -5442
  17. package/lib/components/Calendar/Calendar.stories.tsx +13 -0
  18. package/lib/components/Calendar/Calendar.tsx +25 -20
  19. package/lib/components/Calendar/Calendar.types.ts +2 -1
  20. package/lib/components/Calendar/__tests__/Calendar.test.tsx +39 -0
  21. package/lib/components/Datepicker/Datepicker.stories.tsx +26 -18
  22. package/lib/components/Datepicker/Datepicker.types.ts +1 -0
  23. package/lib/components/Datepicker/__tests__/Datepicker.test.tsx +64 -0
  24. package/lib/components/Datepicker/subcomponents/CustomDatepicker.tsx +8 -0
  25. package/lib/components/Header/Header.mdx +8 -1
  26. package/lib/components/Header/Header.stories.tsx +13 -0
  27. package/lib/components/Header/Header.tsx +3 -0
  28. package/lib/components/Header/__tests__/Header.test.tsx +22 -0
  29. package/lib/components/HeaderNew/Header.tsx +7 -1
  30. package/lib/components/HeaderNew/__tests__/Header.test.tsx +41 -0
  31. package/lib/components/NavigationBar/NavigationBar.stories.tsx +99 -0
  32. package/lib/components/NavigationBar/NavigationBar.tsx +76 -0
  33. package/lib/components/NavigationBar/NavigationBarItem.tsx +236 -0
  34. package/lib/components/NavigationBar/__tests__/NavigationBar.test.tsx +90 -0
  35. package/lib/components/NavigationBar/index.ts +5 -0
  36. package/lib/components/index.ts +6 -0
  37. package/package.json +1 -1
@@ -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';
@@ -148,6 +148,12 @@ export type { HeaderProps } from './Header';
148
148
  export { default as HeaderNew } from './HeaderNew';
149
149
  export type { HeaderProps as HeaderNewProps } from './HeaderNew';
150
150
 
151
+ export { default as NavigationBar } from './NavigationBar';
152
+ export type {
153
+ NavigationBarItemProps,
154
+ NavigationBarProps,
155
+ } from './NavigationBar';
156
+
151
157
  export { default as HeaderDraft } from './HeaderDraft';
152
158
  export type { HeaderDraftProps } from './HeaderDraft';
153
159
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.43.4",
5
+ "version": "0.44.2",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",