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
@@ -9,6 +9,7 @@ const meta = {
9
9
  parameters: { layout: 'padded' },
10
10
  argTypes: {
11
11
  pickedDate: { control: { type: 'date' } },
12
+ pickedMonth: { control: { type: 'date' } },
12
13
  minDate: { control: { type: 'date' } },
13
14
  maxDate: { control: { type: 'date' } },
14
15
  showAcademicWeeks: { control: { type: 'boolean' } },
@@ -37,6 +38,9 @@ export const Default: Story = {
37
38
  args.pickedDate = args.pickedDate
38
39
  ? parseDateFromUNIXTimestamp(args.pickedDate)
39
40
  : null;
41
+ args.pickedMonth = args.pickedMonth
42
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
43
+ : null;
40
44
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
41
45
  return (
42
46
  <Calendar
@@ -79,6 +83,9 @@ export const WithEvents: Story = {
79
83
  args.pickedDate = args.pickedDate
80
84
  ? parseDateFromUNIXTimestamp(args.pickedDate)
81
85
  : null;
86
+ args.pickedMonth = args.pickedMonth
87
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
88
+ : null;
82
89
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
83
90
  return (
84
91
  <Calendar
@@ -158,6 +165,9 @@ export const WithAcademicWeeks: Story = {
158
165
  args.pickedDate = args.pickedDate
159
166
  ? parseDateFromUNIXTimestamp(args.pickedDate)
160
167
  : null;
168
+ args.pickedMonth = args.pickedMonth
169
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
170
+ : null;
161
171
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
162
172
  return (
163
173
  <Calendar
@@ -198,6 +208,9 @@ export const MinMaxDates: Story = {
198
208
  args.pickedDate = args.pickedDate
199
209
  ? parseDateFromUNIXTimestamp(args.pickedDate)
200
210
  : null;
211
+ args.pickedMonth = args.pickedMonth
212
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
213
+ : null;
201
214
  const onDatePick = (date: Date | null) => updateArgs({ pickedDate: date });
202
215
  return (
203
216
  <Calendar
@@ -11,6 +11,7 @@ const DEFAULT_MAX_YEAR = 2100;
11
11
 
12
12
  const Calendar = ({
13
13
  pickedDate = null,
14
+ pickedMonth = null,
14
15
  onDatePick,
15
16
  minDate = null,
16
17
  maxDate = null,
@@ -37,6 +38,11 @@ const Calendar = ({
37
38
  pickedDate = null;
38
39
  }
39
40
 
41
+ if (pickedMonth && isNaN(pickedMonth.getTime())) {
42
+ console.warn('Calendar: pickedMonth is invalid, defaulting to null');
43
+ pickedMonth = null;
44
+ }
45
+
40
46
  const clampDisplayMonth = useCallback(
41
47
  (date: Date) => {
42
48
  const year = date.getFullYear();
@@ -54,36 +60,35 @@ const Calendar = ({
54
60
  [yearRangeEnd, yearRangeStart]
55
61
  );
56
62
 
63
+ // pickedDate takes precedence over pickedMonth
64
+ const activePickedDate = pickedDate ?? pickedMonth;
65
+
57
66
  // Used to track prop value changes
58
- const pickedDateRef = useRef<Date | null>(pickedDate ?? null);
67
+ const activePickedDateRef = useRef<Date | null>(activePickedDate);
59
68
 
60
69
  // Determines the month currently displayed in the calendar
61
70
  const [displayMonth, setDisplayMonth] = useState<Date>(
62
- // Display month initialises as either the picked date's month or the current month
63
- clampDisplayMonth(pickedDate ?? new Date())
71
+ // Display month initialises as the picked date's month, else the picked month, else the current month
72
+ clampDisplayMonth(activePickedDate ?? new Date())
64
73
  );
65
74
 
66
- // Snap displayed month to the picked date's month if it changes
75
+ // Snap displayed month to the active picked date/month if it changes
67
76
  useEffect(() => {
68
77
  if (
69
- // If the picked date is valid
70
- pickedDate &&
71
- !isNaN(pickedDate.getTime()) &&
72
- // If the picked date has changed
73
- pickedDateRef.current?.getTime() !== pickedDate.getTime()
78
+ // If there's an active picked date/month
79
+ activePickedDate &&
80
+ // If it has changed
81
+ activePickedDateRef.current?.getTime() !== activePickedDate.getTime() &&
82
+ // If it's not in the currently displayed month
83
+ (activePickedDate.getMonth() !== displayMonth.getMonth() ||
84
+ activePickedDate.getFullYear() !== displayMonth.getFullYear())
74
85
  ) {
75
- // If the picked date is not in the currently displayed month
76
- if (
77
- pickedDate.getMonth() !== displayMonth.getMonth() ||
78
- pickedDate.getFullYear() !== displayMonth.getFullYear()
79
- ) {
80
- // Update the display month to the picked date's month
81
- setDisplayMonth(clampDisplayMonth(pickedDate));
82
- }
86
+ // Update the display month to match
87
+ setDisplayMonth(clampDisplayMonth(activePickedDate));
83
88
  }
84
- // Update the ref to the new picked date
85
- pickedDateRef.current = pickedDate;
86
- }, [pickedDate, displayMonth, clampDisplayMonth]);
89
+ // Update the ref to the new active picked date/month
90
+ activePickedDateRef.current = activePickedDate;
91
+ }, [activePickedDate, displayMonth, clampDisplayMonth]);
87
92
 
88
93
  useEffect(() => {
89
94
  const clampedDisplayMonth = clampDisplayMonth(displayMonth);
@@ -9,7 +9,8 @@ export type AcademicWeek = {
9
9
  };
10
10
 
11
11
  export interface CalendarProps {
12
- pickedDate?: Date | null;
12
+ pickedDate?: Date | null; // Date to select and display
13
+ pickedMonth?: Date | null; // Date whose month is displayed when pickedDate is not set
13
14
  onDatePick?: (date: Date | null, event?: React.SyntheticEvent) => void;
14
15
  minDate?: string | null; // ISO date string: YYYY-MM-DD format
15
16
  maxDate?: string | null; // ISO date string: YYYY-MM-DD format
@@ -84,6 +84,45 @@ describe('Calendar', () => {
84
84
  );
85
85
  });
86
86
 
87
+ test('Displays the picked month without selecting an individual date', () => {
88
+ customRender(<Calendar pickedMonth={new Date('2025-07-01')} />);
89
+
90
+ expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
91
+ 'July'
92
+ );
93
+ expect(screen.getByRole('combobox', { name: 'Year' })).toHaveTextContent(
94
+ '2025'
95
+ );
96
+ });
97
+
98
+ test('pickedDate takes precedence over pickedMonth for the displayed month', () => {
99
+ customRender(
100
+ <Calendar
101
+ pickedDate={new Date('2025-03-22')}
102
+ pickedMonth={new Date('2025-07-01')}
103
+ />
104
+ );
105
+
106
+ expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
107
+ 'March'
108
+ );
109
+ });
110
+
111
+ test('Updates the displayed month when the pickedMonth prop changes', () => {
112
+ const { rerender } = customRender(
113
+ <Calendar pickedMonth={new Date('2025-07-01')} />
114
+ );
115
+ expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
116
+ 'July'
117
+ );
118
+
119
+ rerender(<Calendar pickedMonth={new Date('2025-09-01')} />);
120
+
121
+ expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
122
+ 'September'
123
+ );
124
+ });
125
+
87
126
  test('Can change the displayed year with the year select', () => {
88
127
  customRender(<Calendar pickedDate={new Date('2025-03-01')} />);
89
128
 
@@ -12,6 +12,7 @@ const meta = {
12
12
  value: { control: { type: 'date' } },
13
13
  minDate: { control: { type: 'date' } },
14
14
  maxDate: { control: { type: 'date' } },
15
+ pickedMonth: { control: { type: 'date' } },
15
16
  disabled: { control: { type: 'boolean' } },
16
17
  clearable: { control: { type: 'boolean' } },
17
18
  showAcademicWeeks: { control: { type: 'boolean' } },
@@ -22,6 +23,13 @@ const meta = {
22
23
  export default meta;
23
24
  type Story = StoryObj<typeof meta>;
24
25
 
26
+ // Convert UNIX timestamp from Storybook controls to `Date` object
27
+ // https://storybook.js.org/docs/essentials/controls#annotation
28
+ const parseDateFromUNIXTimestamp = (timestamp: number) => {
29
+ const date = new Date(timestamp);
30
+ return isNaN(date.getTime()) ? null : date;
31
+ };
32
+
25
33
  const dateToISOString = (date: Date) => {
26
34
  return date.toISOString().split('T')[0];
27
35
  };
@@ -29,9 +37,9 @@ const dateToISOString = (date: Date) => {
29
37
  export const Default: Story = {
30
38
  render: () => {
31
39
  const [args, updateArgs] = useArgs();
32
-
33
- // Storybook controls provide UNIX timestamps for dates, need to convert
34
- // https://storybook.js.org/docs/essentials/controls#annotation
40
+ args.pickedMonth = args.pickedMonth
41
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
42
+ : null;
35
43
  args.value = args.value ? new Date(args.value) : null;
36
44
  args.minDate = args.minDate
37
45
  ? new Date(args.minDate).toLocaleDateString('sv-SE')
@@ -54,9 +62,9 @@ export const Default: Story = {
54
62
  export const InAField: Story = {
55
63
  render: () => {
56
64
  const [args, updateArgs] = useArgs();
57
-
58
- // Storybook controls provide UNIX timestamps for dates, need to convert
59
- // https://storybook.js.org/docs/essentials/controls#annotation
65
+ args.pickedMonth = args.pickedMonth
66
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
67
+ : null;
60
68
  args.value = args.value ? new Date(args.value) : null;
61
69
  args.minDate = args.minDate
62
70
  ? new Date(args.minDate).toLocaleDateString('sv-SE')
@@ -116,9 +124,9 @@ export const WithEvents: Story = {
116
124
  },
117
125
  render: () => {
118
126
  const [args, updateArgs] = useArgs();
119
-
120
- // Storybook controls provide UNIX timestamps for dates, need to convert
121
- // https://storybook.js.org/docs/essentials/controls#annotation
127
+ args.pickedMonth = args.pickedMonth
128
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
129
+ : null;
122
130
  args.value = args.value ? new Date(args.value) : null;
123
131
  args.minDate = args.minDate
124
132
  ? new Date(args.minDate).toLocaleDateString('sv-SE')
@@ -204,9 +212,9 @@ export const WithAcademicWeeks: Story = {
204
212
  },
205
213
  render: () => {
206
214
  const [args, updateArgs] = useArgs();
207
-
208
- // Storybook controls provide UNIX timestamps for dates, need to convert
209
- // https://storybook.js.org/docs/essentials/controls#annotation
215
+ args.pickedMonth = args.pickedMonth
216
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
217
+ : null;
210
218
  args.value = args.value ? new Date(args.value) : null;
211
219
  args.minDate = args.minDate
212
220
  ? new Date(args.minDate).toLocaleDateString('sv-SE')
@@ -253,9 +261,9 @@ export const MinMaxDates: Story = {
253
261
  },
254
262
  render: () => {
255
263
  const [args, updateArgs] = useArgs();
256
-
257
- // Storybook controls provide UNIX timestamps for dates, need to convert
258
- // https://storybook.js.org/docs/essentials/controls#annotation
264
+ args.pickedMonth = args.pickedMonth
265
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
266
+ : null;
259
267
  args.value = args.value ? new Date(args.value) : null;
260
268
  args.minDate = args.minDate
261
269
  ? new Date(args.minDate).toLocaleDateString('sv-SE')
@@ -283,9 +291,9 @@ export const Clearable: Story = {
283
291
  },
284
292
  render: () => {
285
293
  const [args, updateArgs] = useArgs();
286
-
287
- // Storybook controls provide UNIX timestamps for dates, need to convert
288
- // https://storybook.js.org/docs/essentials/controls#annotation
294
+ args.pickedMonth = args.pickedMonth
295
+ ? parseDateFromUNIXTimestamp(args.pickedMonth)
296
+ : null;
289
297
  args.value = args.value ? new Date(args.value) : null;
290
298
  args.minDate = args.minDate
291
299
  ? new Date(args.minDate).toLocaleDateString('sv-SE')
@@ -14,6 +14,7 @@ interface BaseDatepickerProps {
14
14
  maxDate?: string | null; // ISO date string: YYYY-MM-DD
15
15
  minYear?: number;
16
16
  maxYear?: number;
17
+ pickedMonth?: Date | null; // Pre-selects a month to display when no value is set
17
18
  events?: CalendarEvent[];
18
19
  showAcademicWeeks?: boolean;
19
20
  academicWeeks?: AcademicWeek[];
@@ -107,6 +107,55 @@ describe('Datepicker', () => {
107
107
  );
108
108
  });
109
109
 
110
+ test('Does not submit an enclosing form on ENTER key press', async () => {
111
+ const onValueChange = vi.fn();
112
+ const onSubmit = vi.fn((event: React.FormEvent) => event.preventDefault());
113
+ const user = userEvent.setup();
114
+ customRender(
115
+ <form onSubmit={onSubmit}>
116
+ <Datepicker onValueChange={onValueChange} />
117
+ </form>
118
+ );
119
+
120
+ // Internal <input> (the user types into)
121
+ const datepickerInput = screen.getByRole('textbox');
122
+ await user.type(datepickerInput, '10/03/2025');
123
+ expect(datepickerInput).toHaveValue('10/03/2025');
124
+
125
+ // Trigger user-entered date parsing
126
+ await user.keyboard('{Enter}');
127
+
128
+ // The date is still parsed as normal...
129
+ expect(onValueChange).toHaveBeenCalledWith(
130
+ new Date('2025-03-10'),
131
+ expect.anything() // 2nd parameter is the event object (which we're not testing here)
132
+ );
133
+ // ...but the enclosing form is not submitted
134
+ expect(onSubmit).not.toHaveBeenCalled();
135
+ });
136
+
137
+ test('Does not bubble {Enter} key press to a parent element', async () => {
138
+ const onValueChange = vi.fn();
139
+ const parentKeyDown = vi.fn();
140
+ const user = userEvent.setup();
141
+ customRender(
142
+ <div onKeyDown={parentKeyDown}>
143
+ <Datepicker onValueChange={onValueChange} />
144
+ </div>
145
+ );
146
+
147
+ const datepickerInput = screen.getByRole('textbox');
148
+ await user.type(datepickerInput, '10/03/2025');
149
+ // Typing the digits/slashes above also fires keydown events that
150
+ // correctly bubble (only Enter is stopped, to prevent it submitting an
151
+ // enclosing form) - clear those out so only Enter is captured below.
152
+ parentKeyDown.mockClear();
153
+
154
+ await user.keyboard('{Enter}');
155
+
156
+ expect(parentKeyDown).not.toHaveBeenCalled();
157
+ });
158
+
110
159
  test('Input value reflects the date', () => {
111
160
  // 10th March 2025 (2 is March, because month is 0-indexed in Date constructor)
112
161
  customRender(<Datepicker value={new Date(2025, 2, 10)} />);
@@ -153,6 +202,21 @@ describe('Datepicker', () => {
153
202
  expect(panel.parentElement).toHaveClass('ucl-overlay');
154
203
  });
155
204
 
205
+ test('Forwards pickedMonth to the Calendar panel without affecting the input value', () => {
206
+ customRender(<Datepicker pickedMonth={new Date('2026-07-01')} />);
207
+
208
+ const input = screen.getByTestId('ucl-uikit-datepicker__input');
209
+ fireEvent.focus(input);
210
+
211
+ expect(screen.getByRole('combobox', { name: 'Month' })).toHaveTextContent(
212
+ 'July'
213
+ );
214
+ expect(screen.getByRole('combobox', { name: 'Year' })).toHaveTextContent(
215
+ '2026'
216
+ );
217
+ expect(input).toHaveValue('');
218
+ });
219
+
156
220
  test('Panel escapes a containing dialog via fixed positioning', () => {
157
221
  customRender(
158
222
  <dialog open>
@@ -20,6 +20,7 @@ interface CustomDatepickerProps extends React.HTMLAttributes<HTMLDivElement> {
20
20
  maxDate?: string | null; // ISO date string: YYYY-MM-DD
21
21
  minYear?: number;
22
22
  maxYear?: number;
23
+ pickedMonth?: Date | null; // Pre-selects a month to display when no value is set
23
24
  disabled?: boolean;
24
25
  clearable?: boolean;
25
26
  events?: CalendarEvent[];
@@ -40,6 +41,7 @@ const CustomDatepicker = ({
40
41
  maxDate,
41
42
  minYear,
42
43
  maxYear,
44
+ pickedMonth = null,
43
45
  disabled = false,
44
46
  clearable = false,
45
47
  events,
@@ -116,6 +118,11 @@ const CustomDatepicker = ({
116
118
 
117
119
  const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
118
120
  if (event.key === 'Enter') {
121
+ // Stop the native "Enter submits the enclosing form" behaviour.
122
+ event.preventDefault();
123
+ // Key events must not bubble out of the datepicker (e.g. so a parent
124
+ // form's own key handlers don't also see this Enter).
125
+ event.stopPropagation();
119
126
  handleParseInput(event);
120
127
  } else if (event.key === 'Escape') {
121
128
  resetField();
@@ -260,6 +267,7 @@ const CustomDatepicker = ({
260
267
  <Panel>
261
268
  <Calendar
262
269
  pickedDate={value}
270
+ pickedMonth={pickedMonth}
263
271
  onDatePick={handlePickCalendarDate}
264
272
  minDate={minDate}
265
273
  maxDate={maxDate}
@@ -3,6 +3,7 @@ import { Meta, Title, Subtitle, Canvas, Controls } from "@storybook/addon-docs/b
3
3
 
4
4
  export const usage = {
5
5
  default: `<Header title='App Name' />`,
6
+ noBorder: `<Header title='App Name' showBorder={false} />`,
6
7
  homeLink: `<Header title='App Name' homeLinkHref='/' homeLinkAriaLabel='Go to home' />`,
7
8
  menu: `<Header title='App Name' homeLinkHref='/'>
8
9
  <Header.Menu>
@@ -26,6 +27,12 @@ to add a menu button on the right.
26
27
 
27
28
  <Canvas source={{ code: usage.default }} />
28
29
 
30
+ ## Border
31
+
32
+ Set `showBorder={false}` when another component, such as `NavigationBar`, sits directly below the header and should provide the dividing border.
33
+
34
+ <Canvas of={HeaderStories.WithoutBorder} source={{ code: usage.noBorder }} />
35
+
29
36
  ## Home link
30
37
 
31
38
  Add `homeLinkHref` (and optionally `homeLinkAriaLabel`) to make the logo/title act as a home link.
@@ -49,4 +56,4 @@ Use `fixed` to pin the header to the top of the viewport.
49
56
  Full props specification for `<Header>` is below.
50
57
  <Canvas source={{ code: usage.default }} />
51
58
  You can use the controls below to manipulate the `<Header>` component above.
52
- <Controls />
59
+ <Controls />
@@ -13,6 +13,7 @@ const meta = {
13
13
  },
14
14
  argTypes: {
15
15
  title: { control: 'text' },
16
+ showBorder: { control: 'boolean' },
16
17
  fixed: { control: 'boolean' },
17
18
  homeLinkHref: { control: 'text' },
18
19
  homeLinkAriaLabel: { control: 'text' },
@@ -77,6 +78,18 @@ export const WithHomeLink: Story = {
77
78
  },
78
79
  };
79
80
 
81
+ export const WithoutBorder: Story = {
82
+ name: 'Without border',
83
+ render: (args) => (
84
+ <div style={{ overflow: 'hidden' }}>
85
+ <Header {...args} />
86
+ </div>
87
+ ),
88
+ args: {
89
+ showBorder: false,
90
+ },
91
+ };
92
+
80
93
  export const WithMenu: Story = {
81
94
  render: (args) => (
82
95
  <div style={storyWrapperStyle}>
@@ -13,6 +13,7 @@ export const DEFAULT_Z_INDEX = 3;
13
13
 
14
14
  export interface HeaderProps extends HTMLAttributes<HTMLElement> {
15
15
  title?: string;
16
+ showBorder?: boolean;
16
17
  /** @deprecated Only used by the default-theme legacy Header. */
17
18
  fixed?: boolean;
18
19
  titleAs?: React.ElementType<React.HTMLAttributes<HTMLElement>>;
@@ -164,6 +165,7 @@ const LegacyHeader = ({
164
165
 
165
166
  const Header = ({
166
167
  title,
168
+ showBorder = true,
167
169
  titleAs = 'div',
168
170
  titleClassName,
169
171
  titleProps,
@@ -206,6 +208,7 @@ const Header = ({
206
208
  return (
207
209
  <HeaderNew
208
210
  title={title ?? ''}
211
+ showBorder={showBorder}
209
212
  titleAs={titleAs}
210
213
  titleClassName={titleClassName}
211
214
  titleProps={titleProps}
@@ -89,4 +89,26 @@ describe('Header', () => {
89
89
  expect(link).toBeInTheDocument();
90
90
  expect(link.getAttribute('href')).toBe('/');
91
91
  });
92
+
93
+ test('passes showBorder to the new header implementation', () => {
94
+ render(
95
+ <ThemeContextProvider theme='light'>
96
+ <Header
97
+ title='LIDS'
98
+ showBorder={false}
99
+ />
100
+ </ThemeContextProvider>
101
+ );
102
+
103
+ const header = screen.getByTestId('ucl-uikit-header');
104
+ const generatedClass = [...header.classList].find((className) =>
105
+ className.startsWith('css-')
106
+ );
107
+
108
+ expect(generatedClass).toBeDefined();
109
+ expect(header).not.toHaveAttribute('showBorder');
110
+ expect(document.head.textContent).toContain(
111
+ `.${generatedClass}{height:78px;padding:0 48px 0 64px;}`
112
+ );
113
+ });
92
114
  });
@@ -12,6 +12,7 @@ export const NAME = 'ucl-uikit-header';
12
12
 
13
13
  export interface HeaderProps extends HTMLAttributes<HTMLElement> {
14
14
  title: string;
15
+ showBorder?: boolean;
15
16
  titleAs?: React.ElementType<React.HTMLAttributes<HTMLElement>>;
16
17
  titleClassName?: string;
17
18
  titleProps?: Record<string, unknown>;
@@ -22,6 +23,7 @@ export interface HeaderProps extends HTMLAttributes<HTMLElement> {
22
23
 
23
24
  const Header = ({
24
25
  title,
26
+ showBorder = true,
25
27
  titleAs = 'div',
26
28
  titleClassName,
27
29
  titleProps,
@@ -51,7 +53,11 @@ const Header = ({
51
53
  @media (min-width: ${theme.breakpoints.desktop}px) {
52
54
  height: ${HEADER_TABLET_HEIGHT_PX}px;
53
55
  padding: 0 ${theme.padding.p48} 0 ${theme.padding.p64};
54
- border-bottom: 1px solid ${theme.colour.border.subtle};
56
+ ${
57
+ showBorder
58
+ ? `border-bottom: 1px solid ${theme.colour.border.subtle};`
59
+ : ''
60
+ }
55
61
  }
56
62
  `;
57
63
 
@@ -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>