uikit-react-public 0.42.0 → 0.43.1

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.
@@ -71,11 +71,8 @@ const AccordionHeading: React.FC<AccordionHeadingProps> = ({
71
71
  `;
72
72
 
73
73
  const disabledStyle = css`
74
- cursor: 'not-allowed';
75
-
76
- &:hover {
77
- background-color: ${theme.colour.text.disabled};
78
- }
74
+ cursor: not-allowed;
75
+ color: ${theme.colour.text.disabled};
79
76
  `;
80
77
 
81
78
  const headingStyle = cx(
@@ -47,6 +47,7 @@ export interface AccordionProps extends HTMLAttributes<HTMLDivElement> {
47
47
  size?: AccordionSize;
48
48
  disabled?: boolean;
49
49
  isOpen?: boolean;
50
+ variant?: 'no-background' | 'on-background';
50
51
  }
51
52
 
52
53
  const Accordion: React.FC<AccordionProps> & {
@@ -56,6 +57,7 @@ const Accordion: React.FC<AccordionProps> & {
56
57
  size = 'default',
57
58
  disabled = false,
58
59
  isOpen = false,
60
+ variant = 'no-background',
59
61
  children,
60
62
  className,
61
63
  testId = NAME,
@@ -76,7 +78,8 @@ const Accordion: React.FC<AccordionProps> & {
76
78
  min-width: 288px;
77
79
  max-width: 660px;
78
80
  box-sizing: border-box;
79
- border: 1px solid ${theme.colour.border.default};
81
+ border-top: 1px solid ${theme.colour.border.default};
82
+ border-bottom: 1px solid ${theme.colour.border.default};
80
83
  background-color: ${theme.colour.surface.default};
81
84
  color: ${theme.colour.text.default};
82
85
  flex-shrink: 1;
@@ -88,7 +91,17 @@ const Accordion: React.FC<AccordionProps> & {
88
91
  color: ${theme.colour.text.disabled};
89
92
  `;
90
93
 
91
- const style = cx(NAME, baseStyle, disabled && disabledStyle, className);
94
+ const onBackgroundStyle = css`
95
+ border: 1px solid ${theme.colour.border.default};
96
+ `;
97
+
98
+ const style = cx(
99
+ NAME,
100
+ baseStyle,
101
+ disabled && disabledStyle,
102
+ variant === 'on-background' && onBackgroundStyle,
103
+ className
104
+ );
92
105
 
93
106
  return (
94
107
  <AccordionContext.Provider
@@ -3,7 +3,7 @@
3
3
  exports[`Accordion > snapshot: no props 1`] = `
4
4
  <div
5
5
  aria-disabled="false"
6
- class="ucl-uikit-accordion css-cn49eu"
6
+ class="ucl-uikit-accordion css-1pwsm43"
7
7
  data-testid="ucl-uikit-accordion"
8
8
  >
9
9
  <h6
@@ -39,7 +39,7 @@ exports[`Accordion > snapshot: no props 1`] = `
39
39
  exports[`Accordion > snapshot: testId prop 1`] = `
40
40
  <div
41
41
  aria-disabled="false"
42
- class="ucl-uikit-accordion css-cn49eu"
42
+ class="ucl-uikit-accordion css-1pwsm43"
43
43
  data-testid="abc"
44
44
  >
45
45
  <h6
@@ -1,14 +1,31 @@
1
- import { HTMLAttributes, memo } from 'react';
1
+ import { HTMLAttributes, NamedExoticComponent, ReactNode, memo } from 'react';
2
2
  import { css, cx } from '@emotion/css';
3
+ import { themes, useTheme } from '../../theme';
4
+ import Icon from '../Icon';
5
+ import MenuNew, {
6
+ MenuBaseItemProps,
7
+ MenuDividerProps,
8
+ MenuHeadProps,
9
+ MenuHeadingProps,
10
+ MenuProps as MenuNewProps,
11
+ MenuSectionProps as MenuNewSectionProps,
12
+ PrimaryMenuItemProps,
13
+ SecondaryMenuItemProps,
14
+ ActionMenuButtonProps,
15
+ MenuAccordionItemProps,
16
+ MenuAccordionProps,
17
+ } from '../MenuNew';
3
18
  import MenuProvider from './Menu.context';
4
19
  import MenuContent from './MenuContent';
5
20
  import MenuItem from './MenuItem';
21
+ import { MenuItemProps } from './MenuItem';
6
22
  import MenuSection from './MenuSection';
23
+ import { MenuSectionProps } from './MenuSection';
7
24
  import MenuTrigger from './MenuTrigger';
8
25
 
9
26
  export const NAME = 'ucl-uikit-menu';
10
27
 
11
- export interface MenuProps extends HTMLAttributes<HTMLDivElement> {
28
+ export interface LegacyMenuProps extends HTMLAttributes<HTMLDivElement> {
12
29
  defaultOpen?: boolean;
13
30
  title?: string;
14
31
  menuId?: string;
@@ -16,7 +33,25 @@ export interface MenuProps extends HTMLAttributes<HTMLDivElement> {
16
33
  position?: 'left' | 'right';
17
34
  }
18
35
 
19
- const Menu = ({
36
+ export type MenuProps = Omit<MenuNewProps, 'children'> &
37
+ Pick<LegacyMenuProps, 'defaultOpen' | 'title' | 'menuId' | 'position'> & {
38
+ children?: ReactNode;
39
+ };
40
+
41
+ export type {
42
+ MenuDividerProps,
43
+ MenuHeadProps,
44
+ MenuHeadingProps,
45
+ MenuNewSectionProps,
46
+ MenuAccordionProps,
47
+ MenuAccordionItemProps,
48
+ MenuBaseItemProps,
49
+ PrimaryMenuItemProps,
50
+ SecondaryMenuItemProps,
51
+ ActionMenuButtonProps,
52
+ };
53
+
54
+ const LegacyMenuRoot = ({
20
55
  defaultOpen = false,
21
56
  title,
22
57
  position = 'right',
@@ -25,7 +60,7 @@ const Menu = ({
25
60
  children,
26
61
  className,
27
62
  ...props
28
- }: MenuProps) => {
63
+ }: LegacyMenuProps) => {
29
64
  const baseStyle = css`
30
65
  position: relative;
31
66
  `;
@@ -52,16 +87,137 @@ const Menu = ({
52
87
  );
53
88
  };
54
89
 
55
- export interface IMenuSubComponents {
90
+ export interface LegacyMenuComponent extends NamedExoticComponent<LegacyMenuProps> {
56
91
  Section: typeof MenuSection;
57
92
  Item: typeof MenuItem;
58
93
  }
59
94
 
60
- const MemoMenu = memo(Menu);
95
+ export const LegacyMenu = Object.assign(memo(LegacyMenuRoot), {
96
+ Section: MenuSection,
97
+ Item: MenuItem,
98
+ }) as LegacyMenuComponent;
61
99
 
62
- const MenuWithSubComponents = MemoMenu as typeof MemoMenu & IMenuSubComponents;
100
+ const Menu = ({
101
+ defaultOpen,
102
+ title,
103
+ position,
104
+ menuId,
105
+ testId = NAME,
106
+ className,
107
+ children,
108
+ border,
109
+ collapsed,
110
+ ...props
111
+ }: MenuProps) => {
112
+ const [theme] = useTheme();
113
+
114
+ if (theme === themes.default) {
115
+ return (
116
+ <LegacyMenu
117
+ defaultOpen={defaultOpen}
118
+ title={title}
119
+ position={position}
120
+ menuId={menuId}
121
+ testId={testId}
122
+ className={className}
123
+ {...props}
124
+ >
125
+ {children}
126
+ </LegacyMenu>
127
+ );
128
+ }
129
+
130
+ return (
131
+ <MenuNew
132
+ testId={testId}
133
+ className={className}
134
+ border={border}
135
+ collapsed={collapsed}
136
+ {...props}
137
+ >
138
+ {children}
139
+ </MenuNew>
140
+ );
141
+ };
142
+
143
+ const MenuSectionBridge = (props: MenuSectionProps | MenuNewSectionProps) => {
144
+ const [theme] = useTheme();
145
+ const Section = theme === themes.default ? MenuSection : MenuNew.Section;
146
+
147
+ return <Section {...props} />;
148
+ };
149
+
150
+ const MenuItemBridge = ({
151
+ secondary,
152
+ externalLink,
153
+ trailingContent,
154
+ ...props
155
+ }: MenuItemProps & PrimaryMenuItemProps & SecondaryMenuItemProps) => {
156
+ const [theme] = useTheme();
157
+
158
+ if (theme === themes.default) {
159
+ return (
160
+ <MenuItem
161
+ secondary={secondary}
162
+ externalLink={externalLink}
163
+ {...props}
164
+ />
165
+ );
166
+ }
167
+
168
+ if (secondary) {
169
+ return (
170
+ <MenuNew.SecondaryItem
171
+ externalLink={externalLink}
172
+ {...props}
173
+ />
174
+ );
175
+ }
176
+
177
+ const resolvedTrailingContent =
178
+ trailingContent ??
179
+ (externalLink ? (
180
+ <Icon.ExternalLink
181
+ size={16}
182
+ aria-hidden='true'
183
+ focusable='false'
184
+ />
185
+ ) : undefined);
186
+
187
+ return (
188
+ <MenuNew.PrimaryItem
189
+ trailingContent={resolvedTrailingContent}
190
+ {...props}
191
+ />
192
+ );
193
+ };
194
+
195
+ interface MenuComponent extends NamedExoticComponent<MenuProps> {
196
+ Section: typeof MenuSectionBridge;
197
+ Item: typeof MenuItemBridge;
198
+ Head: typeof MenuNew.Head;
199
+ Heading: typeof MenuNew.Heading;
200
+ Accordion: typeof MenuNew.Accordion;
201
+ Divider: typeof MenuNew.Divider;
202
+ BaseItem: typeof MenuNew.BaseItem;
203
+ PrimaryItem: typeof MenuNew.PrimaryItem;
204
+ SecondaryItem: typeof MenuNew.SecondaryItem;
205
+ ActionButton: typeof MenuNew.ActionButton;
206
+ }
207
+
208
+ const MemoMenu = memo(Menu);
63
209
 
64
- MenuWithSubComponents.Section = MenuSection;
65
- MenuWithSubComponents.Item = MenuItem;
210
+ const MenuWithSubComponents = Object.assign(MemoMenu, {
211
+ Section: MenuSectionBridge,
212
+ Item: MenuItemBridge,
213
+ Head: MenuNew.Head,
214
+ Heading: MenuNew.Heading,
215
+ Accordion: MenuNew.Accordion,
216
+ Divider: MenuNew.Divider,
217
+ BaseItem: MenuNew.BaseItem,
218
+ PrimaryItem: MenuNew.PrimaryItem,
219
+ SecondaryItem: MenuNew.SecondaryItem,
220
+ ActionButton: MenuNew.ActionButton,
221
+ }) as MenuComponent;
66
222
 
67
223
  export default MenuWithSubComponents;
@@ -1,8 +1,15 @@
1
1
  export { default } from './Menu';
2
+ export { LegacyMenu } from './Menu';
2
3
  export type { MenuProps } from './Menu';
4
+ /** @deprecated Use MenuProps instead. LegacyMenuProps only supports the default theme. */
5
+ export type { LegacyMenuProps } from './Menu';
3
6
 
7
+ /** @deprecated Use Menu.PrimaryItem/Menu.SecondaryItem from the package root instead. */
4
8
  export { default as MenuTrigger } from './MenuTrigger';
9
+ /** @deprecated Use MenuNew component props from the package root instead. */
5
10
  export type { MenuTriggerProps } from './MenuTrigger';
6
11
 
12
+ /** @deprecated Use Menu.Head/Menu.Section from the package root instead. */
7
13
  export { default as MenuContent } from './MenuContent';
14
+ /** @deprecated Use MenuNew component props from the package root instead. */
8
15
  export type { MenuContentProps } from './MenuContent';
@@ -1,6 +1,6 @@
1
1
  import { css, cx } from '@emotion/css';
2
2
  import Button, { ButtonProps } from '../Button';
3
- import { useTheme } from '../..';
3
+ import { useTheme } from '../../theme';
4
4
 
5
5
  export const NAME = 'ucl-uikit-menu__action-button';
6
6
 
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { css } from '@emotion/css';
3
+ import SkipToContent from './SkipToContent';
4
+
5
+ const meta = {
6
+ title: 'Components/SkipToContent',
7
+ component: SkipToContent,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ argTypes: {
12
+ href: { control: { type: 'text' } },
13
+ children: { control: { type: 'text' } },
14
+ testId: { control: { type: 'text' } },
15
+ className: { control: false },
16
+ },
17
+ args: {
18
+ href: '#main-content',
19
+ children: 'Skip to content',
20
+ },
21
+ tags: ['autodocs'],
22
+ } satisfies Meta<typeof SkipToContent>;
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ export const Default: Story = {
28
+ render: (args) => (
29
+ <>
30
+ <SkipToContent {...args} />
31
+ <div
32
+ className={css`
33
+ padding: 96px 24px 24px;
34
+ font-family: sans-serif;
35
+ `}
36
+ >
37
+ Press Tab to focus the skip link.
38
+ <main
39
+ id='main-content'
40
+ tabIndex={-1}
41
+ >
42
+ Main content target
43
+ </main>
44
+ </div>
45
+ </>
46
+ ),
47
+ };
@@ -0,0 +1,68 @@
1
+ import { AnchorHTMLAttributes, Ref, memo } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import useTheme from '../../theme/useTheme';
4
+ import Button from '../Button';
5
+
6
+ export const NAME = 'ucl-uikit-skip-to-content';
7
+
8
+ export interface SkipToContentProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
9
+ testId?: string;
10
+ ref?: Ref<HTMLAnchorElement>;
11
+ }
12
+
13
+ const SkipToContent = ({
14
+ href = '#main-content',
15
+ testId = NAME,
16
+ className,
17
+ children = 'Skip to content',
18
+ ref,
19
+ ...props
20
+ }: SkipToContentProps) => {
21
+ const [theme] = useTheme();
22
+
23
+ const wrapperStyle = css`
24
+ box-sizing: border-box;
25
+ display: flex;
26
+ flex-direction: column;
27
+ align-items: flex-start;
28
+ justify-content: center;
29
+ gap: ${theme.padding.p8};
30
+ width: 100%;
31
+ max-height: 0;
32
+ padding: 0 ${theme.padding.p16};
33
+ overflow: hidden;
34
+ background-color: ${theme.colour.fill.inverse};
35
+
36
+ &:focus-within {
37
+ max-height: 72px;
38
+ padding-top: ${theme.padding.p12};
39
+ padding-bottom: ${theme.padding.p12};
40
+ }
41
+
42
+ @media screen and (min-width: ${theme.breakpoints.tablet}px) {
43
+ padding-right: 60px;
44
+ padding-left: 60px;
45
+ }
46
+ `;
47
+
48
+ const style = cx(NAME, className);
49
+
50
+ return (
51
+ <div className={wrapperStyle}>
52
+ <Button
53
+ as='a'
54
+ variant='secondary'
55
+ size='small'
56
+ href={href}
57
+ className={style}
58
+ testId={testId}
59
+ ref={ref}
60
+ {...props}
61
+ >
62
+ {children}
63
+ </Button>
64
+ </div>
65
+ );
66
+ };
67
+
68
+ export default memo(SkipToContent);
@@ -0,0 +1,38 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { ThemeContextProvider } from '../../../theme/useTheme';
4
+ import SkipToContent from '../SkipToContent';
5
+
6
+ const wrap = (ui: React.ReactElement) =>
7
+ render(<ThemeContextProvider>{ui}</ThemeContextProvider>);
8
+
9
+ describe('SkipToContent', () => {
10
+ test('renders with default text and href', () => {
11
+ wrap(<SkipToContent />);
12
+
13
+ const link = screen.getByRole('link', { name: 'Skip to content' });
14
+
15
+ expect(link).toBeInTheDocument();
16
+ expect(link).toHaveAttribute('href', '#main-content');
17
+ });
18
+
19
+ test('accepts custom text and href', () => {
20
+ wrap(<SkipToContent href='#content'>Skip to main content</SkipToContent>);
21
+
22
+ const link = screen.getByRole('link', { name: 'Skip to main content' });
23
+
24
+ expect(link).toHaveAttribute('href', '#content');
25
+ });
26
+
27
+ test('applies custom test id and className', () => {
28
+ const { getByTestId } = wrap(
29
+ <SkipToContent
30
+ testId='custom-skip-link'
31
+ className='custom-class'
32
+ />
33
+ );
34
+
35
+ expect(getByTestId('custom-skip-link')).toBeInTheDocument();
36
+ expect(getByTestId('custom-skip-link').classList).toContain('custom-class');
37
+ });
38
+ });
@@ -0,0 +1,2 @@
1
+ export { default } from './SkipToContent';
2
+ export type { SkipToContentProps } from './SkipToContent';
@@ -119,6 +119,10 @@ export type { AppMenuProps } from './AppMenu';
119
119
  export { default as Menu } from './Menu';
120
120
  export type { MenuProps } from './Menu';
121
121
 
122
+ /** @deprecated Use Menu instead. MenuLegacy only supports the default theme. */
123
+ export { LegacyMenu as MenuLegacy } from './Menu';
124
+ export type { LegacyMenuProps as MenuLegacyProps } from './Menu';
125
+
122
126
  export { default as MenuNew } from './MenuNew';
123
127
  export type { MenuProps as MenuNewProps } from './MenuNew';
124
128
 
@@ -213,6 +217,9 @@ export type { CookieNoticeProps } from './CookieNotice';
213
217
  export { default as Search } from './Search';
214
218
  export type { SearchProps } from './Search';
215
219
 
220
+ export { default as SkipToContent } from './SkipToContent';
221
+ export type { SkipToContentProps } from './SkipToContent';
222
+
216
223
  export { default as Dropdown } from './Dropdown';
217
224
  export type {
218
225
  DropdownProps,
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.42.0",
5
+ "version": "0.43.1",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",