@automattic/vip-design-system 2.20.1 → 2.20.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.
- package/build/system/NewForm/FormAutocompleteMultiselect.jsx +7 -5
- package/build/system/Wizard/Wizard.d.ts +6 -0
- package/build/system/Wizard/Wizard.js +3 -0
- package/build/system/Wizard/Wizard.stories.d.ts +1 -0
- package/build/system/Wizard/Wizard.stories.js +34 -0
- package/build/system/Wizard/Wizard.test.d.ts +2 -0
- package/build/system/Wizard/Wizard.test.js +119 -0
- package/build/system/Wizard/WizardStep.d.ts +8 -0
- package/build/system/Wizard/WizardStep.js +21 -8
- package/build/system/theme/index.js +10 -3
- package/package.json +1 -1
- package/src/system/NewForm/FormAutocompleteMultiselect.jsx +7 -5
- package/src/system/Wizard/Wizard.stories.tsx +32 -0
- package/src/system/Wizard/Wizard.test.tsx +79 -0
- package/src/system/Wizard/Wizard.tsx +8 -0
- package/src/system/Wizard/WizardStep.tsx +29 -9
- package/src/system/theme/index.ts +7 -0
|
@@ -111,7 +111,7 @@ const inlineChipsContainerStyles = {
|
|
|
111
111
|
flexWrap: 'wrap',
|
|
112
112
|
alignItems: 'center',
|
|
113
113
|
p: 1,
|
|
114
|
-
pr:
|
|
114
|
+
pr: 5,
|
|
115
115
|
position: 'relative',
|
|
116
116
|
'& .autocomplete__input': {
|
|
117
117
|
...defaultStyles[ '& .autocomplete__input' ],
|
|
@@ -125,13 +125,15 @@ const inlineChipsContainerStyles = {
|
|
|
125
125
|
lineHeight: '24px',
|
|
126
126
|
minHeight: '24px',
|
|
127
127
|
'& .autocomplete__dropdown-arrow-down': {
|
|
128
|
-
top: '
|
|
129
|
-
bottom: '
|
|
128
|
+
top: '6px',
|
|
129
|
+
bottom: 'unset',
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
-
const DefaultArrow = config =>
|
|
134
|
+
const DefaultArrow = config => (
|
|
135
|
+
<FormSelectArrow className={ config.className } separator={ false } />
|
|
136
|
+
);
|
|
135
137
|
|
|
136
138
|
const AddSelectionStatus = ( { status } ) => {
|
|
137
139
|
return (
|
|
@@ -446,7 +448,7 @@ const FormAutocompleteMultiselect = React.forwardRef(
|
|
|
446
448
|
unselectValue={ unselectValue }
|
|
447
449
|
/>
|
|
448
450
|
) ) }
|
|
449
|
-
<div sx={ { flex: '1 1 120px', minWidth: '120px' } }>
|
|
451
|
+
<div sx={ { flex: '1 1 120px', minWidth: '120px', mr: -5 } }>
|
|
450
452
|
<Autocomplete
|
|
451
453
|
id={ forLabel }
|
|
452
454
|
aria-busy={ loading }
|
|
@@ -19,6 +19,12 @@ export interface WizardProps {
|
|
|
19
19
|
* @default []
|
|
20
20
|
*/
|
|
21
21
|
skipped?: number[];
|
|
22
|
+
/**
|
|
23
|
+
* Array of zero-based indices for steps that are in an error state. An errored
|
|
24
|
+
* step shows a red error icon, title, and left border (see WizardStep `error`).
|
|
25
|
+
* @default []
|
|
26
|
+
*/
|
|
27
|
+
errored?: number[];
|
|
22
28
|
/** Additional CSS class name for the wizard container. */
|
|
23
29
|
className?: string;
|
|
24
30
|
/**
|
|
@@ -23,6 +23,8 @@ export var Wizard = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef) {
|
|
|
23
23
|
completed = _ref$completed === void 0 ? [] : _ref$completed,
|
|
24
24
|
_ref$skipped = _ref.skipped,
|
|
25
25
|
skipped = _ref$skipped === void 0 ? [] : _ref$skipped,
|
|
26
|
+
_ref$errored = _ref.errored,
|
|
27
|
+
errored = _ref$errored === void 0 ? [] : _ref$errored,
|
|
26
28
|
_ref$className = _ref.className,
|
|
27
29
|
className = _ref$className === void 0 ? null : _ref$className,
|
|
28
30
|
_ref$titleAutofocus = _ref.titleAutofocus,
|
|
@@ -61,6 +63,7 @@ export var Wizard = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef) {
|
|
|
61
63
|
active: index === activeStep,
|
|
62
64
|
complete: completed.includes(index),
|
|
63
65
|
skipped: skipped.includes(index),
|
|
66
|
+
error: errored.includes(index),
|
|
64
67
|
order: index + 1,
|
|
65
68
|
totalSteps: steps.length,
|
|
66
69
|
subTitle: subTitle,
|
|
@@ -12,6 +12,7 @@ declare const _default: {
|
|
|
12
12
|
export default _default;
|
|
13
13
|
type Story = StoryObj<typeof Wizard>;
|
|
14
14
|
export declare const Primary: Story;
|
|
15
|
+
export declare const Error: Story;
|
|
15
16
|
export declare const Default: Story;
|
|
16
17
|
export declare const WithTitleAutoFocus: Story;
|
|
17
18
|
export declare const HideStepText: Story;
|
|
@@ -37,6 +37,40 @@ export var Primary = {
|
|
|
37
37
|
}]
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
+
export var Error = {
|
|
41
|
+
render: function render() {
|
|
42
|
+
var steps = [{
|
|
43
|
+
title: 'Step One',
|
|
44
|
+
titleVariant: 'h3',
|
|
45
|
+
children: _jsxs(Box, {
|
|
46
|
+
children: [_jsx(Text, {
|
|
47
|
+
sx: {
|
|
48
|
+
display: 'block',
|
|
49
|
+
mb: 3,
|
|
50
|
+
color: 'texts.secondary'
|
|
51
|
+
},
|
|
52
|
+
children: "Something went wrong. Please try again."
|
|
53
|
+
}), _jsx(Button, {
|
|
54
|
+
children: "Retry"
|
|
55
|
+
})]
|
|
56
|
+
})
|
|
57
|
+
}, {
|
|
58
|
+
title: 'Step Two',
|
|
59
|
+
titleVariant: 'h3'
|
|
60
|
+
}, {
|
|
61
|
+
title: 'Step Three',
|
|
62
|
+
titleVariant: 'h3'
|
|
63
|
+
}];
|
|
64
|
+
return _jsx(Box, {
|
|
65
|
+
mt: 4,
|
|
66
|
+
children: _jsx(Wizard, {
|
|
67
|
+
activeStep: 0,
|
|
68
|
+
steps: steps,
|
|
69
|
+
errored: [0]
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
40
74
|
export var Default = {
|
|
41
75
|
render: function render() {
|
|
42
76
|
var steps = [{
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
function _regenerator() { /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */ var e, t, r = "function" == typeof Symbol ? Symbol : {}, n = r.iterator || "@@iterator", o = r.toStringTag || "@@toStringTag"; function i(r, n, o, i) { var c = n && n.prototype instanceof Generator ? n : Generator, u = Object.create(c.prototype); return _regeneratorDefine2(u, "_invoke", function (r, n, o) { var i, c, u, f = 0, p = o || [], y = !1, G = { p: 0, n: 0, v: e, a: d, f: d.bind(e, 4), d: function d(t, r) { return i = t, c = 0, u = e, G.n = r, a; } }; function d(r, n) { for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) { var o, i = p[t], d = G.p, l = i[2]; r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0)); } if (o || r > 1) return a; throw y = !0, n; } return function (o, p, l) { if (f > 1) throw TypeError("Generator is already running"); for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) { i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u); try { if (f = 2, i) { if (c || (o = "next"), t = i[o]) { if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object"); if (!t.done) return t; u = t.value, c < 2 && (c = 0); } else 1 === c && (t = i["return"]) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1); i = e; } else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break; } catch (t) { i = e, c = 1, u = t; } finally { f = 1; } } return { value: t, done: y }; }; }(r, o, i), !0), u; } var a = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} t = Object.getPrototypeOf; var c = [][n] ? t(t([][n]())) : (_regeneratorDefine2(t = {}, n, function () { return this; }), t), u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c); function f(e) { return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine2(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e; } return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine2(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine2(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine2(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine2(u), _regeneratorDefine2(u, o, "Generator"), _regeneratorDefine2(u, n, function () { return this; }), _regeneratorDefine2(u, "toString", function () { return "[object Generator]"; }), (_regenerator = function _regenerator() { return { w: i, m: f }; })(); }
|
|
2
|
+
function _regeneratorDefine2(e, r, n, t) { var i = Object.defineProperty; try { i({}, "", {}); } catch (e) { i = 0; } _regeneratorDefine2 = function _regeneratorDefine(e, r, n, t) { function o(r, n) { _regeneratorDefine2(e, r, function (e) { return this._invoke(r, n, e); }); } r ? i ? i(e, r, { value: n, enumerable: !t, configurable: !t, writable: !t }) : e[r] = n : (o("next", 0), o("throw", 1), o("return", 2)); }, _regeneratorDefine2(e, r, n, t); }
|
|
3
|
+
function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
|
|
4
|
+
function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
|
|
5
|
+
/** @jsxImportSource theme-ui */
|
|
6
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
7
|
+
// @ts-nocheck
|
|
8
|
+
|
|
9
|
+
import { render, screen } from '@testing-library/react';
|
|
10
|
+
import { axe } from 'jest-axe';
|
|
11
|
+
import { ThemeUIProvider } from 'theme-ui';
|
|
12
|
+
import { Wizard } from './Wizard';
|
|
13
|
+
import { theme } from '../';
|
|
14
|
+
import { jsx as _jsx } from "theme-ui/jsx-runtime";
|
|
15
|
+
jest.mock('@theme-ui/match-media');
|
|
16
|
+
var renderWithTheme = function renderWithTheme(children) {
|
|
17
|
+
return render(_jsx(ThemeUIProvider, {
|
|
18
|
+
theme: theme,
|
|
19
|
+
children: children
|
|
20
|
+
}));
|
|
21
|
+
};
|
|
22
|
+
var steps = [{
|
|
23
|
+
title: 'Salesforce Domain',
|
|
24
|
+
children: _jsx("div", {
|
|
25
|
+
children: "Domain content"
|
|
26
|
+
})
|
|
27
|
+
}, {
|
|
28
|
+
title: 'Install Packages'
|
|
29
|
+
}, {
|
|
30
|
+
title: 'Connect to Salesforce'
|
|
31
|
+
}];
|
|
32
|
+
describe('<Wizard /> error state', function () {
|
|
33
|
+
it('marks an errored step with the error status class', function () {
|
|
34
|
+
var _renderWithTheme = renderWithTheme(_jsx(Wizard, {
|
|
35
|
+
activeStep: 0,
|
|
36
|
+
steps: steps,
|
|
37
|
+
errored: [0]
|
|
38
|
+
})),
|
|
39
|
+
container = _renderWithTheme.container;
|
|
40
|
+
expect(container.querySelector('.wizard-step-error')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
it('still renders the active step content when it is in an error state', function () {
|
|
43
|
+
renderWithTheme(_jsx(Wizard, {
|
|
44
|
+
activeStep: 0,
|
|
45
|
+
steps: steps,
|
|
46
|
+
errored: [0]
|
|
47
|
+
}));
|
|
48
|
+
expect(screen.getByText('Domain content')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
it('announces the error status to screen readers', function () {
|
|
51
|
+
renderWithTheme(_jsx(Wizard, {
|
|
52
|
+
activeStep: 0,
|
|
53
|
+
steps: steps,
|
|
54
|
+
errored: [0]
|
|
55
|
+
}));
|
|
56
|
+
expect(screen.getByText(/Step has an error/i)).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
it('does not apply the error status to steps that are not errored', function () {
|
|
59
|
+
var _renderWithTheme2 = renderWithTheme(_jsx(Wizard, {
|
|
60
|
+
activeStep: 0,
|
|
61
|
+
steps: steps,
|
|
62
|
+
errored: [0]
|
|
63
|
+
})),
|
|
64
|
+
container = _renderWithTheme2.container;
|
|
65
|
+
|
|
66
|
+
// Only one step should carry the error status.
|
|
67
|
+
expect(container.querySelectorAll('.wizard-step-error')).toHaveLength(1);
|
|
68
|
+
});
|
|
69
|
+
it('has no accessibility violations in the error state', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee() {
|
|
70
|
+
var _renderWithTheme3, container, _t;
|
|
71
|
+
return _regenerator().w(function (_context) {
|
|
72
|
+
while (1) switch (_context.n) {
|
|
73
|
+
case 0:
|
|
74
|
+
_renderWithTheme3 = renderWithTheme(_jsx(Wizard, {
|
|
75
|
+
activeStep: 0,
|
|
76
|
+
steps: steps,
|
|
77
|
+
errored: [0]
|
|
78
|
+
})), container = _renderWithTheme3.container;
|
|
79
|
+
_t = expect;
|
|
80
|
+
_context.n = 1;
|
|
81
|
+
return axe(container);
|
|
82
|
+
case 1:
|
|
83
|
+
_t(_context.v).toHaveNoViolations();
|
|
84
|
+
case 2:
|
|
85
|
+
return _context.a(2);
|
|
86
|
+
}
|
|
87
|
+
}, _callee);
|
|
88
|
+
})));
|
|
89
|
+
it('hides the subtitle of an errored step, showing only its error content', function () {
|
|
90
|
+
var errorSteps = [{
|
|
91
|
+
title: 'Step One',
|
|
92
|
+
subTitle: 'Step instructions',
|
|
93
|
+
children: _jsx("div", {
|
|
94
|
+
children: "Error body"
|
|
95
|
+
})
|
|
96
|
+
}];
|
|
97
|
+
renderWithTheme(_jsx(Wizard, {
|
|
98
|
+
activeStep: 0,
|
|
99
|
+
steps: errorSteps,
|
|
100
|
+
errored: [0]
|
|
101
|
+
}));
|
|
102
|
+
expect(screen.queryByText('Step instructions')).not.toBeInTheDocument();
|
|
103
|
+
expect(screen.getByText('Error body')).toBeInTheDocument();
|
|
104
|
+
});
|
|
105
|
+
it('shows the subtitle of an active step that is not errored', function () {
|
|
106
|
+
var okSteps = [{
|
|
107
|
+
title: 'Step One',
|
|
108
|
+
subTitle: 'Step instructions',
|
|
109
|
+
children: _jsx("div", {
|
|
110
|
+
children: "Body"
|
|
111
|
+
})
|
|
112
|
+
}];
|
|
113
|
+
renderWithTheme(_jsx(Wizard, {
|
|
114
|
+
activeStep: 0,
|
|
115
|
+
steps: okSteps
|
|
116
|
+
}));
|
|
117
|
+
expect(screen.getByText('Step instructions')).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -38,6 +38,14 @@ export interface WizardStepProps {
|
|
|
38
38
|
* @default false
|
|
39
39
|
*/
|
|
40
40
|
skipped?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Whether this step is in an error state. Takes visual precedence over the
|
|
43
|
+
* active/complete/skipped status: renders a red error icon, title, and left
|
|
44
|
+
* border. The subtitle is hidden so the step shows only its error content
|
|
45
|
+
* (children); children continue to render for the active step.
|
|
46
|
+
* @default false
|
|
47
|
+
*/
|
|
48
|
+
error?: boolean;
|
|
41
49
|
/** Callback invoked when the user clicks the change action on a completed or skipped step. */
|
|
42
50
|
onChange?: () => void;
|
|
43
51
|
/** An array of label-value pairs displayed as a summary when the step is completed or skipped. */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* External dependencies
|
|
5
5
|
*/
|
|
6
6
|
import React, { useLayoutEffect } from 'react';
|
|
7
|
-
import { BsCircleFill, BsFillCheckCircleFill } from 'react-icons/bs';
|
|
7
|
+
import { BsCircleFill, BsFillCheckCircleFill, BsXCircleFill } from 'react-icons/bs';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies
|
|
@@ -23,6 +23,8 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
|
|
|
23
23
|
skipped = _ref$skipped === void 0 ? false : _ref$skipped,
|
|
24
24
|
_ref$complete = _ref.complete,
|
|
25
25
|
complete = _ref$complete === void 0 ? false : _ref$complete,
|
|
26
|
+
_ref$error = _ref.error,
|
|
27
|
+
error = _ref$error === void 0 ? false : _ref$error,
|
|
26
28
|
children = _ref.children,
|
|
27
29
|
active = _ref.active,
|
|
28
30
|
order = _ref.order,
|
|
@@ -45,7 +47,11 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
|
|
|
45
47
|
var titleRef = React.useRef(null);
|
|
46
48
|
var status = 'inactive';
|
|
47
49
|
var statusText = 'Step not completed';
|
|
48
|
-
if (
|
|
50
|
+
if (error) {
|
|
51
|
+
// Error takes visual precedence over every other status.
|
|
52
|
+
status = 'error';
|
|
53
|
+
statusText = 'Step has an error';
|
|
54
|
+
} else if (active && !(complete && totalSteps === 1)) {
|
|
49
55
|
// if the step is active but is an unique step, we don't want to show as active status
|
|
50
56
|
status = 'active';
|
|
51
57
|
statusText = ''; // not adding the status text for active step since it's announced by aria-current
|
|
@@ -60,6 +66,12 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
|
|
|
60
66
|
statusText = "Status: " + statusText;
|
|
61
67
|
}
|
|
62
68
|
var stepText = "STEP " + order + " OF " + totalSteps;
|
|
69
|
+
var StatusIcon = BsCircleFill;
|
|
70
|
+
if (error) {
|
|
71
|
+
StatusIcon = BsXCircleFill;
|
|
72
|
+
} else if (complete) {
|
|
73
|
+
StatusIcon = BsFillCheckCircleFill;
|
|
74
|
+
}
|
|
63
75
|
var borderLeftColor = "wizard.step.border." + status;
|
|
64
76
|
var statusIconColor = "wizard.step.icon." + status;
|
|
65
77
|
var statusIconStyles = {
|
|
@@ -123,9 +135,7 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
|
|
|
123
135
|
alignItems: 'center'
|
|
124
136
|
},
|
|
125
137
|
"aria-hidden": "true",
|
|
126
|
-
children: [
|
|
127
|
-
sx: statusIconStyles
|
|
128
|
-
}) : _jsx(BsCircleFill, {
|
|
138
|
+
children: [_jsx(StatusIcon, {
|
|
129
139
|
sx: statusIconStyles
|
|
130
140
|
}), title]
|
|
131
141
|
}), _jsx(ScreenReaderText, {
|
|
@@ -160,14 +170,17 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
|
|
|
160
170
|
sx: {
|
|
161
171
|
mt: 2
|
|
162
172
|
}
|
|
163
|
-
}), subTitle && active && _jsx(Text, {
|
|
173
|
+
}), subTitle && active && !error && _jsx(Text, {
|
|
164
174
|
sx: {
|
|
165
175
|
my: 3
|
|
166
176
|
},
|
|
167
177
|
children: subTitle
|
|
168
|
-
}), active && Boolean(children) &&
|
|
178
|
+
}), active && Boolean(children) &&
|
|
179
|
+
// In the error state there's no subtitle, so the content sits directly
|
|
180
|
+
// under the heading and needs a bit more breathing room (12px vs 8px).
|
|
181
|
+
_jsx(Box, {
|
|
169
182
|
sx: {
|
|
170
|
-
pt: 2
|
|
183
|
+
pt: error ? 3 : 2
|
|
171
184
|
},
|
|
172
185
|
children: children
|
|
173
186
|
})]
|
|
@@ -88,24 +88,31 @@ var getComponentColors = function getComponentColors(theme, gColor, gVariants) {
|
|
|
88
88
|
number: {
|
|
89
89
|
color: theme.text.helper
|
|
90
90
|
},
|
|
91
|
+
// The `error` status (heading/icon/border below) intentionally uses
|
|
92
|
+
// `theme.text.error` (#bf2a23) for all three, per the Figma error design —
|
|
93
|
+
// not the `support.*.error` palette used by other statuses, whose reds are
|
|
94
|
+
// different shades (icon #e74135, accent #ff745f) and would not match.
|
|
91
95
|
heading: {
|
|
92
96
|
complete: theme.text.success,
|
|
93
97
|
active: theme.heading,
|
|
94
98
|
inactive: theme.text.helper,
|
|
95
|
-
skipped: theme.text.helper
|
|
99
|
+
skipped: theme.text.helper,
|
|
100
|
+
error: theme.text.error
|
|
96
101
|
},
|
|
97
102
|
icon: {
|
|
98
103
|
complete: theme.support.icon.success,
|
|
99
104
|
active: theme.link["default"],
|
|
100
105
|
inactive: theme.input.border.disabled,
|
|
101
|
-
skipped: theme.input.border.disabled
|
|
106
|
+
skipped: theme.input.border.disabled,
|
|
107
|
+
error: theme.text.error
|
|
102
108
|
},
|
|
103
109
|
border: {
|
|
104
110
|
"default": theme.border['2'],
|
|
105
111
|
complete: theme.support.accent.success,
|
|
106
112
|
active: theme.border.accent,
|
|
107
113
|
inactive: theme.input.border.disabled,
|
|
108
|
-
skipped: theme.input.border.disabled
|
|
114
|
+
skipped: theme.input.border.disabled,
|
|
115
|
+
error: theme.text.error
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
},
|
package/package.json
CHANGED
|
@@ -111,7 +111,7 @@ const inlineChipsContainerStyles = {
|
|
|
111
111
|
flexWrap: 'wrap',
|
|
112
112
|
alignItems: 'center',
|
|
113
113
|
p: 1,
|
|
114
|
-
pr:
|
|
114
|
+
pr: 5,
|
|
115
115
|
position: 'relative',
|
|
116
116
|
'& .autocomplete__input': {
|
|
117
117
|
...defaultStyles[ '& .autocomplete__input' ],
|
|
@@ -125,13 +125,15 @@ const inlineChipsContainerStyles = {
|
|
|
125
125
|
lineHeight: '24px',
|
|
126
126
|
minHeight: '24px',
|
|
127
127
|
'& .autocomplete__dropdown-arrow-down': {
|
|
128
|
-
top: '
|
|
129
|
-
bottom: '
|
|
128
|
+
top: '6px',
|
|
129
|
+
bottom: 'unset',
|
|
130
130
|
},
|
|
131
131
|
},
|
|
132
132
|
};
|
|
133
133
|
|
|
134
|
-
const DefaultArrow = config =>
|
|
134
|
+
const DefaultArrow = config => (
|
|
135
|
+
<FormSelectArrow className={ config.className } separator={ false } />
|
|
136
|
+
);
|
|
135
137
|
|
|
136
138
|
const AddSelectionStatus = ( { status } ) => {
|
|
137
139
|
return (
|
|
@@ -446,7 +448,7 @@ const FormAutocompleteMultiselect = React.forwardRef(
|
|
|
446
448
|
unselectValue={ unselectValue }
|
|
447
449
|
/>
|
|
448
450
|
) ) }
|
|
449
|
-
<div sx={ { flex: '1 1 120px', minWidth: '120px' } }>
|
|
451
|
+
<div sx={ { flex: '1 1 120px', minWidth: '120px', mr: -5 } }>
|
|
450
452
|
<Autocomplete
|
|
451
453
|
id={ forLabel }
|
|
452
454
|
aria-busy={ loading }
|
|
@@ -47,6 +47,38 @@ export const Primary: Story = {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
export const Error: Story = {
|
|
51
|
+
render: () => {
|
|
52
|
+
const steps: WizardStepProps[] = [
|
|
53
|
+
{
|
|
54
|
+
title: 'Step One',
|
|
55
|
+
titleVariant: 'h3',
|
|
56
|
+
children: (
|
|
57
|
+
<Box>
|
|
58
|
+
<Text sx={ { display: 'block', mb: 3, color: 'texts.secondary' } }>
|
|
59
|
+
Something went wrong. Please try again.
|
|
60
|
+
</Text>
|
|
61
|
+
<Button>Retry</Button>
|
|
62
|
+
</Box>
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: 'Step Two',
|
|
67
|
+
titleVariant: 'h3',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: 'Step Three',
|
|
71
|
+
titleVariant: 'h3',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
return (
|
|
75
|
+
<Box mt={ 4 }>
|
|
76
|
+
<Wizard activeStep={ 0 } steps={ steps } errored={ [ 0 ] } />
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
50
82
|
export const Default: Story = {
|
|
51
83
|
render: () => {
|
|
52
84
|
const steps: WizardStepProps[] = [
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** @jsxImportSource theme-ui */
|
|
2
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
import { render, screen } from '@testing-library/react';
|
|
6
|
+
import { axe } from 'jest-axe';
|
|
7
|
+
import { ThemeUIProvider } from 'theme-ui';
|
|
8
|
+
|
|
9
|
+
import { Wizard } from './Wizard';
|
|
10
|
+
import { theme } from '../';
|
|
11
|
+
|
|
12
|
+
jest.mock( '@theme-ui/match-media' );
|
|
13
|
+
|
|
14
|
+
const renderWithTheme = children =>
|
|
15
|
+
render( <ThemeUIProvider theme={ theme }>{ children }</ThemeUIProvider> );
|
|
16
|
+
|
|
17
|
+
const steps = [
|
|
18
|
+
{ title: 'Salesforce Domain', children: <div>Domain content</div> },
|
|
19
|
+
{ title: 'Install Packages' },
|
|
20
|
+
{ title: 'Connect to Salesforce' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
describe( '<Wizard /> error state', () => {
|
|
24
|
+
it( 'marks an errored step with the error status class', () => {
|
|
25
|
+
const { container } = renderWithTheme(
|
|
26
|
+
<Wizard activeStep={ 0 } steps={ steps } errored={ [ 0 ] } />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
expect( container.querySelector( '.wizard-step-error' ) ).toBeInTheDocument();
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'still renders the active step content when it is in an error state', () => {
|
|
33
|
+
renderWithTheme( <Wizard activeStep={ 0 } steps={ steps } errored={ [ 0 ] } /> );
|
|
34
|
+
|
|
35
|
+
expect( screen.getByText( 'Domain content' ) ).toBeInTheDocument();
|
|
36
|
+
} );
|
|
37
|
+
|
|
38
|
+
it( 'announces the error status to screen readers', () => {
|
|
39
|
+
renderWithTheme( <Wizard activeStep={ 0 } steps={ steps } errored={ [ 0 ] } /> );
|
|
40
|
+
|
|
41
|
+
expect( screen.getByText( /Step has an error/i ) ).toBeInTheDocument();
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
it( 'does not apply the error status to steps that are not errored', () => {
|
|
45
|
+
const { container } = renderWithTheme(
|
|
46
|
+
<Wizard activeStep={ 0 } steps={ steps } errored={ [ 0 ] } />
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Only one step should carry the error status.
|
|
50
|
+
expect( container.querySelectorAll( '.wizard-step-error' ) ).toHaveLength( 1 );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'has no accessibility violations in the error state', async () => {
|
|
54
|
+
const { container } = renderWithTheme(
|
|
55
|
+
<Wizard activeStep={ 0 } steps={ steps } errored={ [ 0 ] } />
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect( await axe( container ) ).toHaveNoViolations();
|
|
59
|
+
} );
|
|
60
|
+
|
|
61
|
+
it( 'hides the subtitle of an errored step, showing only its error content', () => {
|
|
62
|
+
const errorSteps = [
|
|
63
|
+
{ title: 'Step One', subTitle: 'Step instructions', children: <div>Error body</div> },
|
|
64
|
+
];
|
|
65
|
+
renderWithTheme( <Wizard activeStep={ 0 } steps={ errorSteps } errored={ [ 0 ] } /> );
|
|
66
|
+
|
|
67
|
+
expect( screen.queryByText( 'Step instructions' ) ).not.toBeInTheDocument();
|
|
68
|
+
expect( screen.getByText( 'Error body' ) ).toBeInTheDocument();
|
|
69
|
+
} );
|
|
70
|
+
|
|
71
|
+
it( 'shows the subtitle of an active step that is not errored', () => {
|
|
72
|
+
const okSteps = [
|
|
73
|
+
{ title: 'Step One', subTitle: 'Step instructions', children: <div>Body</div> },
|
|
74
|
+
];
|
|
75
|
+
renderWithTheme( <Wizard activeStep={ 0 } steps={ okSteps } /> );
|
|
76
|
+
|
|
77
|
+
expect( screen.getByText( 'Step instructions' ) ).toBeInTheDocument();
|
|
78
|
+
} );
|
|
79
|
+
} );
|
|
@@ -27,6 +27,12 @@ export interface WizardProps {
|
|
|
27
27
|
* @default []
|
|
28
28
|
*/
|
|
29
29
|
skipped?: number[];
|
|
30
|
+
/**
|
|
31
|
+
* Array of zero-based indices for steps that are in an error state. An errored
|
|
32
|
+
* step shows a red error icon, title, and left border (see WizardStep `error`).
|
|
33
|
+
* @default []
|
|
34
|
+
*/
|
|
35
|
+
errored?: number[];
|
|
30
36
|
/** Additional CSS class name for the wizard container. */
|
|
31
37
|
className?: string;
|
|
32
38
|
/**
|
|
@@ -57,6 +63,7 @@ export const Wizard = React.forwardRef< HTMLDivElement, WizardProps >(
|
|
|
57
63
|
activeStep,
|
|
58
64
|
completed = [],
|
|
59
65
|
skipped = [],
|
|
66
|
+
errored = [],
|
|
60
67
|
className = null,
|
|
61
68
|
titleAutofocus = false,
|
|
62
69
|
showStepText = true,
|
|
@@ -95,6 +102,7 @@ export const Wizard = React.forwardRef< HTMLDivElement, WizardProps >(
|
|
|
95
102
|
active={ index === activeStep }
|
|
96
103
|
complete={ completed.includes( index ) }
|
|
97
104
|
skipped={ skipped.includes( index ) }
|
|
105
|
+
error={ errored.includes( index ) }
|
|
98
106
|
key={ index }
|
|
99
107
|
order={ index + 1 }
|
|
100
108
|
totalSteps={ steps.length }
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* External dependencies
|
|
5
5
|
*/
|
|
6
6
|
import React, { useLayoutEffect } from 'react';
|
|
7
|
-
import { BsCircleFill, BsFillCheckCircleFill } from 'react-icons/bs';
|
|
7
|
+
import { BsCircleFill, BsFillCheckCircleFill, BsXCircleFill } from 'react-icons/bs';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Internal dependencies
|
|
@@ -48,6 +48,14 @@ export interface WizardStepProps {
|
|
|
48
48
|
* @default false
|
|
49
49
|
*/
|
|
50
50
|
skipped?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Whether this step is in an error state. Takes visual precedence over the
|
|
53
|
+
* active/complete/skipped status: renders a red error icon, title, and left
|
|
54
|
+
* border. The subtitle is hidden so the step shows only its error content
|
|
55
|
+
* (children); children continue to render for the active step.
|
|
56
|
+
* @default false
|
|
57
|
+
*/
|
|
58
|
+
error?: boolean;
|
|
51
59
|
/** Callback invoked when the user clicks the change action on a completed or skipped step. */
|
|
52
60
|
onChange?: () => void;
|
|
53
61
|
/** An array of label-value pairs displayed as a summary when the step is completed or skipped. */
|
|
@@ -91,6 +99,7 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
|
|
|
91
99
|
subTitle,
|
|
92
100
|
skipped = false,
|
|
93
101
|
complete = false,
|
|
102
|
+
error = false,
|
|
94
103
|
children,
|
|
95
104
|
active,
|
|
96
105
|
order,
|
|
@@ -111,7 +120,11 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
|
|
|
111
120
|
const titleRef = React.useRef< HTMLHeadingElement >( null );
|
|
112
121
|
let status = 'inactive';
|
|
113
122
|
let statusText = 'Step not completed';
|
|
114
|
-
if (
|
|
123
|
+
if ( error ) {
|
|
124
|
+
// Error takes visual precedence over every other status.
|
|
125
|
+
status = 'error';
|
|
126
|
+
statusText = 'Step has an error';
|
|
127
|
+
} else if ( active && ! ( complete && totalSteps === 1 ) ) {
|
|
115
128
|
// if the step is active but is an unique step, we don't want to show as active status
|
|
116
129
|
status = 'active';
|
|
117
130
|
statusText = ''; // not adding the status text for active step since it's announced by aria-current
|
|
@@ -127,6 +140,13 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
|
|
|
127
140
|
}
|
|
128
141
|
const stepText = `STEP ${ order } OF ${ totalSteps }`;
|
|
129
142
|
|
|
143
|
+
let StatusIcon = BsCircleFill;
|
|
144
|
+
if ( error ) {
|
|
145
|
+
StatusIcon = BsXCircleFill;
|
|
146
|
+
} else if ( complete ) {
|
|
147
|
+
StatusIcon = BsFillCheckCircleFill;
|
|
148
|
+
}
|
|
149
|
+
|
|
130
150
|
const borderLeftColor = `wizard.step.border.${ status }`;
|
|
131
151
|
const statusIconColor = `wizard.step.icon.${ status }`;
|
|
132
152
|
const statusIconStyles = {
|
|
@@ -187,11 +207,7 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
|
|
|
187
207
|
) }
|
|
188
208
|
|
|
189
209
|
<Flex as="span" sx={ { alignItems: 'center' } } aria-hidden="true">
|
|
190
|
-
{
|
|
191
|
-
<BsFillCheckCircleFill sx={ statusIconStyles } />
|
|
192
|
-
) : (
|
|
193
|
-
<BsCircleFill sx={ statusIconStyles } />
|
|
194
|
-
) }
|
|
210
|
+
<StatusIcon sx={ statusIconStyles } />
|
|
195
211
|
{ title }
|
|
196
212
|
</Flex>
|
|
197
213
|
|
|
@@ -226,9 +242,13 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
|
|
|
226
242
|
/>
|
|
227
243
|
) }
|
|
228
244
|
|
|
229
|
-
{ subTitle && active && <Text sx={ { my: 3 } }>{ subTitle }</Text> }
|
|
245
|
+
{ subTitle && active && ! error && <Text sx={ { my: 3 } }>{ subTitle }</Text> }
|
|
230
246
|
|
|
231
|
-
{ active && Boolean( children ) &&
|
|
247
|
+
{ active && Boolean( children ) && (
|
|
248
|
+
// In the error state there's no subtitle, so the content sits directly
|
|
249
|
+
// under the heading and needs a bit more breathing room (12px vs 8px).
|
|
250
|
+
<Box sx={ { pt: error ? 3 : 2 } }>{ children }</Box>
|
|
251
|
+
) }
|
|
232
252
|
</Card>
|
|
233
253
|
);
|
|
234
254
|
}
|
|
@@ -130,17 +130,23 @@ const getComponentColors = ( theme, gColor, gVariants ) => ( {
|
|
|
130
130
|
number: {
|
|
131
131
|
color: theme.text.helper,
|
|
132
132
|
},
|
|
133
|
+
// The `error` status (heading/icon/border below) intentionally uses
|
|
134
|
+
// `theme.text.error` (#bf2a23) for all three, per the Figma error design —
|
|
135
|
+
// not the `support.*.error` palette used by other statuses, whose reds are
|
|
136
|
+
// different shades (icon #e74135, accent #ff745f) and would not match.
|
|
133
137
|
heading: {
|
|
134
138
|
complete: theme.text.success,
|
|
135
139
|
active: theme.heading,
|
|
136
140
|
inactive: theme.text.helper,
|
|
137
141
|
skipped: theme.text.helper,
|
|
142
|
+
error: theme.text.error,
|
|
138
143
|
},
|
|
139
144
|
icon: {
|
|
140
145
|
complete: theme.support.icon.success,
|
|
141
146
|
active: theme.link.default,
|
|
142
147
|
inactive: theme.input.border.disabled,
|
|
143
148
|
skipped: theme.input.border.disabled,
|
|
149
|
+
error: theme.text.error,
|
|
144
150
|
},
|
|
145
151
|
border: {
|
|
146
152
|
default: theme.border[ '2' ],
|
|
@@ -148,6 +154,7 @@ const getComponentColors = ( theme, gColor, gVariants ) => ( {
|
|
|
148
154
|
active: theme.border.accent,
|
|
149
155
|
inactive: theme.input.border.disabled,
|
|
150
156
|
skipped: theme.input.border.disabled,
|
|
157
|
+
error: theme.text.error,
|
|
151
158
|
},
|
|
152
159
|
},
|
|
153
160
|
},
|