uikit-react-public 0.25.1 → 0.25.3

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 (38) 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 +5380 -5138
  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/lib/hooks/useMediaQuery.ts +11 -1
  36. package/package.json +1 -1
  37. package/dist/components/MenuNew/ActionMenuItem.d.ts +0 -7
  38. package/lib/components/MenuNew/ActionMenuItem.tsx +0 -31
@@ -5,21 +5,28 @@ import { useTheme } from '../../theme';
5
5
  export interface MenuBaseItemProps extends HTMLAttributes<HTMLDivElement> {
6
6
  icon?: ReactNode;
7
7
  iconPosition?: 'left' | 'right';
8
+ endAdornment?: ReactNode;
9
+ underlineOnHover?: boolean;
10
+ asChild?: boolean;
8
11
  }
9
12
 
10
13
  const MenuBaseItem = ({
11
14
  icon,
12
15
  iconPosition = 'left',
16
+ endAdornment,
13
17
  className,
14
18
  children,
15
19
  onClick,
16
20
  onKeyDown,
17
21
  role,
18
22
  tabIndex,
23
+ underlineOnHover = true,
24
+ asChild = false,
19
25
  ...props
20
26
  }: MenuBaseItemProps) => {
21
27
  const [theme] = useTheme();
22
28
  const isInteractive = typeof onClick === 'function';
29
+ const shouldUseButtonSemantics = !asChild;
23
30
 
24
31
  const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
25
32
  onKeyDown?.(event);
@@ -39,32 +46,55 @@ const MenuBaseItem = ({
39
46
  };
40
47
 
41
48
  const baseStyle = css`
42
- width: 360px;
49
+ width: 100%;
43
50
  box-sizing: border-box;
44
51
  min-height: 48px;
52
+ border-radius: ${theme.radius.r2};
45
53
  display: flex;
46
54
  align-items: center;
47
- gap: ${theme.margin.m32};
55
+ gap: ${theme.margin.m16};
48
56
  cursor: pointer;
57
+ outline: none;
49
58
 
50
- &:hover .ucl-uikit-menu__item-text {
51
- text-decoration: underline;
59
+ &:focus-visible {
60
+ box-shadow: ${theme.boxShadow.focus};
52
61
  }
62
+
63
+ &:focus-within {
64
+ box-shadow: ${theme.boxShadow.focus};
65
+ }
66
+
67
+ ${underlineOnHover &&
68
+ `
69
+ &:hover .ucl-uikit-menu__item-text {
70
+ text-decoration: underline;
71
+ }
72
+ `}
53
73
  `;
54
74
  const style = cx(baseStyle, className);
55
75
 
76
+ const endAdornmentStyle = css`
77
+ margin-left: auto;
78
+ display: inline-flex;
79
+ align-items: center;
80
+ flex-shrink: 0;
81
+ `;
82
+
56
83
  return (
57
84
  <div
58
85
  className={style}
59
86
  onClick={onClick}
60
87
  onKeyDown={handleKeyDown}
61
- role={role ?? (isInteractive ? 'button' : undefined)}
62
- tabIndex={tabIndex ?? (isInteractive ? 0 : undefined)}
88
+ role={role ?? (shouldUseButtonSemantics ? 'button' : undefined)}
89
+ tabIndex={tabIndex ?? (shouldUseButtonSemantics ? 0 : undefined)}
63
90
  {...props}
64
91
  >
65
92
  {iconPosition === 'left' && icon}
66
93
  {children}
67
94
  {iconPosition === 'right' && icon}
95
+ {endAdornment && (
96
+ <span className={endAdornmentStyle}>{endAdornment}</span>
97
+ )}
68
98
  </div>
69
99
  );
70
100
  };
@@ -10,7 +10,9 @@ const MenuDivider = ({ className, ...props }: MenuDividerProps) => {
10
10
  const [theme] = useTheme();
11
11
  const baseStyle = css`
12
12
  border: 0;
13
- border-top: 1px solid ${theme.colour.border.subtle};
13
+ height: 1px;
14
+ background-color: ${theme.colour.border.subtle};
15
+ margin: ${theme.margin.m16} 0;
14
16
  `;
15
17
  const style = cx(NAME, baseStyle, className);
16
18
 
@@ -0,0 +1,56 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import Heading from '../Heading';
4
+ import { useTheme } from '../../theme';
5
+
6
+ export const NAME = 'ucl-uikit-menu__heading';
7
+
8
+ export interface MenuHeadingProps extends HTMLAttributes<HTMLDivElement> {
9
+ testId?: string;
10
+ }
11
+
12
+ const MenuHeading = ({
13
+ testId = NAME,
14
+ className,
15
+ children,
16
+ ...props
17
+ }: MenuHeadingProps) => {
18
+ const [theme] = useTheme();
19
+
20
+ const baseStyle = css`
21
+ box-sizing: border-box;
22
+ margin: ${theme.margin.m32} 0 ${theme.margin.m16};
23
+ text-transform: uppercase;
24
+
25
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
26
+ padding: 0 ${theme.padding.p16};
27
+ }
28
+ `;
29
+
30
+ const style = cx(NAME, baseStyle, className);
31
+
32
+ const headingStyle = css`
33
+ color: ${theme.colour.text.secondary};
34
+ text-transform: uppercase;
35
+ font-size: 14px;
36
+ `;
37
+
38
+ return (
39
+ <div
40
+ className={style}
41
+ data-testid={testId}
42
+ {...props}
43
+ >
44
+ <Heading
45
+ as='h3'
46
+ level='xs'
47
+ margins={false}
48
+ className={headingStyle}
49
+ >
50
+ {children}
51
+ </Heading>
52
+ </div>
53
+ );
54
+ };
55
+
56
+ export default MenuHeading;
@@ -33,6 +33,12 @@ const MenuItemText = ({
33
33
  color: inherit;
34
34
  text-decoration: inherit;
35
35
  }
36
+
37
+ &:focus,
38
+ &:focus-visible {
39
+ outline: none;
40
+ box-shadow: none;
41
+ }
36
42
  `;
37
43
  const style = cx(NAME, className);
38
44
 
@@ -0,0 +1,36 @@
1
+ import { HTMLAttributes } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+
4
+ export const NAME = 'ucl-uikit-menu__section';
5
+
6
+ export interface MenuSectionProps extends HTMLAttributes<HTMLElement> {
7
+ testId?: string;
8
+ }
9
+
10
+ const MenuSection = ({
11
+ testId = NAME,
12
+ className,
13
+ children,
14
+ ...props
15
+ }: MenuSectionProps) => {
16
+ const style = cx(
17
+ NAME,
18
+ css`
19
+ margin: 0;
20
+ padding: 0;
21
+ `,
22
+ className
23
+ );
24
+
25
+ return (
26
+ <section
27
+ className={style}
28
+ data-testid={testId}
29
+ {...props}
30
+ >
31
+ {children}
32
+ </section>
33
+ );
34
+ };
35
+
36
+ export default MenuSection;
@@ -26,6 +26,10 @@ const PrimaryMenuItem = ({
26
26
 
27
27
  const baseStyle = css`
28
28
  color: ${theme.colour.text.default};
29
+
30
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
31
+ padding: 0 ${theme.padding.p16};
32
+ }
29
33
  `;
30
34
 
31
35
  const style = cx(NAME, baseStyle, className);
@@ -41,8 +45,9 @@ const PrimaryMenuItem = ({
41
45
  return (
42
46
  <MenuBaseItem
43
47
  className={style}
44
- {...props}
48
+ asChild={asChild}
45
49
  iconPosition='left'
50
+ {...props}
46
51
  >
47
52
  <span className={contentStyle}>
48
53
  <MenuItemText
@@ -25,6 +25,7 @@ const SecondaryMenuItem = ({
25
25
  const [theme] = useTheme();
26
26
 
27
27
  const baseStyle = css`
28
+ padding-inline: ${theme.margin.m16};
28
29
  color: ${theme.colour.text.secondary};
29
30
  `;
30
31
 
@@ -43,6 +44,7 @@ const SecondaryMenuItem = ({
43
44
  return (
44
45
  <MenuBaseItem
45
46
  className={style}
47
+ asChild={asChild}
46
48
  {...props}
47
49
  >
48
50
  <span className={contentStyle}>
@@ -13,6 +13,92 @@ describe('Menu', () => {
13
13
  expect(screen.getByTestId('ucl-uikit-menu')).toBeInTheDocument();
14
14
  });
15
15
 
16
+ test('renders heading item', () => {
17
+ wrap(
18
+ <Menu>
19
+ <Menu.Heading>Main navigation</Menu.Heading>
20
+ </Menu>
21
+ );
22
+
23
+ const heading = screen.getByTestId('ucl-uikit-menu__heading');
24
+ expect(heading).toBeInTheDocument();
25
+ expect(heading).toHaveTextContent('Main navigation');
26
+ expect(heading).not.toHaveAttribute('role', 'button');
27
+ });
28
+
29
+ test('renders semantic section wrapper', () => {
30
+ wrap(
31
+ <Menu>
32
+ <Menu.Section>
33
+ <Menu.Heading>Support</Menu.Heading>
34
+ </Menu.Section>
35
+ </Menu>
36
+ );
37
+
38
+ const section = screen.getByTestId('ucl-uikit-menu__section');
39
+ expect(section.tagName).toBe('SECTION');
40
+ });
41
+
42
+ test('accordion renders like a primary item', () => {
43
+ const { getByText } = wrap(
44
+ <Menu>
45
+ <Menu.Accordion
46
+ label='Services'
47
+ icon={<span data-testid='accordion-left-icon'>I</span>}
48
+ >
49
+ <Menu.Accordion.Item>Library</Menu.Accordion.Item>
50
+ </Menu.Accordion>
51
+ </Menu>
52
+ );
53
+
54
+ const item = getByText('Services').closest('.ucl-uikit-menu__accordion');
55
+ expect(item?.firstChild).toHaveAttribute(
56
+ 'data-testid',
57
+ 'accordion-left-icon'
58
+ );
59
+ expect(
60
+ screen.getByTestId('ucl-uikit-menu__accordion__icon--chevron-down')
61
+ ).toBeInTheDocument();
62
+ });
63
+
64
+ test('accordion toggles to expanded state with up chevron', () => {
65
+ wrap(
66
+ <Menu>
67
+ <Menu.Accordion
68
+ label='Services'
69
+ icon={<span data-testid='accordion-left-icon'>I</span>}
70
+ >
71
+ <Menu.Accordion.Item>Library</Menu.Accordion.Item>
72
+ </Menu.Accordion>
73
+ </Menu>
74
+ );
75
+
76
+ expect(screen.queryByText('Library')).not.toBeInTheDocument();
77
+ fireEvent.click(screen.getByText('Services'));
78
+ expect(
79
+ screen.getByTestId('ucl-uikit-menu__accordion__icon--chevron-up')
80
+ ).toBeInTheDocument();
81
+ expect(screen.getByText('Library')).toBeInTheDocument();
82
+ });
83
+
84
+ test('accordion item supports asChild link content', () => {
85
+ wrap(
86
+ <Menu>
87
+ <Menu.Accordion
88
+ label='Services'
89
+ icon={<span data-testid='accordion-left-icon'>I</span>}
90
+ defaultExpanded
91
+ >
92
+ <Menu.Accordion.Item asChild>
93
+ <a href='/library'>Library</a>
94
+ </Menu.Accordion.Item>
95
+ </Menu.Accordion>
96
+ </Menu>
97
+ );
98
+
99
+ expect(screen.getByRole('link', { name: 'Library' })).toBeInTheDocument();
100
+ });
101
+
16
102
  test('has no border by default', () => {
17
103
  wrap(<Menu>Content</Menu>);
18
104
 
@@ -63,6 +149,21 @@ describe('Menu', () => {
63
149
  expect(onClick).toHaveBeenCalledTimes(2);
64
150
  });
65
151
 
152
+ test('non-asChild items expose button semantics', () => {
153
+ wrap(
154
+ <Menu>
155
+ <Menu.PrimaryItem>Home</Menu.PrimaryItem>
156
+ </Menu>
157
+ );
158
+
159
+ const item = screen
160
+ .getByText('Home')
161
+ .closest('.ucl-uikit-menu__primary-item');
162
+
163
+ expect(item).toHaveAttribute('role', 'button');
164
+ expect(item).toHaveAttribute('tabindex', '0');
165
+ });
166
+
66
167
  test('renders asChild link content for primary item', () => {
67
168
  wrap(
68
169
  <Menu>
@@ -75,6 +176,10 @@ describe('Menu', () => {
75
176
  const link = screen.getByRole('link', { name: 'Home' });
76
177
  expect(link).toBeInTheDocument();
77
178
  expect(link.closest('.ucl-uikit-menu__item-text')).toBeInTheDocument();
179
+ expect(link.closest('.ucl-uikit-menu__primary-item')).not.toHaveAttribute(
180
+ 'role',
181
+ 'button'
182
+ );
78
183
  });
79
184
 
80
185
  test('primary item enforces icon on the left', () => {
@@ -90,17 +195,35 @@ describe('Menu', () => {
90
195
  expect(item?.firstChild).toHaveAttribute('data-testid', 'left-icon');
91
196
  });
92
197
 
93
- test('action item enforces icon on the right', () => {
94
- const { getByText } = wrap(
198
+ test('can disable hover underline', () => {
199
+ wrap(
200
+ <Menu>
201
+ <Menu.PrimaryItem underlineOnHover={false}>Home</Menu.PrimaryItem>
202
+ </Menu>
203
+ );
204
+
205
+ const item = screen
206
+ .getByText('Home')
207
+ .closest('.ucl-uikit-menu__primary-item');
208
+ const text = screen.getByText('Home');
209
+
210
+ fireEvent.mouseEnter(item as Element);
211
+
212
+ expect(window.getComputedStyle(text).textDecorationLine).not.toBe(
213
+ 'underline'
214
+ );
215
+ });
216
+
217
+ test('action button renders icon', () => {
218
+ wrap(
95
219
  <Menu>
96
- <Menu.ActionItem icon={<span data-testid='right-icon'>I</span>}>
220
+ <Menu.ActionButton icon={<span data-testid='right-icon'>I</span>}>
97
221
  Sign out
98
- </Menu.ActionItem>
222
+ </Menu.ActionButton>
99
223
  </Menu>
100
224
  );
101
225
 
102
- const item = getByText('Sign out').closest('.ucl-uikit-menu__action-item');
103
- expect(item?.lastChild).toHaveAttribute('data-testid', 'right-icon');
226
+ expect(screen.getByTestId('right-icon')).toBeInTheDocument();
104
227
  });
105
228
 
106
229
  test('secondary externalLink item renders external icon', () => {
@@ -2,8 +2,11 @@ export { default } from './Menu';
2
2
  export type {
3
3
  MenuProps,
4
4
  MenuDividerProps,
5
+ MenuHeadingProps,
6
+ MenuSectionProps,
7
+ MenuAccordionProps,
5
8
  MenuBaseItemProps,
6
9
  PrimaryMenuItemProps,
7
10
  SecondaryMenuItemProps,
8
- ActionMenuItemProps,
11
+ ActionMenuButtonProps,
9
12
  } from './Menu';
@@ -95,7 +95,7 @@ exports[`Cell > Snapshot: variants 1`] = `
95
95
  >
96
96
  <button
97
97
  aria-disabled="false"
98
- class="ucl-uikit-button css-9aah8t"
98
+ class="ucl-uikit-button css-1r34e1w"
99
99
  data-testid="ucl-uikit-button"
100
100
  >
101
101
  Button
@@ -1,11 +1,21 @@
1
1
  import { useState, useEffect } from 'react';
2
2
 
3
+ const getInitialMatch = (query: string): boolean => {
4
+ if (typeof window === 'undefined') return false;
5
+ return window.matchMedia(query).matches;
6
+ };
7
+
3
8
  const useMediaQuery = (query: string): boolean => {
4
- const [isQueryMatch, setIsQueryMatch] = useState<boolean>(false);
9
+ const [isQueryMatch, setIsQueryMatch] = useState<boolean>(() =>
10
+ getInitialMatch(query)
11
+ );
5
12
 
6
13
  useEffect(() => {
14
+ if (typeof window === 'undefined') return;
15
+
7
16
  const mediaQueryList = window.matchMedia(query);
8
17
 
18
+ // Keep state in sync if query changes
9
19
  setIsQueryMatch(mediaQueryList.matches);
10
20
 
11
21
  const handleChange = (event: MediaQueryListEvent) => {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.25.1",
5
+ "version": "0.25.3",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -1,7 +0,0 @@
1
- import { MenuBaseItemProps } from './MenuBaseItem';
2
- export declare const NAME = "ucl-uikit-menu__action-item";
3
- export interface ActionMenuItemProps extends MenuBaseItemProps {
4
- asChild?: boolean;
5
- }
6
- declare const ActionMenuItem: ({ className, children, asChild, ...props }: ActionMenuItemProps) => import("react/jsx-runtime").JSX.Element;
7
- export default ActionMenuItem;
@@ -1,31 +0,0 @@
1
- import { cx } from '@emotion/css';
2
- import { MenuBaseItemProps } from './MenuBaseItem';
3
- import MenuBaseItem from './MenuBaseItem';
4
- import MenuItemText from './MenuItemText';
5
-
6
- export const NAME = 'ucl-uikit-menu__action-item';
7
-
8
- export interface ActionMenuItemProps extends MenuBaseItemProps {
9
- asChild?: boolean;
10
- }
11
-
12
- const ActionMenuItem = ({
13
- className,
14
- children,
15
- asChild,
16
- ...props
17
- }: ActionMenuItemProps) => {
18
- const style = cx(NAME, className);
19
-
20
- return (
21
- <MenuBaseItem
22
- className={style}
23
- {...props}
24
- iconPosition='right'
25
- >
26
- <MenuItemText asChild={asChild}>{children}</MenuItemText>
27
- </MenuBaseItem>
28
- );
29
- };
30
-
31
- export default ActionMenuItem;