uikit-react-public 0.50.0 → 0.50.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/Alert/Alert.d.ts +4 -2
  2. package/dist/components/Alert/Alert.stories.d.ts +10 -1
  3. package/dist/components/Button/Button.d.ts +14 -4
  4. package/dist/components/Button/Button.stories.d.ts +29 -2
  5. package/dist/components/Button/style/buttonPrimarySubtleStyle.d.ts +1 -1
  6. package/dist/components/Button/style/buttonPrimarySuccessStyle.d.ts +4 -0
  7. package/dist/components/Button/style/buttonSecondaryStyle.d.ts +1 -1
  8. package/dist/components/Button/style/buttonSecondarySubtleStyle.d.ts +1 -1
  9. package/dist/components/Button/style/buttonTertiaryDestructiveNoPaddingStyle.d.ts +4 -0
  10. package/dist/components/Button/style/buttonTertiaryDestructiveStyle.d.ts +1 -1
  11. package/dist/components/Button/style/buttonTertiaryNoPaddingStyle.d.ts +1 -1
  12. package/dist/components/Button/style/buttonTertiaryStyle.d.ts +1 -1
  13. package/dist/index.js +4485 -4373
  14. package/lib/components/Alert/Alert.mdx +13 -1
  15. package/lib/components/Alert/Alert.stories.tsx +29 -0
  16. package/lib/components/Alert/Alert.tsx +32 -1
  17. package/lib/components/Alert/__tests__/Alert.test.tsx +78 -24
  18. package/lib/components/Button/Button.mdx +57 -9
  19. package/lib/components/Button/Button.stories.tsx +72 -0
  20. package/lib/components/Button/Button.tsx +68 -26
  21. package/lib/components/Button/__tests__/Button.test.tsx +159 -1
  22. package/lib/components/Button/__tests__/__snapshots__/Button.test.tsx.snap +11 -11
  23. package/lib/components/Button/style/buttonAccentStyle.ts +2 -0
  24. package/lib/components/Button/style/buttonPrimaryDestructiveStyle.ts +2 -0
  25. package/lib/components/Button/style/buttonPrimaryStyle.ts +2 -0
  26. package/lib/components/Button/style/buttonPrimarySubtleStyle.ts +10 -11
  27. package/lib/components/Button/style/buttonPrimarySuccessStyle.ts +39 -0
  28. package/lib/components/Button/style/buttonPrimaryWarningStyle.ts +2 -0
  29. package/lib/components/Button/style/buttonSecondaryDestructiveStyle.ts +13 -7
  30. package/lib/components/Button/style/buttonSecondaryStyle.ts +49 -16
  31. package/lib/components/Button/style/buttonSecondarySubtleStyle.ts +15 -13
  32. package/lib/components/Button/style/buttonTertiaryDestructiveNoPaddingStyle.ts +34 -0
  33. package/lib/components/Button/style/buttonTertiaryDestructiveStyle.ts +9 -8
  34. package/lib/components/Button/style/buttonTertiaryNoPaddingStyle.ts +13 -15
  35. package/lib/components/Button/style/buttonTertiaryStyle.ts +10 -9
  36. package/lib/components/Table/subcomponents/Cell/__tests__/__snapshots__/Cell.test.tsx.snap +1 -1
  37. package/package.json +1 -1
@@ -1,5 +1,11 @@
1
1
  import * as AlertStories from './Alert.stories';
2
- import { Meta, Title, Subtitle, Canvas, Controls } from '@storybook/addon-docs/blocks';
2
+ import {
3
+ Meta,
4
+ Title,
5
+ Subtitle,
6
+ Canvas,
7
+ Controls,
8
+ } from '@storybook/addon-docs/blocks';
3
9
 
4
10
  <Meta of={AlertStories} />
5
11
  <Title />
@@ -14,6 +20,12 @@ The `<Alert>` component is used to convey contextual messages with visual hierar
14
20
 
15
21
  Use the component with `Alert.Title` and `Alert.Message` as children to maintain semantic separation of content.
16
22
 
23
+ ## Action button
24
+
25
+ Pass a `Button` to `actionButton` to add an action. Alert sets the button to the extra-small size and applies the colour associated with the alert type.
26
+
27
+ <Canvas of={AlertStories.WithActionButton} />
28
+
17
29
  ## Variants
18
30
 
19
31
  ### Info
@@ -1,5 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import Alert from './Alert';
3
+ import Button from '../Button';
4
+ import Icon from '../Icon';
3
5
  import { theme } from '../../theme';
4
6
 
5
7
  const marginKeys = Object.keys(theme.margin) as Array<
@@ -35,6 +37,10 @@ const meta = {
35
37
  className: { control: { type: 'text' } },
36
38
  testId: { control: { type: 'text' } },
37
39
  children: { control: false },
40
+ actionButton: {
41
+ control: false,
42
+ table: { type: { summary: 'ReactElement<ButtonProps>' } },
43
+ },
38
44
  m: commonMarginPropsInfo,
39
45
  mv: commonMarginPropsInfo,
40
46
  mh: commonMarginPropsInfo,
@@ -98,6 +104,29 @@ export const Error: Story = {
98
104
  },
99
105
  };
100
106
 
107
+ export const WithActionButton: Story = {
108
+ name: 'With action button',
109
+ render: (args) => (
110
+ <Alert
111
+ {...args}
112
+ actionButton={
113
+ <Button
114
+ leftIcon={<Icon.ChevronRight />}
115
+ rightIcon={<Icon.ChevronRight />}
116
+ >
117
+ Action
118
+ </Button>
119
+ }
120
+ >
121
+ <Alert.Title>Title of {args.type} alert</Alert.Title>
122
+ <Alert.Message>This is a {args.type} alert message.</Alert.Message>
123
+ </Alert>
124
+ ),
125
+ args: {
126
+ type: 'info',
127
+ },
128
+ };
129
+
101
130
  export const CommonMargins: Story = {
102
131
  render: Template,
103
132
  name: 'With common margins',
@@ -1,6 +1,13 @@
1
- import { createContext, useContext, HTMLAttributes } from 'react';
1
+ import {
2
+ cloneElement,
3
+ createContext,
4
+ useContext,
5
+ HTMLAttributes,
6
+ ReactElement,
7
+ } from 'react';
2
8
  import { css, cx } from '@emotion/css';
3
9
  import Icon, { IconProps } from '../Icon';
10
+ import type { ButtonProps, ButtonVariant } from '../Button/Button';
4
11
  import AlertMessage from './AlertMessage';
5
12
  import AlertTitle from './AlertTitle';
6
13
  import marginsStyle, { MarginProps } from '../common/marginsStyle';
@@ -29,6 +36,7 @@ export interface AlertBaseProps extends HTMLAttributes<HTMLDivElement> {
29
36
  type: AlertType;
30
37
  iconProps?: Omit<IconProps, 'svg'>;
31
38
  contentProps?: HTMLAttributes<HTMLDivElement>;
39
+ actionButton?: ReactElement<ButtonProps>;
32
40
  testId?: string;
33
41
  }
34
42
 
@@ -38,6 +46,7 @@ const Alert = ({
38
46
  type,
39
47
  iconProps,
40
48
  contentProps,
49
+ actionButton,
41
50
  children,
42
51
  testId = NAME,
43
52
  className,
@@ -114,6 +123,12 @@ const Alert = ({
114
123
  overflow-wrap: anywhere;
115
124
  `;
116
125
 
126
+ const actionButtonStyle = css`
127
+ align-self: center;
128
+ flex-shrink: 0;
129
+ margin-left: auto;
130
+ `;
131
+
117
132
  const IconComp = {
118
133
  info: Icon.Info,
119
134
  success: Icon.CheckCircle,
@@ -121,6 +136,21 @@ const Alert = ({
121
136
  error: Icon.XCircle,
122
137
  }[type];
123
138
 
139
+ const actionButtonVariant: Record<AlertType, ButtonVariant> = {
140
+ info: 'accent',
141
+ success: 'primary-success',
142
+ warning: 'primary-warning',
143
+ error: 'primary-destructive',
144
+ };
145
+
146
+ const renderedActionButton = actionButton
147
+ ? cloneElement(actionButton, {
148
+ variant: actionButtonVariant[type],
149
+ size: 'xsmall',
150
+ className: cx(actionButtonStyle, actionButton.props.className),
151
+ })
152
+ : null;
153
+
124
154
  return (
125
155
  <AlertContext.Provider value={{ type }}>
126
156
  <div
@@ -140,6 +170,7 @@ const Alert = ({
140
170
  >
141
171
  {children}
142
172
  </div>
173
+ {renderedActionButton}
143
174
  </div>
144
175
  </AlertContext.Provider>
145
176
  );
@@ -1,7 +1,19 @@
1
- import { describe, expect, test } from 'vitest';
2
- import { render } from '@testing-library/react';
3
- import { ThemeContextProvider } from '../../../theme/useTheme';
1
+ import { describe, expect, test, vitest } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { theme, ThemeContextProvider } from '../../../theme';
4
5
  import Alert from '../Alert';
6
+ import Button from '../../Button';
7
+ import Icon from '../../Icon';
8
+
9
+ const hexToRgb = (hex: string) => {
10
+ const value = hex.replace('#', '');
11
+ const red = parseInt(value.slice(0, 2), 16);
12
+ const green = parseInt(value.slice(2, 4), 16);
13
+ const blue = parseInt(value.slice(4, 6), 16);
14
+
15
+ return `rgb(${red}, ${green}, ${blue})`;
16
+ };
5
17
 
6
18
  describe('Alert', () => {
7
19
  // Snapshot tests
@@ -10,15 +22,11 @@ describe('Alert', () => {
10
22
  const renderResult = render(
11
23
  <ThemeContextProvider>
12
24
  <Alert type='info'>
13
- <Alert.Message>
14
- This is an informational alert.
15
- </Alert.Message>
25
+ <Alert.Message>This is an informational alert.</Alert.Message>
16
26
  </Alert>
17
27
  </ThemeContextProvider>
18
28
  );
19
- expect(
20
- renderResult.container.firstChild
21
- ).toMatchSnapshot();
29
+ expect(renderResult.container.firstChild).toMatchSnapshot();
22
30
  });
23
31
 
24
32
  test('snapshot: type=success', () => {
@@ -26,15 +34,11 @@ describe('Alert', () => {
26
34
  <ThemeContextProvider>
27
35
  <Alert type='success'>
28
36
  <Alert.Title>Success</Alert.Title>
29
- <Alert.Message>
30
- Your operation completed successfully.
31
- </Alert.Message>
37
+ <Alert.Message>Your operation completed successfully.</Alert.Message>
32
38
  </Alert>
33
39
  </ThemeContextProvider>
34
40
  );
35
- expect(
36
- renderResult.container.firstChild
37
- ).toMatchSnapshot();
41
+ expect(renderResult.container.firstChild).toMatchSnapshot();
38
42
  });
39
43
 
40
44
  test('snapshot: type=warning', () => {
@@ -48,9 +52,7 @@ describe('Alert', () => {
48
52
  </Alert>
49
53
  </ThemeContextProvider>
50
54
  );
51
- expect(
52
- renderResult.container.firstChild
53
- ).toMatchSnapshot();
55
+ expect(renderResult.container.firstChild).toMatchSnapshot();
54
56
  });
55
57
 
56
58
  test('snapshot: type=error', () => {
@@ -58,14 +60,66 @@ describe('Alert', () => {
58
60
  <ThemeContextProvider>
59
61
  <Alert type='error'>
60
62
  <Alert.Title>Error Alert</Alert.Title>
61
- <Alert.Message>
62
- The operation failed.
63
- </Alert.Message>
63
+ <Alert.Message>The operation failed.</Alert.Message>
64
+ </Alert>
65
+ </ThemeContextProvider>
66
+ );
67
+ expect(renderResult.container.firstChild).toMatchSnapshot();
68
+ });
69
+
70
+ test.each([
71
+ ['info', theme.colour.fill.brand],
72
+ ['success', theme.colour.fill.success],
73
+ ['warning', theme.colour.fill.caution],
74
+ ['error', theme.colour.fill.critical],
75
+ ] as const)(
76
+ 'uses the semantic %s colour for its action button',
77
+ (type, expectedColour) => {
78
+ render(
79
+ <ThemeContextProvider>
80
+ <Alert
81
+ type={type}
82
+ actionButton={<Button>Action</Button>}
83
+ >
84
+ <Alert.Message>Alert message</Alert.Message>
85
+ </Alert>
86
+ </ThemeContextProvider>
87
+ );
88
+
89
+ const actionButton = screen.getByRole('button');
90
+ expect(actionButton).toHaveStyle({
91
+ height: '32px',
92
+ backgroundColor: hexToRgb(expectedColour),
93
+ });
94
+ }
95
+ );
96
+
97
+ test('supports an interactive action button with two icons', async () => {
98
+ const user = userEvent.setup();
99
+ const onClick = vitest.fn();
100
+
101
+ render(
102
+ <ThemeContextProvider>
103
+ <Alert
104
+ type='info'
105
+ actionButton={
106
+ <Button
107
+ leftIcon={<Icon.ChevronRight />}
108
+ rightIcon={<Icon.ChevronRight />}
109
+ onClick={onClick}
110
+ >
111
+ Action
112
+ </Button>
113
+ }
114
+ >
115
+ <Alert.Title>Information</Alert.Title>
116
+ <Alert.Message>Alert message</Alert.Message>
64
117
  </Alert>
65
118
  </ThemeContextProvider>
66
119
  );
67
- expect(
68
- renderResult.container.firstChild
69
- ).toMatchSnapshot();
120
+
121
+ expect(screen.getAllByTestId('ucl-uikit-icon')).toHaveLength(3);
122
+ await user.click(screen.getByRole('button', { name: 'Action' }));
123
+ expect(onClick).toHaveBeenCalledOnce();
70
124
  });
71
125
  });
@@ -13,7 +13,11 @@ export const usage = {
13
13
  default: `<Button>Default Button</Button>`,
14
14
  secondary: `<Button variant='secondary'>Secondary Button</Button>`,
15
15
  destructive: `<Button variant='primary-destructive'>Destructive Button</Button>`,
16
+ success: `<Button variant='primary-success'>Success Button</Button>`,
17
+ destructiveNoPadding: `<Button variant='tertiary-destructive-no-padding'>Destructive Action</Button>`,
18
+ onDark: `<Button variant='secondary' onLight={false}>Secondary Button</Button>`,
16
19
  withIcon: `<Button icon={<Icon.AlertTriangle />}>Button</Button>`,
20
+ withTwoIcons: `<Button leftIcon={<Icon.ChevronRight />} rightIcon={<Icon.ChevronRight />}>Action</Button>`,
17
21
  loading: `<Button loading>Loading Button</Button>`,
18
22
  link: `<Button as='a' href='https://www.ucl.ac.uk' target='_blank'>Go to UCL&#39;s website</Button>`,
19
23
  fullWidth: `<Button fullWidth>Full Width Button</Button>`,
@@ -22,25 +26,53 @@ export const usage = {
22
26
  <Meta of={ButtonStories} />
23
27
  <Title />
24
28
  <Subtitle>
25
- A polymorphic button component with modern variants and backwards-compatible legacy props
29
+ A polymorphic button component with modern variants and backwards-compatible
30
+ legacy props
26
31
  </Subtitle>
27
32
 
28
33
  <Description>
29
- `Button` supports the richer variant set and still supports legacy props such as `destructive` and `size='default'` for compatibility.
34
+ `Button` supports the richer variant set and still supports legacy props such
35
+ as `destructive` and `size='default'` for compatibility.
30
36
  </Description>
31
37
 
32
- <Canvas of={ButtonStories.Default} source={{ code: usage.default }} />
38
+ <Canvas
39
+ of={ButtonStories.Default}
40
+ source={{ code: usage.default }}
41
+ />
33
42
 
34
43
  ## Variants
35
44
 
36
45
  Use `variant` to control hierarchy and emphasis.
37
46
 
38
47
  <Canvas of={ButtonStories.Primary} />
39
- <Canvas of={ButtonStories.Secondary} source={{ code: usage.secondary }} />
48
+ <Canvas
49
+ of={ButtonStories.Secondary}
50
+ source={{ code: usage.secondary }}
51
+ />
40
52
  <Canvas of={ButtonStories.Tertiary} />
41
53
  <Canvas of={ButtonStories.Accent} />
42
54
  <Canvas of={ButtonStories.PrimarySubtle} />
43
- <Canvas of={ButtonStories.Destructive} source={{ code: usage.destructive }} />
55
+ <Canvas
56
+ of={ButtonStories.Success}
57
+ source={{ code: usage.success }}
58
+ />
59
+ <Canvas
60
+ of={ButtonStories.Destructive}
61
+ source={{ code: usage.destructive }}
62
+ />
63
+ <Canvas
64
+ of={ButtonStories.DestructiveTertiaryNoPadding}
65
+ source={{ code: usage.destructiveNoPadding }}
66
+ />
67
+
68
+ ## Dark backgrounds
69
+
70
+ Set `onLight={false}` when a supported inverse variant is placed directly on a dark background.
71
+
72
+ <Canvas
73
+ of={ButtonStories.OnDark}
74
+ source={{ code: usage.onDark }}
75
+ />
44
76
 
45
77
  ## Sizes
46
78
 
@@ -50,20 +82,36 @@ Use `variant` to control hierarchy and emphasis.
50
82
  ## States
51
83
 
52
84
  <Canvas of={ButtonStories.Disabled} />
53
- <Canvas of={ButtonStories.Loading} source={{ code: usage.loading }} />
85
+ <Canvas
86
+ of={ButtonStories.Loading}
87
+ source={{ code: usage.loading }}
88
+ />
54
89
 
55
90
  ## Icons
56
91
 
57
- <Canvas of={ButtonStories.WithIcon} source={{ code: usage.withIcon }} />
92
+ <Canvas
93
+ of={ButtonStories.WithIcon}
94
+ source={{ code: usage.withIcon }}
95
+ />
96
+ <Canvas
97
+ of={ButtonStories.WithTwoIcons}
98
+ source={{ code: usage.withTwoIcons }}
99
+ />
58
100
  <Canvas of={ButtonStories.IconOnly} />
59
101
 
60
102
  ## Polymorphism
61
103
 
62
- <Canvas of={ButtonStories.Link} source={{ code: usage.link }} />
104
+ <Canvas
105
+ of={ButtonStories.Link}
106
+ source={{ code: usage.link }}
107
+ />
63
108
 
64
109
  ## Layout
65
110
 
66
- <Canvas of={ButtonStories.FullWidth} source={{ code: usage.fullWidth }} />
111
+ <Canvas
112
+ of={ButtonStories.FullWidth}
113
+ source={{ code: usage.fullWidth }}
114
+ />
67
115
 
68
116
  ## Common Margins
69
117
 
@@ -33,7 +33,9 @@ const variants = [
33
33
  'primary-destructive',
34
34
  'secondary-destructive',
35
35
  'tertiary-destructive',
36
+ 'tertiary-destructive-no-padding',
36
37
  'primary-warning',
38
+ 'primary-success',
37
39
  ] as const;
38
40
 
39
41
  const meta = {
@@ -80,6 +82,12 @@ const meta = {
80
82
  control: { type: 'boolean' },
81
83
  type: 'boolean',
82
84
  },
85
+ onLight: {
86
+ control: { type: 'boolean' },
87
+ type: 'boolean',
88
+ description:
89
+ 'Set to false for supported inverse variants rendered on a dark background.',
90
+ },
83
91
  disabled: {
84
92
  control: { type: 'boolean' },
85
93
  type: 'boolean',
@@ -92,6 +100,14 @@ const meta = {
92
100
  type: { summary: 'string', detail: "'left' | 'right'" },
93
101
  },
94
102
  },
103
+ leftIcon: {
104
+ control: false,
105
+ table: { type: { summary: 'ReactElement' } },
106
+ },
107
+ rightIcon: {
108
+ control: false,
109
+ table: { type: { summary: 'ReactElement' } },
110
+ },
95
111
  children: {
96
112
  control: { type: 'text' },
97
113
  type: 'string',
@@ -193,6 +209,53 @@ export const Destructive: Story = {
193
209
  },
194
210
  };
195
211
 
212
+ export const DestructiveTertiaryNoPadding: Story = {
213
+ name: 'Destructive tertiary without padding',
214
+ args: {
215
+ variant: 'tertiary-destructive-no-padding',
216
+ children: 'Destructive action',
217
+ },
218
+ };
219
+
220
+ export const Success: Story = {
221
+ name: 'Success primary',
222
+ args: {
223
+ variant: 'primary-success',
224
+ children: 'Success button',
225
+ },
226
+ };
227
+
228
+ export const OnDark: Story = {
229
+ name: 'Inverse variants on dark',
230
+ args: {
231
+ onLight: false,
232
+ },
233
+ render: () => (
234
+ <div
235
+ style={{
236
+ alignItems: 'center',
237
+ background: theme.colour.bg.inversePrimary,
238
+ display: 'flex',
239
+ gap: theme.padding.p24,
240
+ padding: theme.padding.p32,
241
+ }}
242
+ >
243
+ <Button
244
+ onLight={false}
245
+ variant='secondary'
246
+ >
247
+ Secondary action
248
+ </Button>
249
+ <Button
250
+ onLight={false}
251
+ variant='tertiary-no-padding'
252
+ >
253
+ Tertiary action
254
+ </Button>
255
+ </div>
256
+ ),
257
+ };
258
+
196
259
  export const PrimarySubtle: Story = {
197
260
  args: {
198
261
  variant: 'primary-subtle',
@@ -298,6 +361,15 @@ export const IconOnly: Story & {
298
361
  },
299
362
  };
300
363
 
364
+ export const WithTwoIcons: Story = {
365
+ name: 'With two icons',
366
+ args: {
367
+ children: 'Action',
368
+ leftIcon: <Icon.ChevronRight />,
369
+ rightIcon: <Icon.ChevronRight />,
370
+ },
371
+ };
372
+
301
373
  export const Loading: Story = {
302
374
  name: 'Loading',
303
375
  args: {