@automattic/vip-design-system 2.20.2 → 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.
@@ -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,2 @@
1
+ /** @jsxImportSource theme-ui */
2
+ export {};
@@ -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 (active && !(complete && totalSteps === 1)) {
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: [complete ? _jsx(BsFillCheckCircleFill, {
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) && _jsx(Box, {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip-design-system",
3
- "version": "2.20.2",
3
+ "version": "2.20.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Automattic/vip-design-system"
@@ -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 ( active && ! ( complete && totalSteps === 1 ) ) {
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
- { complete ? (
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 ) && <Box sx={ { pt: 2 } }>{ children }</Box> }
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
  },