uikit-react-public 0.25.1 → 0.25.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/DropdownMenu/DropdownMenu.d.ts +3 -2
  2. package/dist/components/DropdownMenu/DropdownMenuTrigger.d.ts +15 -0
  3. package/dist/components/MenuNew/ActionMenuButton.d.ts +6 -0
  4. package/dist/components/MenuNew/Menu.d.ts +9 -3
  5. package/dist/components/MenuNew/MenuAccordion/MenuAccordion.d.ts +15 -0
  6. package/dist/components/MenuNew/MenuAccordion/MenuAccordionChevron.d.ts +6 -0
  7. package/dist/components/MenuNew/MenuAccordion/MenuAccordionItem.d.ts +8 -0
  8. package/dist/components/MenuNew/MenuAccordion/index.d.ts +2 -0
  9. package/dist/components/MenuNew/MenuBaseItem.d.ts +4 -1
  10. package/dist/components/MenuNew/MenuHeading.d.ts +7 -0
  11. package/dist/components/MenuNew/MenuSection.d.ts +7 -0
  12. package/dist/components/MenuNew/index.d.ts +1 -1
  13. package/dist/index.js +5376 -5137
  14. package/lib/components/Button/Button.tsx +4 -1
  15. package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +9 -9
  16. package/lib/components/DropdownMenu/DropdownMenu.tsx +12 -24
  17. package/lib/components/DropdownMenu/DropdownMenuTrigger.tsx +74 -0
  18. package/lib/components/DropdownMenu/__tests__/DropdownMenu.test.tsx +58 -1
  19. package/lib/components/MenuNew/ActionMenuButton.tsx +39 -0
  20. package/lib/components/MenuNew/Menu.tsx +22 -4
  21. package/lib/components/MenuNew/MenuAccordion/MenuAccordion.tsx +104 -0
  22. package/lib/components/MenuNew/MenuAccordion/MenuAccordionChevron.tsx +30 -0
  23. package/lib/components/MenuNew/MenuAccordion/MenuAccordionItem.tsx +47 -0
  24. package/lib/components/MenuNew/MenuAccordion/index.ts +5 -0
  25. package/lib/components/MenuNew/MenuBaseItem.tsx +36 -6
  26. package/lib/components/MenuNew/MenuDivider.tsx +3 -1
  27. package/lib/components/MenuNew/MenuHeading.tsx +56 -0
  28. package/lib/components/MenuNew/MenuItemText.tsx +6 -0
  29. package/lib/components/MenuNew/MenuSection.tsx +36 -0
  30. package/lib/components/MenuNew/PrimaryMenuItem.tsx +6 -1
  31. package/lib/components/MenuNew/SecondaryMenuItem.tsx +2 -0
  32. package/lib/components/MenuNew/__tests__/Menu.test.tsx +129 -6
  33. package/lib/components/MenuNew/index.ts +4 -1
  34. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +1 -1
  35. package/package.json +1 -1
  36. package/dist/components/MenuNew/ActionMenuItem.d.ts +0 -7
  37. package/lib/components/MenuNew/ActionMenuItem.tsx +0 -31
@@ -140,7 +140,6 @@ const Button = <C extends ElementType = 'button'>({
140
140
  border-radius: ${theme.radius.r4};
141
141
  font-family: ${theme.typography.body.md.fontFamily};
142
142
  font-feature-settings: ${theme.typography.body.md.fontSettings};
143
- font-size: 18px;
144
143
  font-weight: 500;
145
144
  -webkit-font-smoothing: antialiased;
146
145
  -moz-osx-font-smoothing: grayscale;
@@ -165,21 +164,25 @@ const Button = <C extends ElementType = 'button'>({
165
164
  const sizeExtraSmallStyle = css`
166
165
  height: 32px;
167
166
  padding: 0 ${paddingPx}px;
167
+ font-size: 14px;
168
168
  `;
169
169
 
170
170
  const sizeSmallStyle = css`
171
171
  height: 40px;
172
172
  padding: 0 ${paddingPx}px;
173
+ font-size: 16px;
173
174
  `;
174
175
 
175
176
  const sizeMediumStyle = css`
176
177
  height: 48px;
177
178
  padding: 0 ${paddingPx}px;
179
+ font-size: 16px;
178
180
  `;
179
181
 
180
182
  const sizeLargeStyle = css`
181
183
  height: 56px;
182
184
  padding: 0 ${paddingPx}px;
185
+ font-size: 18px;
183
186
  `;
184
187
 
185
188
  const fullWidthStyle = css`
@@ -3,7 +3,7 @@
3
3
  exports[`Button > maps legacy destructive prop to destructive variants 1`] = `
4
4
  <button
5
5
  aria-disabled="false"
6
- class="ucl-uikit-button css-yunmm0"
6
+ class="ucl-uikit-button css-ay3o9w"
7
7
  data-testid="ucl-uikit-button"
8
8
  >
9
9
  Delete
@@ -13,7 +13,7 @@ exports[`Button > maps legacy destructive prop to destructive variants 1`] = `
13
13
  exports[`Button > maps legacy size default to medium 1`] = `
14
14
  <button
15
15
  aria-disabled="false"
16
- class="ucl-uikit-button css-oqr4zz"
16
+ class="ucl-uikit-button css-19pu3x7"
17
17
  data-testid="ucl-uikit-button"
18
18
  >
19
19
  Legacy default size
@@ -23,7 +23,7 @@ exports[`Button > maps legacy size default to medium 1`] = `
23
23
  exports[`Button > snapshot: 'as' prop with 'a' 1`] = `
24
24
  <a
25
25
  aria-disabled="false"
26
- class="ucl-uikit-button css-oqr4zz"
26
+ class="ucl-uikit-button css-19pu3x7"
27
27
  data-testid="ucl-uikit-button"
28
28
  href="https://ucl.ac.uk"
29
29
  >
@@ -34,7 +34,7 @@ exports[`Button > snapshot: 'as' prop with 'a' 1`] = `
34
34
  exports[`Button > snapshot: loading 1`] = `
35
35
  <button
36
36
  aria-disabled="false"
37
- class="ucl-uikit-button css-1sgvpd0"
37
+ class="ucl-uikit-button css-16925p4"
38
38
  data-testid="ucl-uikit-button"
39
39
  disabled=""
40
40
  >
@@ -67,7 +67,7 @@ exports[`Button > snapshot: loading 1`] = `
67
67
  exports[`Button > snapshot: no props 1`] = `
68
68
  <button
69
69
  aria-disabled="false"
70
- class="ucl-uikit-button css-12at99t"
70
+ class="ucl-uikit-button css-12mlzyd"
71
71
  data-testid="ucl-uikit-button"
72
72
  />
73
73
  `;
@@ -75,7 +75,7 @@ exports[`Button > snapshot: no props 1`] = `
75
75
  exports[`Button > snapshot: testId prop 1`] = `
76
76
  <button
77
77
  aria-disabled="false"
78
- class="ucl-uikit-button css-12at99t"
78
+ class="ucl-uikit-button css-12mlzyd"
79
79
  data-testid="test123"
80
80
  />
81
81
  `;
@@ -83,7 +83,7 @@ exports[`Button > snapshot: testId prop 1`] = `
83
83
  exports[`Button > snapshot: variant prop 1`] = `
84
84
  <button
85
85
  aria-disabled="false"
86
- class="ucl-uikit-button css-9dplwk"
86
+ class="ucl-uikit-button css-vpw5wr"
87
87
  data-testid="ucl-uikit-button"
88
88
  />
89
89
  `;
@@ -91,7 +91,7 @@ exports[`Button > snapshot: variant prop 1`] = `
91
91
  exports[`Button > snapshot: with icon 1`] = `
92
92
  <button
93
93
  aria-disabled="false"
94
- class="ucl-uikit-button css-oqr4zz"
94
+ class="ucl-uikit-button css-19pu3x7"
95
95
  data-testid="ucl-uikit-button"
96
96
  >
97
97
  <svg
@@ -119,7 +119,7 @@ exports[`Button > snapshot: with icon 1`] = `
119
119
  exports[`Button > snapshot: with icon and iconPosition right 1`] = `
120
120
  <button
121
121
  aria-disabled="false"
122
- class="ucl-uikit-button css-oqr4zz"
122
+ class="ucl-uikit-button css-19pu3x7"
123
123
  data-testid="ucl-uikit-button"
124
124
  >
125
125
  Download
@@ -1,11 +1,9 @@
1
1
  import { HTMLAttributes, ReactNode } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
3
  import Dropdown, { DropdownProps } from '../Dropdown';
4
- import Button from '../Button';
5
- import IconButton from '../IconButton';
6
- import Icon from '../Icon';
7
4
  import useTheme from '../../theme/useTheme';
8
5
  import useMediaQuery from '../../hooks/useMediaQuery';
6
+ import DropdownMenuTrigger from './DropdownMenuTrigger';
9
7
 
10
8
  export const NAME = 'ucl-uikit-dropdown-menu';
11
9
 
@@ -15,7 +13,8 @@ export interface DropdownMenuProps
15
13
  Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
16
14
  children: ReactNode;
17
15
  label?: string;
18
- triggerAriaLabel?: string;
16
+ triggerAriaLabelCollapsed?: string;
17
+ triggerAriaLabelExpanded?: string;
19
18
  dropdownClassName?: string;
20
19
  triggerClassName?: string;
21
20
  contentClassName?: string;
@@ -24,7 +23,8 @@ export interface DropdownMenuProps
24
23
  const DropdownMenu = ({
25
24
  children,
26
25
  label = 'MENU',
27
- triggerAriaLabel = 'Open menu',
26
+ triggerAriaLabelCollapsed = 'Open menu',
27
+ triggerAriaLabelExpanded = 'Close menu',
28
28
  dropdownClassName,
29
29
  triggerClassName,
30
30
  contentClassName,
@@ -56,25 +56,13 @@ const DropdownMenu = ({
56
56
  {...props}
57
57
  >
58
58
  <Dropdown.Trigger>
59
- {isDesktop ? (
60
- <Button
61
- variant='tertiary-no-padding'
62
- size='small'
63
- className={resolvedTriggerClassName}
64
- icon={<Icon.Menu2 />}
65
- iconPosition='right'
66
- aria-label={triggerAriaLabel}
67
- >
68
- {label}
69
- </Button>
70
- ) : (
71
- <IconButton
72
- className={resolvedTriggerClassName}
73
- aria-label={triggerAriaLabel}
74
- >
75
- <Icon.Menu2 />
76
- </IconButton>
77
- )}
59
+ <DropdownMenuTrigger
60
+ isDesktop={isDesktop}
61
+ label={label}
62
+ triggerAriaLabelCollapsed={triggerAriaLabelCollapsed}
63
+ triggerAriaLabelExpanded={triggerAriaLabelExpanded}
64
+ className={resolvedTriggerClassName}
65
+ />
78
66
  </Dropdown.Trigger>
79
67
  <Dropdown.Content className={contentClassName}>
80
68
  {children}
@@ -0,0 +1,74 @@
1
+ import type {
2
+ FocusEventHandler,
3
+ KeyboardEventHandler,
4
+ MouseEventHandler,
5
+ } from 'react';
6
+ import Button from '../Button';
7
+ import IconButton from '../IconButton';
8
+ import Icon from '../Icon';
9
+
10
+ export interface DropdownMenuTriggerProps {
11
+ isDesktop: boolean;
12
+ label: string;
13
+ triggerAriaLabelCollapsed: string;
14
+ triggerAriaLabelExpanded: string;
15
+ className?: string;
16
+ onClick?: MouseEventHandler<HTMLElement>;
17
+ onKeyDown?: KeyboardEventHandler<HTMLElement>;
18
+ onBlur?: FocusEventHandler<HTMLElement>;
19
+ onFocus?: FocusEventHandler<HTMLElement>;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ const DropdownMenuTrigger = ({
24
+ isDesktop,
25
+ label,
26
+ triggerAriaLabelCollapsed,
27
+ triggerAriaLabelExpanded,
28
+ className,
29
+ ...props
30
+ }: DropdownMenuTriggerProps) => {
31
+ const isExpanded = Boolean(props['aria-expanded']);
32
+ const triggerAriaLabel = isExpanded
33
+ ? triggerAriaLabelExpanded
34
+ : triggerAriaLabelCollapsed;
35
+ const icon = isExpanded ? (
36
+ <Icon.X
37
+ title='Close menu'
38
+ testId='ucl-uikit-dropdown-menu__icon-close'
39
+ />
40
+ ) : (
41
+ <Icon.Menu2
42
+ title='Open menu'
43
+ testId='ucl-uikit-dropdown-menu__icon-open'
44
+ />
45
+ );
46
+
47
+ if (isDesktop) {
48
+ return (
49
+ <Button
50
+ variant='tertiary-no-padding'
51
+ size='small'
52
+ className={className}
53
+ icon={icon}
54
+ iconPosition='right'
55
+ aria-label={triggerAriaLabel}
56
+ {...props}
57
+ >
58
+ {label}
59
+ </Button>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <IconButton
65
+ className={className}
66
+ aria-label={triggerAriaLabel}
67
+ {...props}
68
+ >
69
+ {icon}
70
+ </IconButton>
71
+ );
72
+ };
73
+
74
+ export default DropdownMenuTrigger;
@@ -33,6 +33,9 @@ describe('DropdownMenu', () => {
33
33
  screen.getByRole('button', { name: 'Open menu' })
34
34
  ).toBeInTheDocument();
35
35
  expect(screen.getByText('Menu')).toBeInTheDocument();
36
+ expect(
37
+ screen.getByTestId('ucl-uikit-dropdown-menu__icon-open')
38
+ ).toBeVisible();
36
39
  });
37
40
 
38
41
  test('renders icon-only trigger on smaller screens', () => {
@@ -50,6 +53,40 @@ describe('DropdownMenu', () => {
50
53
  screen.getByRole('button', { name: 'Open menu' })
51
54
  ).toBeInTheDocument();
52
55
  expect(screen.queryByText('Menu')).not.toBeInTheDocument();
56
+ expect(
57
+ screen.getByTestId('ucl-uikit-dropdown-menu__icon-open')
58
+ ).toBeVisible();
59
+ });
60
+
61
+ test('switches trigger icon from menu to close when expanded', () => {
62
+ render(
63
+ <ThemeContextProvider>
64
+ <DropdownMenu>
65
+ <div>Menu content</div>
66
+ </DropdownMenu>
67
+ </ThemeContextProvider>
68
+ );
69
+
70
+ const trigger = screen.getByRole('button', { name: 'Open menu' });
71
+
72
+ expect(
73
+ screen.getByTestId('ucl-uikit-dropdown-menu__icon-open')
74
+ ).toBeVisible();
75
+ expect(
76
+ screen.queryByTestId('ucl-uikit-dropdown-menu__icon-close')
77
+ ).not.toBeInTheDocument();
78
+
79
+ fireEvent.click(trigger);
80
+
81
+ expect(
82
+ screen.getByRole('button', { name: 'Close menu' })
83
+ ).toBeInTheDocument();
84
+ expect(
85
+ screen.getByTestId('ucl-uikit-dropdown-menu__icon-close')
86
+ ).toBeVisible();
87
+ expect(
88
+ screen.queryByTestId('ucl-uikit-dropdown-menu__icon-open')
89
+ ).not.toBeInTheDocument();
53
90
  });
54
91
 
55
92
  test('supports className overrides for trigger and content', () => {
@@ -65,7 +102,7 @@ describe('DropdownMenu', () => {
65
102
  </ThemeContextProvider>
66
103
  );
67
104
 
68
- expect(screen.getByRole('button', { name: 'Open menu' })).toHaveClass(
105
+ expect(screen.getByRole('button', { name: 'Close menu' })).toHaveClass(
69
106
  'trigger-override'
70
107
  );
71
108
  expect(screen.getByTestId('ucl-uikit-dropdown__content')).toHaveClass(
@@ -93,4 +130,24 @@ describe('DropdownMenu', () => {
93
130
  fireEvent.click(trigger);
94
131
  expect(content).not.toBeVisible();
95
132
  });
133
+
134
+ test('supports separate collapsed and expanded aria labels', () => {
135
+ render(
136
+ <ThemeContextProvider>
137
+ <DropdownMenu
138
+ triggerAriaLabelCollapsed='Show menu options'
139
+ triggerAriaLabelExpanded='Hide menu options'
140
+ >
141
+ <div>Menu content</div>
142
+ </DropdownMenu>
143
+ </ThemeContextProvider>
144
+ );
145
+
146
+ const trigger = screen.getByRole('button', { name: 'Show menu options' });
147
+ fireEvent.click(trigger);
148
+
149
+ expect(
150
+ screen.getByRole('button', { name: 'Hide menu options' })
151
+ ).toBeInTheDocument();
152
+ });
96
153
  });
@@ -0,0 +1,39 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import Button, { ButtonProps } from '../Button';
3
+ import { useTheme } from '../..';
4
+
5
+ export const NAME = 'ucl-uikit-menu__action-button';
6
+
7
+ export interface ActionMenuButtonProps extends Omit<
8
+ ButtonProps<'button'>,
9
+ 'variant' | 'as'
10
+ > {}
11
+
12
+ const ActionMenuButton = ({
13
+ className,
14
+ children,
15
+ ...props
16
+ }: ActionMenuButtonProps) => {
17
+ const [theme] = useTheme();
18
+
19
+ const baseStyle = css`
20
+ margin: ${theme.margin.m8} 0;
21
+ background-color: ${theme.colour.surface.primary};
22
+ `;
23
+
24
+ const style = cx(NAME, baseStyle, className);
25
+
26
+ return (
27
+ <Button
28
+ className={style}
29
+ variant='secondary'
30
+ size='small'
31
+ fullWidth
32
+ {...props}
33
+ >
34
+ {children}
35
+ </Button>
36
+ );
37
+ };
38
+
39
+ export default ActionMenuButton;
@@ -2,7 +2,13 @@ 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 ActionMenuItem, { ActionMenuItemProps } from './ActionMenuItem';
5
+ import MenuHeading, { MenuHeadingProps } from './MenuHeading';
6
+ import MenuSection, { MenuSectionProps } from './MenuSection';
7
+ import MenuAccordion, {
8
+ MenuAccordionItemProps,
9
+ MenuAccordionProps,
10
+ } from './MenuAccordion';
11
+ import ActionMenuButton, { ActionMenuButtonProps } from './ActionMenuButton';
6
12
  import PrimaryMenuItem, { PrimaryMenuItemProps } from './PrimaryMenuItem';
7
13
  import SecondaryMenuItem, { SecondaryMenuItemProps } from './SecondaryMenuItem';
8
14
  import { useTheme } from '../../theme';
@@ -16,10 +22,14 @@ export interface MenuProps extends HTMLAttributes<HTMLDivElement> {
16
22
 
17
23
  export type {
18
24
  MenuDividerProps,
25
+ MenuHeadingProps,
26
+ MenuSectionProps,
27
+ MenuAccordionProps,
28
+ MenuAccordionItemProps,
19
29
  MenuBaseItemProps,
20
30
  PrimaryMenuItemProps,
21
31
  SecondaryMenuItemProps,
22
- ActionMenuItemProps,
32
+ ActionMenuButtonProps,
23
33
  };
24
34
 
25
35
  const Menu = ({
@@ -32,8 +42,10 @@ const Menu = ({
32
42
  const [theme] = useTheme();
33
43
  const baseStyle = css`
34
44
  padding: ${theme.padding.p16} ${theme.padding.p24};
45
+ background-color: ${theme.colour.surface.primary};
35
46
 
36
47
  @media screen and (min-width: ${theme.breakpoints.tablet}px) {
48
+ width: 360px;
37
49
  padding: ${theme.padding.p48} ${theme.padding.p64};
38
50
  }
39
51
  `;
@@ -54,19 +66,25 @@ const Menu = ({
54
66
  };
55
67
 
56
68
  interface MenuComponent extends NamedExoticComponent<MenuProps> {
69
+ Section: typeof MenuSection;
70
+ Heading: typeof MenuHeading;
71
+ Accordion: typeof MenuAccordion;
57
72
  Divider: typeof MenuDivider;
58
73
  BaseItem: typeof MenuBaseItem;
59
74
  PrimaryItem: typeof PrimaryMenuItem;
60
75
  SecondaryItem: typeof SecondaryMenuItem;
61
- ActionItem: typeof ActionMenuItem;
76
+ ActionButton: typeof ActionMenuButton;
62
77
  }
63
78
 
64
79
  const MemoMenu: MenuComponent = Object.assign(memo(Menu), {
80
+ Section: MenuSection,
81
+ Heading: MenuHeading,
82
+ Accordion: MenuAccordion,
65
83
  Divider: MenuDivider,
66
84
  BaseItem: MenuBaseItem,
67
85
  PrimaryItem: PrimaryMenuItem,
68
86
  SecondaryItem: SecondaryMenuItem,
69
- ActionItem: ActionMenuItem,
87
+ ActionButton: ActionMenuButton,
70
88
  });
71
89
 
72
90
  export default MemoMenu;
@@ -0,0 +1,104 @@
1
+ import { css, cx } from '@emotion/css';
2
+ import {
3
+ MouseEvent,
4
+ NamedExoticComponent,
5
+ ReactNode,
6
+ memo,
7
+ useState,
8
+ } from 'react';
9
+ import { MenuBaseItemProps } from '../MenuBaseItem';
10
+ import MenuBaseItem from '../MenuBaseItem';
11
+ import MenuItemText from '../MenuItemText';
12
+ import { useTheme } from '../../../theme';
13
+ import MenuAccordionChevron from './MenuAccordionChevron';
14
+ import MenuAccordionItem, { MenuAccordionItemProps } from './MenuAccordionItem';
15
+
16
+ export const NAME = 'ucl-uikit-menu__accordion';
17
+
18
+ export interface MenuAccordionProps extends Omit<
19
+ MenuBaseItemProps,
20
+ 'iconPosition' | 'asChild' | 'children'
21
+ > {
22
+ label: ReactNode;
23
+ defaultExpanded?: boolean;
24
+ children?: ReactNode;
25
+ }
26
+
27
+ const MenuAccordion = ({
28
+ className,
29
+ label,
30
+ children,
31
+ defaultExpanded = false,
32
+ onClick,
33
+ ...props
34
+ }: MenuAccordionProps) => {
35
+ const [theme] = useTheme();
36
+ const [expanded, setExpanded] = useState(defaultExpanded);
37
+
38
+ const baseStyle = css`
39
+ width: 100%;
40
+ color: ${theme.colour.text.default};
41
+
42
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
43
+ padding: 0 ${theme.padding.p16};
44
+ }
45
+
46
+ &:hover {
47
+ background-color: ${theme.colour.fill.brandSubtleHover};
48
+ }
49
+ `;
50
+
51
+ const style = cx(NAME, baseStyle, className);
52
+
53
+ const textStyle = css``;
54
+
55
+ const contentStyle = css`
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: ${theme.margin.m16};
59
+ `;
60
+
61
+ const bodyStyle = css`
62
+ width: 100%;
63
+ margin-top: ${theme.margin.m8};
64
+ `;
65
+
66
+ const handleClick = (event: MouseEvent<HTMLDivElement>) => {
67
+ setExpanded((previous) => !previous);
68
+ onClick?.(event);
69
+ };
70
+
71
+ return (
72
+ <>
73
+ <MenuBaseItem
74
+ className={style}
75
+ iconPosition='left'
76
+ underlineOnHover={false}
77
+ onClick={handleClick}
78
+ endAdornment={<MenuAccordionChevron expanded={expanded} />}
79
+ aria-expanded={expanded}
80
+ {...props}
81
+ >
82
+ <span className={contentStyle}>
83
+ <MenuItemText className={textStyle}>{label}</MenuItemText>
84
+ </span>
85
+ </MenuBaseItem>
86
+ {expanded && <div className={bodyStyle}>{children}</div>}
87
+ </>
88
+ );
89
+ };
90
+
91
+ interface MenuAccordionComponent extends NamedExoticComponent<MenuAccordionProps> {
92
+ Item: typeof MenuAccordionItem;
93
+ }
94
+
95
+ const MemoMenuAccordion: MenuAccordionComponent = Object.assign(
96
+ memo(MenuAccordion),
97
+ {
98
+ Item: MenuAccordionItem,
99
+ }
100
+ );
101
+
102
+ export type { MenuAccordionItemProps };
103
+
104
+ export default MemoMenuAccordion;
@@ -0,0 +1,30 @@
1
+ import Icon from '../../Icon';
2
+
3
+ export interface MenuAccordionChevronProps {
4
+ expanded: boolean;
5
+ className?: string;
6
+ }
7
+
8
+ const MenuAccordionChevron = ({
9
+ expanded,
10
+ className,
11
+ }: MenuAccordionChevronProps) =>
12
+ expanded ? (
13
+ <Icon.ChevronUp
14
+ className={className}
15
+ size={24}
16
+ aria-hidden='true'
17
+ focusable='false'
18
+ testId='ucl-uikit-menu__accordion__icon--chevron-up'
19
+ />
20
+ ) : (
21
+ <Icon.ChevronDown
22
+ className={className}
23
+ size={24}
24
+ aria-hidden='true'
25
+ focusable='false'
26
+ testId='ucl-uikit-menu__accordion__icon--chevron-down'
27
+ />
28
+ );
29
+
30
+ export default MenuAccordionChevron;
@@ -0,0 +1,47 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import MenuBaseItem from '../MenuBaseItem';
4
+ import MenuItemText from '../MenuItemText';
5
+ import { useTheme } from '../../../theme';
6
+
7
+ export const NAME = 'ucl-uikit-menu__accordion-item';
8
+
9
+ export interface MenuAccordionItemProps extends HTMLAttributes<HTMLDivElement> {
10
+ asChild?: boolean;
11
+ testId?: string;
12
+ }
13
+
14
+ const MenuAccordionItem = ({
15
+ asChild,
16
+ testId = NAME,
17
+ className,
18
+ children,
19
+ ...props
20
+ }: MenuAccordionItemProps) => {
21
+ const [theme] = useTheme();
22
+
23
+ const baseStyle = css`
24
+ margin: 0;
25
+ padding-left: ${theme.padding.p40};
26
+
27
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
28
+ padding: 0 ${theme.padding.p56};
29
+ }
30
+ `;
31
+
32
+ const style = cx(NAME, baseStyle, className);
33
+
34
+ return (
35
+ <MenuBaseItem
36
+ className={style}
37
+ iconPosition='left'
38
+ asChild={asChild}
39
+ data-testid={testId}
40
+ {...props}
41
+ >
42
+ <MenuItemText asChild={asChild}>{children}</MenuItemText>
43
+ </MenuBaseItem>
44
+ );
45
+ };
46
+
47
+ export default MenuAccordionItem;
@@ -0,0 +1,5 @@
1
+ export { default } from './MenuAccordion';
2
+ export type {
3
+ MenuAccordionProps,
4
+ MenuAccordionItemProps,
5
+ } from './MenuAccordion';