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.
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +3 -2
- package/dist/components/DropdownMenu/DropdownMenuTrigger.d.ts +15 -0
- package/dist/components/MenuNew/ActionMenuButton.d.ts +6 -0
- package/dist/components/MenuNew/Menu.d.ts +9 -3
- package/dist/components/MenuNew/MenuAccordion/MenuAccordion.d.ts +15 -0
- package/dist/components/MenuNew/MenuAccordion/MenuAccordionChevron.d.ts +6 -0
- package/dist/components/MenuNew/MenuAccordion/MenuAccordionItem.d.ts +8 -0
- package/dist/components/MenuNew/MenuAccordion/index.d.ts +2 -0
- package/dist/components/MenuNew/MenuBaseItem.d.ts +4 -1
- package/dist/components/MenuNew/MenuHeading.d.ts +7 -0
- package/dist/components/MenuNew/MenuSection.d.ts +7 -0
- package/dist/components/MenuNew/index.d.ts +1 -1
- package/dist/index.js +5376 -5137
- package/lib/components/Button/Button.tsx +4 -1
- package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +9 -9
- package/lib/components/DropdownMenu/DropdownMenu.tsx +12 -24
- package/lib/components/DropdownMenu/DropdownMenuTrigger.tsx +74 -0
- package/lib/components/DropdownMenu/__tests__/DropdownMenu.test.tsx +58 -1
- package/lib/components/MenuNew/ActionMenuButton.tsx +39 -0
- package/lib/components/MenuNew/Menu.tsx +22 -4
- package/lib/components/MenuNew/MenuAccordion/MenuAccordion.tsx +104 -0
- package/lib/components/MenuNew/MenuAccordion/MenuAccordionChevron.tsx +30 -0
- package/lib/components/MenuNew/MenuAccordion/MenuAccordionItem.tsx +47 -0
- package/lib/components/MenuNew/MenuAccordion/index.ts +5 -0
- package/lib/components/MenuNew/MenuBaseItem.tsx +36 -6
- package/lib/components/MenuNew/MenuDivider.tsx +3 -1
- package/lib/components/MenuNew/MenuHeading.tsx +56 -0
- package/lib/components/MenuNew/MenuItemText.tsx +6 -0
- package/lib/components/MenuNew/MenuSection.tsx +36 -0
- package/lib/components/MenuNew/PrimaryMenuItem.tsx +6 -1
- package/lib/components/MenuNew/SecondaryMenuItem.tsx +2 -0
- package/lib/components/MenuNew/__tests__/Menu.test.tsx +129 -6
- package/lib/components/MenuNew/index.ts +4 -1
- package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +1 -1
- package/package.json +1 -1
- package/dist/components/MenuNew/ActionMenuItem.d.ts +0 -7
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|