uikit-react-public 0.25.3 → 0.25.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import React, {
2
2
  createContext,
3
3
  useContext,
4
+ useId,
4
5
  useMemo,
5
6
  useRef,
6
7
  useState,
@@ -12,6 +13,7 @@ interface DropdownContextType {
12
13
  toggle: () => void;
13
14
  triggerRef: React.RefObject<HTMLElement | null>;
14
15
  contentRef: React.RefObject<HTMLDivElement | null>;
16
+ contentId: string;
15
17
  }
16
18
 
17
19
  const DropdownContext = createContext<DropdownContextType | undefined>(
@@ -28,6 +30,10 @@ export const useDropdownContext = () => {
28
30
  return context;
29
31
  };
30
32
 
33
+ export const useDropdownContextOptional = () => {
34
+ return useContext(DropdownContext);
35
+ };
36
+
31
37
  interface DropdownProviderProps {
32
38
  defaultOpen?: boolean;
33
39
  children: React.ReactNode;
@@ -40,6 +46,7 @@ const DropdownProvider = ({
40
46
  const [isOpen, setIsOpen] = useState(defaultOpen);
41
47
  const triggerRef = useRef<HTMLElement>(null);
42
48
  const contentRef = useRef<HTMLDivElement>(null);
49
+ const contentId = useId();
43
50
 
44
51
  const value = useMemo(
45
52
  () => ({
@@ -48,8 +55,9 @@ const DropdownProvider = ({
48
55
  toggle: () => setIsOpen((prev) => !prev),
49
56
  triggerRef,
50
57
  contentRef,
58
+ contentId,
51
59
  }),
52
- [isOpen]
60
+ [contentId, isOpen]
53
61
  );
54
62
 
55
63
  return (
@@ -1,4 +1,4 @@
1
- import { HTMLAttributes, useEffect } from 'react';
1
+ import { HTMLAttributes, useEffect, useRef } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
3
  import { useTheme } from '../../theme';
4
4
  import DropdownProvider, { useDropdownContext } from './Dropdown.context';
@@ -19,7 +19,8 @@ const DropdownBody = ({
19
19
  ...props
20
20
  }: Omit<DropdownProps, 'defaultOpen'>) => {
21
21
  const [theme] = useTheme();
22
- const { setIsOpen, triggerRef, contentRef } = useDropdownContext();
22
+ const { isOpen, setIsOpen, triggerRef, contentRef } = useDropdownContext();
23
+ const wasOpenRef = useRef(isOpen);
23
24
 
24
25
  useEffect(() => {
25
26
  const handleDocumentClick = (event: MouseEvent) => {
@@ -50,6 +51,13 @@ const DropdownBody = ({
50
51
  };
51
52
  }, [setIsOpen, triggerRef, contentRef]);
52
53
 
54
+ useEffect(() => {
55
+ if (wasOpenRef.current && !isOpen) {
56
+ triggerRef.current?.focus();
57
+ }
58
+ wasOpenRef.current = isOpen;
59
+ }, [isOpen, triggerRef]);
60
+
53
61
  const baseStyle = css`
54
62
  position: relative;
55
63
  display: inline-flex;
@@ -1,6 +1,8 @@
1
- import { HTMLAttributes, memo } from 'react';
1
+ import { HTMLAttributes, memo, useEffect, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import { css, cx } from '@emotion/css';
3
4
  import { useTheme } from '../../theme';
5
+ import useMediaQuery from '../../hooks/useMediaQuery';
4
6
  import { useDropdownContext } from './Dropdown.context';
5
7
 
6
8
  export const NAME = 'ucl-uikit-dropdown__content';
@@ -10,35 +12,167 @@ export interface DropdownContentProps extends HTMLAttributes<HTMLDivElement> {
10
12
  align?: 'left' | 'right';
11
13
  }
12
14
 
15
+ const TABLET_BOTTOM_GAP_PX = 32;
16
+
13
17
  const DropdownContent = ({
14
18
  testId = NAME,
15
19
  align = 'right',
16
20
  className,
17
21
  children,
22
+ id,
18
23
  ...props
19
24
  }: DropdownContentProps) => {
20
25
  const [theme] = useTheme();
21
- const { isOpen, contentRef } = useDropdownContext();
26
+ const { isOpen, contentRef, contentId } = useDropdownContext();
27
+ const [tabletMaxHeight, setTabletMaxHeight] = useState<number | undefined>(
28
+ undefined
29
+ );
30
+ const isTabletAndUp = useMediaQuery(
31
+ `(min-width: ${theme.breakpoints.tablet}px)`
32
+ );
33
+
34
+ useEffect(() => {
35
+ if (typeof document === 'undefined') return;
36
+ if (isTabletAndUp || !isOpen) return;
37
+
38
+ const container = contentRef.current;
39
+ if (!container) return;
40
+
41
+ const { body } = document;
42
+ const previousOverflow = body.style.overflow;
43
+ const backgroundElements = Array.from(body.children).filter(
44
+ (element) => element !== container
45
+ ) as HTMLElement[];
46
+
47
+ const previousBackgroundState = backgroundElements.map((element) => ({
48
+ element,
49
+ ariaHidden: element.getAttribute('aria-hidden'),
50
+ inert: (element as HTMLElement & { inert?: boolean }).inert,
51
+ }));
52
+
53
+ body.style.overflow = 'hidden';
54
+ previousBackgroundState.forEach(({ element }) => {
55
+ element.setAttribute('aria-hidden', 'true');
56
+ (element as HTMLElement & { inert?: boolean }).inert = true;
57
+ });
58
+
59
+ const getFocusableElements = () =>
60
+ Array.from(
61
+ container.querySelectorAll<HTMLElement>(
62
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
63
+ )
64
+ ).filter((element) => !element.hasAttribute('disabled'));
65
+
66
+ const focusableElements = getFocusableElements();
67
+ if (focusableElements.length > 0) {
68
+ focusableElements[0].focus();
69
+ } else {
70
+ container.focus();
71
+ }
72
+
73
+ const handleKeyDown = (event: KeyboardEvent) => {
74
+ if (event.key !== 'Tab') return;
75
+
76
+ const elements = getFocusableElements();
77
+ if (elements.length === 0) {
78
+ event.preventDefault();
79
+ container.focus();
80
+ return;
81
+ }
82
+
83
+ const first = elements[0];
84
+ const last = elements[elements.length - 1];
85
+ const active = document.activeElement as HTMLElement | null;
86
+
87
+ if (event.shiftKey && active === first) {
88
+ event.preventDefault();
89
+ last.focus();
90
+ } else if (!event.shiftKey && active === last) {
91
+ event.preventDefault();
92
+ first.focus();
93
+ }
94
+ };
95
+
96
+ document.addEventListener('keydown', handleKeyDown);
97
+
98
+ return () => {
99
+ body.style.overflow = previousOverflow;
100
+ previousBackgroundState.forEach(({ element, ariaHidden, inert }) => {
101
+ if (ariaHidden === null) {
102
+ element.removeAttribute('aria-hidden');
103
+ } else {
104
+ element.setAttribute('aria-hidden', ariaHidden);
105
+ }
106
+ (element as HTMLElement & { inert?: boolean }).inert = inert;
107
+ });
108
+ document.removeEventListener('keydown', handleKeyDown);
109
+ };
110
+ }, [contentRef, isOpen, isTabletAndUp]);
111
+
112
+ useEffect(() => {
113
+ if (typeof window === 'undefined') return;
114
+ if (!isTabletAndUp || !isOpen) {
115
+ setTabletMaxHeight(undefined);
116
+ return;
117
+ }
118
+
119
+ const updateMaxHeight = () => {
120
+ const top = contentRef.current?.getBoundingClientRect().top ?? 0;
121
+ setTabletMaxHeight(
122
+ Math.max(window.innerHeight - top - TABLET_BOTTOM_GAP_PX, 0)
123
+ );
124
+ };
125
+
126
+ updateMaxHeight();
127
+ window.addEventListener('resize', updateMaxHeight);
128
+
129
+ return () => {
130
+ window.removeEventListener('resize', updateMaxHeight);
131
+ };
132
+ }, [contentRef, isOpen, isTabletAndUp]);
22
133
 
23
134
  const baseStyle = css`
24
135
  display: none;
25
- position: absolute;
26
- top: calc(100% + ${theme.margin.m8});
27
- z-index: 100;
28
- min-width: max-content;
136
+ z-index: 9999;
137
+ position: fixed;
138
+ inset: 0;
139
+ width: 100vw;
140
+ max-width: 100vw;
141
+ height: 100dvh;
142
+ box-sizing: border-box;
143
+ overflow-y: auto;
144
+ overflow-x: hidden;
29
145
  background-color: ${theme.colour.surface.default};
30
146
  color: ${theme.colour.text.default};
31
- border: ${theme.border.b1} solid ${theme.colour.border.default};
32
- box-shadow: ${theme.boxShadow.y1};
33
- border-radius: ${theme.radius.r4};
147
+
148
+ @media (min-width: ${theme.breakpoints.tablet}px) {
149
+ z-index: 100;
150
+ position: absolute;
151
+ inset: auto;
152
+ top: calc(100% + ${theme.margin.m8});
153
+ width: auto;
154
+ max-width: none;
155
+ height: auto;
156
+ overflow-y: auto;
157
+ overflow-x: visible;
158
+ background-color: ${theme.colour.surface.default};
159
+ color: ${theme.colour.text.default};
160
+ border: ${theme.border.b1} solid ${theme.colour.border.default};
161
+ box-shadow: ${theme.boxShadow.y1};
162
+ border-radius: ${theme.radius.r4};
163
+ }
34
164
  `;
35
165
 
36
166
  const alignLeftStyle = css`
37
- left: 0;
167
+ @media (min-width: ${theme.breakpoints.tablet}px) {
168
+ left: 0;
169
+ }
38
170
  `;
39
171
 
40
172
  const alignRightStyle = css`
41
- right: 0;
173
+ @media (min-width: ${theme.breakpoints.tablet}px) {
174
+ right: 0;
175
+ }
42
176
  `;
43
177
 
44
178
  const openStyle = css`
@@ -50,21 +184,47 @@ const DropdownContent = ({
50
184
  baseStyle,
51
185
  align === 'left' && alignLeftStyle,
52
186
  align === 'right' && alignRightStyle,
53
- isOpen && openStyle,
54
- className
187
+ isOpen && openStyle
55
188
  );
56
189
 
57
- return (
190
+ const panelStyle = css`
191
+ box-sizing: border-box;
192
+ width: 100%;
193
+ max-width: 100%;
194
+ min-height: 100%;
195
+
196
+ @media (min-width: ${theme.breakpoints.tablet}px) {
197
+ width: auto;
198
+ max-width: none;
199
+ min-width: max-content;
200
+ min-height: auto;
201
+ overflow-y: auto;
202
+ background: transparent;
203
+ color: inherit;
204
+ }
205
+ `;
206
+
207
+ const content = (
58
208
  <div
59
209
  ref={contentRef}
60
210
  className={style}
61
- role='menu'
62
- data-testid={testId}
63
- {...props}
211
+ style={isTabletAndUp ? { maxHeight: tabletMaxHeight } : undefined}
212
+ tabIndex={isTabletAndUp ? undefined : -1}
213
+ role={isTabletAndUp ? undefined : 'dialog'}
214
+ aria-modal={isTabletAndUp ? undefined : true}
64
215
  >
65
- {children}
216
+ <div
217
+ className={cx(panelStyle, className)}
218
+ id={id ?? contentId}
219
+ data-testid={testId}
220
+ {...props}
221
+ >
222
+ {children}
223
+ </div>
66
224
  </div>
67
225
  );
226
+
227
+ return !isTabletAndUp ? createPortal(content, document.body) : content;
68
228
  };
69
229
 
70
230
  export default memo(DropdownContent);
@@ -15,7 +15,7 @@ export interface DropdownTriggerProps {
15
15
  }
16
16
 
17
17
  const DropdownTrigger = ({ children }: DropdownTriggerProps) => {
18
- const { isOpen, toggle, triggerRef } = useDropdownContext();
18
+ const { isOpen, toggle, triggerRef, contentId } = useDropdownContext();
19
19
 
20
20
  if (!isValidElement(children)) {
21
21
  return null;
@@ -54,8 +54,9 @@ const DropdownTrigger = ({ children }: DropdownTriggerProps) => {
54
54
  }
55
55
  },
56
56
  onKeyDown: handleKeyDown,
57
- 'aria-haspopup': childProps['aria-haspopup'] ?? 'menu',
57
+ 'aria-haspopup': childProps['aria-haspopup'],
58
58
  'aria-expanded': isOpen,
59
+ 'aria-controls': childProps['aria-controls'] ?? contentId,
59
60
  });
60
61
  };
61
62
 
@@ -57,7 +57,7 @@ describe('Dropdown', () => {
57
57
  fireEvent.click(screen.getByRole('button', { name: 'Open dropdown' }));
58
58
  expect(content).toBeVisible();
59
59
 
60
- fireEvent.mouseDown(screen.getByRole('button', { name: 'Outside' }));
60
+ fireEvent.mouseDown(screen.getByText('Outside'));
61
61
  expect(content).not.toBeVisible();
62
62
  });
63
63
  });
@@ -4,14 +4,24 @@ import Dropdown, { DropdownProps } from '../Dropdown';
4
4
  import useTheme from '../../theme/useTheme';
5
5
  import useMediaQuery from '../../hooks/useMediaQuery';
6
6
  import DropdownMenuTrigger from './DropdownMenuTrigger';
7
+ import { useDropdownContext } from '../Dropdown/Dropdown.context';
7
8
 
8
9
  export const NAME = 'ucl-uikit-dropdown-menu';
9
10
 
11
+ export interface DropdownMenuChildrenApi {
12
+ close: () => void;
13
+ }
14
+
15
+ type DropdownMenuChildren =
16
+ | ReactNode
17
+ | ((api: DropdownMenuChildrenApi) => ReactNode);
18
+
10
19
  export interface DropdownMenuProps
11
20
  extends
12
21
  Omit<DropdownProps, 'children'>,
13
22
  Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
14
- children: ReactNode;
23
+ align?: 'left' | 'right';
24
+ children: DropdownMenuChildren;
15
25
  label?: string;
16
26
  triggerAriaLabelCollapsed?: string;
17
27
  triggerAriaLabelExpanded?: string;
@@ -20,7 +30,22 @@ export interface DropdownMenuProps
20
30
  contentClassName?: string;
21
31
  }
22
32
 
33
+ const DropdownMenuContent = ({
34
+ children,
35
+ }: {
36
+ children: DropdownMenuChildren;
37
+ }) => {
38
+ const { setIsOpen } = useDropdownContext();
39
+
40
+ if (typeof children === 'function') {
41
+ return children({ close: () => setIsOpen(false) });
42
+ }
43
+
44
+ return children;
45
+ };
46
+
23
47
  const DropdownMenu = ({
48
+ align = 'right',
24
49
  children,
25
50
  label = 'MENU',
26
51
  triggerAriaLabelCollapsed = 'Open menu',
@@ -50,6 +75,16 @@ const DropdownMenu = ({
50
75
  triggerClassName
51
76
  );
52
77
 
78
+ const contentBaseStyle = css`
79
+ background-color: ${theme.colour.surface.primary};
80
+
81
+ @media (min-width: ${theme.breakpoints.tablet}px) {
82
+ width: 460px;
83
+ }
84
+ `;
85
+
86
+ const contentStyle = cx(contentBaseStyle, contentClassName);
87
+
53
88
  return (
54
89
  <Dropdown
55
90
  className={dropdownStyle}
@@ -64,8 +99,11 @@ const DropdownMenu = ({
64
99
  className={resolvedTriggerClassName}
65
100
  />
66
101
  </Dropdown.Trigger>
67
- <Dropdown.Content className={contentClassName}>
68
- {children}
102
+ <Dropdown.Content
103
+ className={contentStyle}
104
+ align={align}
105
+ >
106
+ <DropdownMenuContent>{children}</DropdownMenuContent>
69
107
  </Dropdown.Content>
70
108
  </Dropdown>
71
109
  );
@@ -1,6 +1,8 @@
1
1
  import { beforeEach, describe, expect, test, vi } from 'vitest';
2
2
  import { fireEvent, render, screen } from '@testing-library/react';
3
3
  import DropdownMenu from '../DropdownMenu';
4
+ import Button from '../../Button';
5
+ import Menu from '../../MenuNew';
4
6
  import { ThemeContextProvider } from '../../../theme/useTheme';
5
7
 
6
8
  const mockMatchMedia = (matchesDesktop: boolean) => {
@@ -150,4 +152,51 @@ describe('DropdownMenu', () => {
150
152
  screen.getByRole('button', { name: 'Hide menu options' })
151
153
  ).toBeInTheDocument();
152
154
  });
155
+
156
+ test('supports render-prop children with close helper', () => {
157
+ render(
158
+ <ThemeContextProvider>
159
+ <DropdownMenu>
160
+ {({ close }) => (
161
+ <Button
162
+ aria-label='Close from content'
163
+ onClick={close}
164
+ >
165
+ Close
166
+ </Button>
167
+ )}
168
+ </DropdownMenu>
169
+ </ThemeContextProvider>
170
+ );
171
+
172
+ const trigger = screen.getByRole('button', { name: 'Open menu' });
173
+ const content = screen.getByTestId('ucl-uikit-dropdown__content');
174
+
175
+ fireEvent.click(trigger);
176
+ expect(content).toBeVisible();
177
+
178
+ fireEvent.click(screen.getByRole('button', { name: 'Close from content' }));
179
+ expect(content).not.toBeVisible();
180
+ });
181
+
182
+ test('menu head close button closes dropdown without render-prop usage', () => {
183
+ render(
184
+ <ThemeContextProvider>
185
+ <DropdownMenu>
186
+ <Menu>
187
+ <Menu.Head />
188
+ </Menu>
189
+ </DropdownMenu>
190
+ </ThemeContextProvider>
191
+ );
192
+
193
+ const trigger = screen.getByRole('button', { name: 'Open menu' });
194
+ const content = screen.getByTestId('ucl-uikit-dropdown__content');
195
+
196
+ fireEvent.click(trigger);
197
+ expect(content).toBeVisible();
198
+
199
+ fireEvent.click(screen.getByTestId('ucl-uikit-menu__head__close-button'));
200
+ expect(content).not.toBeVisible();
201
+ });
153
202
  });
@@ -2,6 +2,7 @@ import { HTMLAttributes, NamedExoticComponent, memo } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
3
  import MenuBaseItem, { MenuBaseItemProps } from './MenuBaseItem';
4
4
  import MenuDivider, { MenuDividerProps } from './MenuDivider';
5
+ import MenuHead, { MenuHeadProps } from './MenuHead';
5
6
  import MenuHeading, { MenuHeadingProps } from './MenuHeading';
6
7
  import MenuSection, { MenuSectionProps } from './MenuSection';
7
8
  import MenuAccordion, {
@@ -22,6 +23,7 @@ export interface MenuProps extends HTMLAttributes<HTMLDivElement> {
22
23
 
23
24
  export type {
24
25
  MenuDividerProps,
26
+ MenuHeadProps,
25
27
  MenuHeadingProps,
26
28
  MenuSectionProps,
27
29
  MenuAccordionProps,
@@ -41,11 +43,12 @@ const Menu = ({
41
43
  }: MenuProps) => {
42
44
  const [theme] = useTheme();
43
45
  const baseStyle = css`
44
- padding: ${theme.padding.p16} ${theme.padding.p24};
46
+ width: 100%;
47
+ box-sizing: border-box;
48
+ padding: ${theme.padding.p16} ${theme.padding.p16};
45
49
  background-color: ${theme.colour.surface.primary};
46
50
 
47
51
  @media screen and (min-width: ${theme.breakpoints.tablet}px) {
48
- width: 360px;
49
52
  padding: ${theme.padding.p48} ${theme.padding.p64};
50
53
  }
51
54
  `;
@@ -58,6 +61,7 @@ const Menu = ({
58
61
  <nav
59
62
  className={style}
60
63
  data-testid={testId}
64
+ role='menu'
61
65
  {...props}
62
66
  >
63
67
  {children}
@@ -67,6 +71,7 @@ const Menu = ({
67
71
 
68
72
  interface MenuComponent extends NamedExoticComponent<MenuProps> {
69
73
  Section: typeof MenuSection;
74
+ Head: typeof MenuHead;
70
75
  Heading: typeof MenuHeading;
71
76
  Accordion: typeof MenuAccordion;
72
77
  Divider: typeof MenuDivider;
@@ -78,6 +83,7 @@ interface MenuComponent extends NamedExoticComponent<MenuProps> {
78
83
 
79
84
  const MemoMenu: MenuComponent = Object.assign(memo(Menu), {
80
85
  Section: MenuSection,
86
+ Head: MenuHead,
81
87
  Heading: MenuHeading,
82
88
  Accordion: MenuAccordion,
83
89
  Divider: MenuDivider,
@@ -38,6 +38,7 @@ const MenuAccordion = ({
38
38
  const baseStyle = css`
39
39
  width: 100%;
40
40
  color: ${theme.colour.text.default};
41
+ padding: 0 ${theme.padding.p16};
41
42
 
42
43
  @media screen and (min-width: ${theme.breakpoints.tablet}px) {
43
44
  padding: 0 ${theme.padding.p16};
@@ -0,0 +1,76 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import { useTheme } from '../../theme';
4
+ import { UclLogoNew } from '../UclLogoNew';
5
+ import IconButton from '../IconButton';
6
+ import Icon from '../Icon';
7
+ import { useDropdownContextOptional } from '../Dropdown/Dropdown.context';
8
+
9
+ export const NAME = 'ucl-uikit-menu__head';
10
+
11
+ export interface MenuHeadProps extends HTMLAttributes<HTMLDivElement> {
12
+ testId?: string;
13
+ onClose?: () => void;
14
+ closeButtonAriaLabel?: string;
15
+ }
16
+
17
+ const MenuHead = ({
18
+ testId = NAME,
19
+ onClose,
20
+ closeButtonAriaLabel = 'Close menu',
21
+ className,
22
+ ...props
23
+ }: MenuHeadProps) => {
24
+ const [theme] = useTheme();
25
+ const dropdownContext = useDropdownContextOptional();
26
+
27
+ const baseStyle = css`
28
+ box-sizing: border-box;
29
+ margin: 0 0 48px;
30
+ padding: 0 ${theme.padding.p8};
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: space-between;
34
+
35
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
36
+ display: none;
37
+ padding: 0 ${theme.padding.p16};
38
+ }
39
+ `;
40
+
41
+ const style = cx(NAME, baseStyle, className);
42
+
43
+ const closeButtonStyle = css`
44
+ width: 32px;
45
+ height: 32px;
46
+ display: inline-flex;
47
+ align-items: center;
48
+ justify-content: center;
49
+ `;
50
+
51
+ return (
52
+ <div
53
+ className={style}
54
+ data-testid={testId}
55
+ {...props}
56
+ >
57
+ <UclLogoNew
58
+ testId={`${testId}__logo`}
59
+ height={30}
60
+ />
61
+ <IconButton
62
+ testId={`${testId}__close-button`}
63
+ aria-label={closeButtonAriaLabel}
64
+ onClick={() => {
65
+ onClose?.();
66
+ dropdownContext?.setIsOpen(false);
67
+ }}
68
+ className={closeButtonStyle}
69
+ >
70
+ <Icon.X size={32} />
71
+ </IconButton>
72
+ </div>
73
+ );
74
+ };
75
+
76
+ export default MenuHead;
@@ -20,6 +20,7 @@ const MenuHeading = ({
20
20
  const baseStyle = css`
21
21
  box-sizing: border-box;
22
22
  margin: ${theme.margin.m32} 0 ${theme.margin.m16};
23
+ padding: 0 ${theme.padding.p16};
23
24
  text-transform: uppercase;
24
25
 
25
26
  @media screen and (min-width: ${theme.breakpoints.tablet}px) {
@@ -26,6 +26,7 @@ const PrimaryMenuItem = ({
26
26
 
27
27
  const baseStyle = css`
28
28
  color: ${theme.colour.text.default};
29
+ padding: 0 ${theme.padding.p16};
29
30
 
30
31
  @media screen and (min-width: ${theme.breakpoints.tablet}px) {
31
32
  padding: 0 ${theme.padding.p16};
@@ -25,8 +25,12 @@ const SecondaryMenuItem = ({
25
25
  const [theme] = useTheme();
26
26
 
27
27
  const baseStyle = css`
28
- padding-inline: ${theme.margin.m16};
28
+ padding: 0 ${theme.padding.p16};
29
29
  color: ${theme.colour.text.secondary};
30
+
31
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
32
+ padding: 0 ${theme.padding.p16};
33
+ }
30
34
  `;
31
35
 
32
36
  const style = cx(NAME, baseStyle, className);
@@ -26,6 +26,31 @@ describe('Menu', () => {
26
26
  expect(heading).not.toHaveAttribute('role', 'button');
27
27
  });
28
28
 
29
+ test('renders head item', () => {
30
+ const onClose = vi.fn();
31
+
32
+ wrap(
33
+ <Menu>
34
+ <Menu.Head onClose={onClose} />
35
+ </Menu>
36
+ );
37
+
38
+ const head = screen.getByTestId('ucl-uikit-menu__head');
39
+ const logo = screen.getByTestId('ucl-uikit-menu__head__logo');
40
+ const closeButton = screen.getByTestId(
41
+ 'ucl-uikit-menu__head__close-button'
42
+ );
43
+
44
+ expect(head).toBeInTheDocument();
45
+ expect(logo).toBeInTheDocument();
46
+ expect(closeButton).toBeInTheDocument();
47
+ expect(head).toHaveClass('ucl-uikit-menu__head');
48
+ expect(head).not.toHaveAttribute('role', 'button');
49
+
50
+ fireEvent.click(closeButton);
51
+ expect(onClose).toHaveBeenCalledTimes(1);
52
+ });
53
+
29
54
  test('renders semantic section wrapper', () => {
30
55
  wrap(
31
56
  <Menu>