uikit-react-public 0.50.1 → 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.
- package/dist/components/Alert/Alert.d.ts +4 -2
- package/dist/components/Alert/Alert.stories.d.ts +10 -1
- package/dist/components/Button/Button.d.ts +8 -2
- package/dist/components/Button/Button.stories.d.ts +20 -2
- package/dist/components/Button/style/buttonPrimarySuccessStyle.d.ts +4 -0
- package/dist/index.js +3372 -3325
- package/lib/components/Alert/Alert.mdx +13 -1
- package/lib/components/Alert/Alert.stories.tsx +29 -0
- package/lib/components/Alert/Alert.tsx +32 -1
- package/lib/components/Alert/__tests__/Alert.test.tsx +78 -24
- package/lib/components/Button/Button.mdx +46 -10
- package/lib/components/Button/Button.stories.tsx +26 -0
- package/lib/components/Button/Button.tsx +27 -8
- package/lib/components/Button/__tests__/Button.test.tsx +39 -0
- package/lib/components/Button/style/buttonPrimarySuccessStyle.ts +39 -0
- package/package.json +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import * as AlertStories from './Alert.stories';
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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,9 +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>`,
|
|
16
17
|
destructiveNoPadding: `<Button variant='tertiary-destructive-no-padding'>Destructive Action</Button>`,
|
|
17
18
|
onDark: `<Button variant='secondary' onLight={false}>Secondary Button</Button>`,
|
|
18
19
|
withIcon: `<Button icon={<Icon.AlertTriangle />}>Button</Button>`,
|
|
20
|
+
withTwoIcons: `<Button leftIcon={<Icon.ChevronRight />} rightIcon={<Icon.ChevronRight />}>Action</Button>`,
|
|
19
21
|
loading: `<Button loading>Loading Button</Button>`,
|
|
20
22
|
link: `<Button as='a' href='https://www.ucl.ac.uk' target='_blank'>Go to UCL's website</Button>`,
|
|
21
23
|
fullWidth: `<Button fullWidth>Full Width Button</Button>`,
|
|
@@ -24,25 +26,40 @@ export const usage = {
|
|
|
24
26
|
<Meta of={ButtonStories} />
|
|
25
27
|
<Title />
|
|
26
28
|
<Subtitle>
|
|
27
|
-
A polymorphic button component with modern variants and backwards-compatible
|
|
29
|
+
A polymorphic button component with modern variants and backwards-compatible
|
|
30
|
+
legacy props
|
|
28
31
|
</Subtitle>
|
|
29
32
|
|
|
30
33
|
<Description>
|
|
31
|
-
`Button` supports the richer variant set and still supports legacy props such
|
|
34
|
+
`Button` supports the richer variant set and still supports legacy props such
|
|
35
|
+
as `destructive` and `size='default'` for compatibility.
|
|
32
36
|
</Description>
|
|
33
37
|
|
|
34
|
-
<Canvas
|
|
38
|
+
<Canvas
|
|
39
|
+
of={ButtonStories.Default}
|
|
40
|
+
source={{ code: usage.default }}
|
|
41
|
+
/>
|
|
35
42
|
|
|
36
43
|
## Variants
|
|
37
44
|
|
|
38
45
|
Use `variant` to control hierarchy and emphasis.
|
|
39
46
|
|
|
40
47
|
<Canvas of={ButtonStories.Primary} />
|
|
41
|
-
<Canvas
|
|
48
|
+
<Canvas
|
|
49
|
+
of={ButtonStories.Secondary}
|
|
50
|
+
source={{ code: usage.secondary }}
|
|
51
|
+
/>
|
|
42
52
|
<Canvas of={ButtonStories.Tertiary} />
|
|
43
53
|
<Canvas of={ButtonStories.Accent} />
|
|
44
54
|
<Canvas of={ButtonStories.PrimarySubtle} />
|
|
45
|
-
<Canvas
|
|
55
|
+
<Canvas
|
|
56
|
+
of={ButtonStories.Success}
|
|
57
|
+
source={{ code: usage.success }}
|
|
58
|
+
/>
|
|
59
|
+
<Canvas
|
|
60
|
+
of={ButtonStories.Destructive}
|
|
61
|
+
source={{ code: usage.destructive }}
|
|
62
|
+
/>
|
|
46
63
|
<Canvas
|
|
47
64
|
of={ButtonStories.DestructiveTertiaryNoPadding}
|
|
48
65
|
source={{ code: usage.destructiveNoPadding }}
|
|
@@ -52,7 +69,10 @@ Use `variant` to control hierarchy and emphasis.
|
|
|
52
69
|
|
|
53
70
|
Set `onLight={false}` when a supported inverse variant is placed directly on a dark background.
|
|
54
71
|
|
|
55
|
-
<Canvas
|
|
72
|
+
<Canvas
|
|
73
|
+
of={ButtonStories.OnDark}
|
|
74
|
+
source={{ code: usage.onDark }}
|
|
75
|
+
/>
|
|
56
76
|
|
|
57
77
|
## Sizes
|
|
58
78
|
|
|
@@ -62,20 +82,36 @@ Set `onLight={false}` when a supported inverse variant is placed directly on a d
|
|
|
62
82
|
## States
|
|
63
83
|
|
|
64
84
|
<Canvas of={ButtonStories.Disabled} />
|
|
65
|
-
<Canvas
|
|
85
|
+
<Canvas
|
|
86
|
+
of={ButtonStories.Loading}
|
|
87
|
+
source={{ code: usage.loading }}
|
|
88
|
+
/>
|
|
66
89
|
|
|
67
90
|
## Icons
|
|
68
91
|
|
|
69
|
-
<Canvas
|
|
92
|
+
<Canvas
|
|
93
|
+
of={ButtonStories.WithIcon}
|
|
94
|
+
source={{ code: usage.withIcon }}
|
|
95
|
+
/>
|
|
96
|
+
<Canvas
|
|
97
|
+
of={ButtonStories.WithTwoIcons}
|
|
98
|
+
source={{ code: usage.withTwoIcons }}
|
|
99
|
+
/>
|
|
70
100
|
<Canvas of={ButtonStories.IconOnly} />
|
|
71
101
|
|
|
72
102
|
## Polymorphism
|
|
73
103
|
|
|
74
|
-
<Canvas
|
|
104
|
+
<Canvas
|
|
105
|
+
of={ButtonStories.Link}
|
|
106
|
+
source={{ code: usage.link }}
|
|
107
|
+
/>
|
|
75
108
|
|
|
76
109
|
## Layout
|
|
77
110
|
|
|
78
|
-
<Canvas
|
|
111
|
+
<Canvas
|
|
112
|
+
of={ButtonStories.FullWidth}
|
|
113
|
+
source={{ code: usage.fullWidth }}
|
|
114
|
+
/>
|
|
79
115
|
|
|
80
116
|
## Common Margins
|
|
81
117
|
|
|
@@ -35,6 +35,7 @@ const variants = [
|
|
|
35
35
|
'tertiary-destructive',
|
|
36
36
|
'tertiary-destructive-no-padding',
|
|
37
37
|
'primary-warning',
|
|
38
|
+
'primary-success',
|
|
38
39
|
] as const;
|
|
39
40
|
|
|
40
41
|
const meta = {
|
|
@@ -99,6 +100,14 @@ const meta = {
|
|
|
99
100
|
type: { summary: 'string', detail: "'left' | 'right'" },
|
|
100
101
|
},
|
|
101
102
|
},
|
|
103
|
+
leftIcon: {
|
|
104
|
+
control: false,
|
|
105
|
+
table: { type: { summary: 'ReactElement' } },
|
|
106
|
+
},
|
|
107
|
+
rightIcon: {
|
|
108
|
+
control: false,
|
|
109
|
+
table: { type: { summary: 'ReactElement' } },
|
|
110
|
+
},
|
|
102
111
|
children: {
|
|
103
112
|
control: { type: 'text' },
|
|
104
113
|
type: 'string',
|
|
@@ -208,6 +217,14 @@ export const DestructiveTertiaryNoPadding: Story = {
|
|
|
208
217
|
},
|
|
209
218
|
};
|
|
210
219
|
|
|
220
|
+
export const Success: Story = {
|
|
221
|
+
name: 'Success primary',
|
|
222
|
+
args: {
|
|
223
|
+
variant: 'primary-success',
|
|
224
|
+
children: 'Success button',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
211
228
|
export const OnDark: Story = {
|
|
212
229
|
name: 'Inverse variants on dark',
|
|
213
230
|
args: {
|
|
@@ -344,6 +361,15 @@ export const IconOnly: Story & {
|
|
|
344
361
|
},
|
|
345
362
|
};
|
|
346
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
|
+
|
|
347
373
|
export const Loading: Story = {
|
|
348
374
|
name: 'Loading',
|
|
349
375
|
args: {
|
|
@@ -22,6 +22,7 @@ import buttonSecondaryDestructiveStyle from './style/buttonSecondaryDestructiveS
|
|
|
22
22
|
import buttonTertiaryDestructiveStyle from './style/buttonTertiaryDestructiveStyle';
|
|
23
23
|
import buttonTertiaryDestructiveNoPaddingStyle from './style/buttonTertiaryDestructiveNoPaddingStyle';
|
|
24
24
|
import buttonPrimaryWarningStyle from './style/buttonPrimaryWarningStyle';
|
|
25
|
+
import buttonPrimarySuccessStyle from './style/buttonPrimarySuccessStyle';
|
|
25
26
|
import buttonPrimarySubtleStyle from './style/buttonPrimarySubtleStyle';
|
|
26
27
|
import buttonSecondarySubtleStyle from './style/buttonSecondarySubtleStyle';
|
|
27
28
|
import buttonTertiaryNoPaddingStyle from './style/buttonTertiaryNoPaddingStyle';
|
|
@@ -42,7 +43,8 @@ export type ButtonVariant =
|
|
|
42
43
|
| 'secondary-destructive'
|
|
43
44
|
| 'tertiary-destructive'
|
|
44
45
|
| 'tertiary-destructive-no-padding'
|
|
45
|
-
| 'primary-warning'
|
|
46
|
+
| 'primary-warning'
|
|
47
|
+
| 'primary-success';
|
|
46
48
|
|
|
47
49
|
const DESTRUCTIVE_VARIANT_MAP: Partial<Record<ButtonVariant, ButtonVariant>> = {
|
|
48
50
|
primary: 'primary-destructive',
|
|
@@ -65,6 +67,8 @@ export interface ButtonBaseProps {
|
|
|
65
67
|
disabled?: boolean;
|
|
66
68
|
icon?: ReactElement<{ className?: string }>;
|
|
67
69
|
iconPosition?: 'left' | 'right';
|
|
70
|
+
leftIcon?: ReactElement<{ className?: string }>;
|
|
71
|
+
rightIcon?: ReactElement<{ className?: string }>;
|
|
68
72
|
tooltip?: string;
|
|
69
73
|
loading?: boolean;
|
|
70
74
|
fullWidth?: boolean;
|
|
@@ -116,6 +120,8 @@ const Button = <C extends ElementType = 'button'>({
|
|
|
116
120
|
disabled = false,
|
|
117
121
|
icon,
|
|
118
122
|
iconPosition = 'left',
|
|
123
|
+
leftIcon,
|
|
124
|
+
rightIcon,
|
|
119
125
|
tooltip,
|
|
120
126
|
loading = false,
|
|
121
127
|
fullWidth = false,
|
|
@@ -185,11 +191,18 @@ const Button = <C extends ElementType = 'button'>({
|
|
|
185
191
|
width: ${children ? contentIconSize : iconOnlySize}px;
|
|
186
192
|
height: ${children ? contentIconSize : iconOnlySize}px;
|
|
187
193
|
`;
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
194
|
+
const styleIcon = (iconElement?: ReactElement<{ className?: string }>) =>
|
|
195
|
+
iconElement
|
|
196
|
+
? cloneElement(iconElement, {
|
|
197
|
+
className: cx(iconElement.props.className, iconStyle),
|
|
198
|
+
})
|
|
199
|
+
: undefined;
|
|
200
|
+
const styledLeftIcon = styleIcon(
|
|
201
|
+
leftIcon ?? (iconPosition === 'left' ? icon : undefined)
|
|
202
|
+
);
|
|
203
|
+
const styledRightIcon = styleIcon(
|
|
204
|
+
rightIcon ?? (iconPosition === 'right' ? icon : undefined)
|
|
205
|
+
);
|
|
193
206
|
|
|
194
207
|
const sizeExtraSmallStyle = css`
|
|
195
208
|
height: 32px;
|
|
@@ -273,6 +286,12 @@ const Button = <C extends ElementType = 'button'>({
|
|
|
273
286
|
disabled,
|
|
274
287
|
loading
|
|
275
288
|
),
|
|
289
|
+
'primary-success': buttonPrimarySuccessStyle(
|
|
290
|
+
theme,
|
|
291
|
+
selected,
|
|
292
|
+
disabled,
|
|
293
|
+
loading
|
|
294
|
+
),
|
|
276
295
|
}[resolvedVariant];
|
|
277
296
|
|
|
278
297
|
const commonDisabledStyle = css`
|
|
@@ -323,9 +342,9 @@ const Button = <C extends ElementType = 'button'>({
|
|
|
323
342
|
/>
|
|
324
343
|
) : (
|
|
325
344
|
<>
|
|
326
|
-
{
|
|
345
|
+
{styledLeftIcon}
|
|
327
346
|
{children}
|
|
328
|
-
{
|
|
347
|
+
{styledRightIcon}
|
|
329
348
|
</>
|
|
330
349
|
)}
|
|
331
350
|
</Component>
|
|
@@ -79,6 +79,25 @@ describe('Button', () => {
|
|
|
79
79
|
expect(renderResult.container.firstChild).toMatchSnapshot();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
test('renders icons on both sides of the label', () => {
|
|
83
|
+
render(
|
|
84
|
+
<ThemeContextProvider>
|
|
85
|
+
<Button
|
|
86
|
+
leftIcon={<Icon.ChevronRight />}
|
|
87
|
+
rightIcon={<Icon.ChevronRight />}
|
|
88
|
+
size='xsmall'
|
|
89
|
+
>
|
|
90
|
+
Action
|
|
91
|
+
</Button>
|
|
92
|
+
</ThemeContextProvider>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const icons = screen.getAllByTestId('ucl-uikit-icon');
|
|
96
|
+
expect(icons).toHaveLength(2);
|
|
97
|
+
expect(icons[0]).toHaveStyle({ width: '16px', height: '16px' });
|
|
98
|
+
expect(icons[1]).toHaveStyle({ width: '16px', height: '16px' });
|
|
99
|
+
});
|
|
100
|
+
|
|
82
101
|
test('snapshot: loading', () => {
|
|
83
102
|
const renderResult = render(
|
|
84
103
|
<ThemeContextProvider>
|
|
@@ -309,6 +328,7 @@ describe('Button', () => {
|
|
|
309
328
|
['accent', darkTheme.colour.text.inverse],
|
|
310
329
|
['primary-destructive', darkTheme.colour.text.inverse],
|
|
311
330
|
['primary-warning', darkTheme.colour.text.caution],
|
|
331
|
+
['primary-success', darkTheme.colour.text.inverse],
|
|
312
332
|
] as const)(
|
|
313
333
|
'uses readable semantic text colour for dark %s variant',
|
|
314
334
|
(variant, expectedColour) => {
|
|
@@ -341,6 +361,25 @@ describe('Button', () => {
|
|
|
341
361
|
);
|
|
342
362
|
});
|
|
343
363
|
|
|
364
|
+
test('uses the inverse icon colour for the success variant', () => {
|
|
365
|
+
render(
|
|
366
|
+
<ThemeContextProvider>
|
|
367
|
+
<Button
|
|
368
|
+
variant='primary-success'
|
|
369
|
+
icon={<Icon.ChevronRight />}
|
|
370
|
+
>
|
|
371
|
+
Success
|
|
372
|
+
</Button>
|
|
373
|
+
</ThemeContextProvider>
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
expect(
|
|
377
|
+
getComputedStyle(screen.getByRole('button'))
|
|
378
|
+
.getPropertyValue('--ucl-uikit-button-icon-colour')
|
|
379
|
+
.trim()
|
|
380
|
+
).toBe(theme.colour.icon.inverse);
|
|
381
|
+
});
|
|
382
|
+
|
|
344
383
|
test.each([
|
|
345
384
|
['xsmall', '32px', '16px'],
|
|
346
385
|
['small', '40px', '20px'],
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { css, cx } from '@emotion/css';
|
|
2
|
+
import { ThemeType } from '../../../theme';
|
|
3
|
+
import { ButtonBaseProps } from '../Button';
|
|
4
|
+
|
|
5
|
+
const buttonPrimarySuccessStyle = (
|
|
6
|
+
theme: ThemeType,
|
|
7
|
+
selected: ButtonBaseProps['selected'],
|
|
8
|
+
disabled: ButtonBaseProps['disabled'],
|
|
9
|
+
loading: ButtonBaseProps['loading']
|
|
10
|
+
) => {
|
|
11
|
+
const baseStyle = css`
|
|
12
|
+
--ucl-uikit-button-icon-colour: ${theme.colour.icon.inverse};
|
|
13
|
+
background-color: ${theme.colour.fill.success};
|
|
14
|
+
color: ${theme.colour.text.inverse};
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const interactiveStyle = css`
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const selectedStyle = css`
|
|
22
|
+
background-color: ${theme.colour.fill.success};
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const disabledStyle = css`
|
|
26
|
+
--ucl-uikit-button-icon-colour: ${theme.colour.icon.disabledOnBgFill};
|
|
27
|
+
background-color: ${theme.colour.fill.disabled};
|
|
28
|
+
color: ${theme.colour.text.onBgFillDisabled};
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
return cx(
|
|
32
|
+
baseStyle,
|
|
33
|
+
selected && selectedStyle,
|
|
34
|
+
!disabled && !loading && interactiveStyle,
|
|
35
|
+
disabled && disabledStyle
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default buttonPrimarySuccessStyle;
|