@automattic/vip-design-system 2.20.3 → 2.20.5

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.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: React Doctor
3
+ on:
4
+ pull_request:
5
+ branches: [trunk]
6
+ types: [opened, reopened, synchronize, ready_for_review]
7
+
8
+ permissions:
9
+ contents: read
10
+ pull-requests: write
11
+
12
+ concurrency:
13
+ group: react-doctor-${{ github.event.pull_request.number || github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ react-doctor:
18
+ name: React Doctor
19
+ runs-on: ubuntu-latest
20
+ timeout-minutes: 10
21
+ steps:
22
+ - uses: actions/checkout@v6
23
+ with:
24
+ # Full history so the PR can be diffed against the trunk base.
25
+ fetch-depth: 0
26
+
27
+ - uses: millionco/react-doctor@547e1e4ecdb70315d81a91ec3605701d58616ee2 # v2.1.0
28
+ with:
29
+ # Advisory only: report findings but never fail the run / block merge.
30
+ blocking: none
31
+ # Only report issues this PR introduces versus the trunk base.
32
+ scope: changed
33
+ # Keep a single sticky summary comment that updates in place.
34
+ comment: true
35
+ # Avoid flooding the PR with inline comments / hitting comment limits.
36
+ review-comments: false
37
+ # No commit status (also keeps permissions to contents:read + PR write).
38
+ commit-status: false
@@ -57,9 +57,6 @@ export var Item = function Item(_ref) {
57
57
  '&:last-child': {
58
58
  borderBottomLeftRadius: 1,
59
59
  borderBottomRightRadius: 1
60
- },
61
- '&:focus-within': function focusWithin(theme) {
62
- return theme.outline;
63
60
  }
64
61
  },
65
62
  children: children
@@ -108,6 +105,19 @@ export var Trigger = /*#__PURE__*/React.forwardRef(function (_ref2, forwardedRef
108
105
  },
109
106
  '&:hover': {
110
107
  backgroundColor: 'accordion.background.hover'
108
+ },
109
+ // Keyboard-only focus ring on the header (the trigger button
110
+ // fills the header). Rendered inset, reusing the shared
111
+ // focus-ring colors, because the parent Item has overflow:hidden
112
+ // for its rounded corners which would clip an outset ring.
113
+ '&:focus-visible': function focusVisible(theme) {
114
+ var _theme$outline;
115
+ var ring = (_theme$outline = theme.outline) == null ? void 0 : _theme$outline.boxShadow;
116
+ return ring ? {
117
+ boxShadow: ring.split(/,(?![^(]*\))/).map(function (layer) {
118
+ return "inset " + layer.trim();
119
+ }).join(', ')
120
+ } : {};
111
121
  }
112
122
  }, sx)
113
123
  }, props, {
@@ -11,7 +11,6 @@ export declare const inputBaseText = "input.text.default";
11
11
  export declare const inputBaseBackground = "input.background.default";
12
12
  export declare const baseControlFocusStyle: {
13
13
  '&:focus-visible': (theme: InputTheme) => Record<string, string> | undefined;
14
- '&:focus-within': (theme: InputTheme) => Record<string, string> | undefined;
15
14
  };
16
15
  export declare const baseControlStyle: {
17
16
  '&:disabled': {
@@ -26,7 +25,6 @@ export declare const baseControlStyle: {
26
25
  opacity: number;
27
26
  };
28
27
  '&:focus-visible': (theme: InputTheme) => Record<string, string> | undefined;
29
- '&:focus-within': (theme: InputTheme) => Record<string, string> | undefined;
30
28
  backgroundColor: string;
31
29
  color: string;
32
30
  borderRadius: number;
@@ -11,9 +11,6 @@ export var inputBaseBackground = 'input.background.default';
11
11
  export var baseControlFocusStyle = {
12
12
  '&:focus-visible': function focusVisible(theme) {
13
13
  return theme.outline;
14
- },
15
- '&:focus-within': function focusWithin(theme) {
16
- return theme.outline;
17
14
  }
18
15
  };
19
16
  export var baseControlStyle = _extends({}, baseControlBorderStyle, {
@@ -11,7 +11,7 @@ export var inputStyle = function inputStyle(variant) {
11
11
  clipPath: 'inset(50%)',
12
12
  width: RADIO_SIZE,
13
13
  height: RADIO_SIZE,
14
- '&:focus ~ label:before': {
14
+ '&:focus-visible ~ label:before': {
15
15
  variant: 'outline',
16
16
  content: '""',
17
17
  border: '1px solid',
@@ -28,8 +28,7 @@ export var DialogTitle = function DialogTitle(_ref) {
28
28
  return _jsx(DialogPrimitive.Title, {
29
29
  sx: {
30
30
  margin: 0,
31
- fontSize: 3,
32
- fontWeight: 'medium',
31
+ variant: 'text.h3',
33
32
  color: 'heading'
34
33
  },
35
34
  children: titleNode
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  * Internal dependencies
5
5
  */
6
6
  import { WizardStepProps } from './WizardStep';
7
+ import { HeadingProps } from '../Heading/Heading';
7
8
  export interface WizardProps {
8
9
  /** The array of step configurations to render. */
9
10
  steps: WizardStepProps[];
@@ -42,6 +43,18 @@ export interface WizardProps {
42
43
  * @default 'table'
43
44
  */
44
45
  summaryAs?: 'table' | 'dl';
46
+ /**
47
+ * The heading variant (typographic style) applied to every step title. A step
48
+ * can override this with its own `titleVariant`.
49
+ * @default 'h3'
50
+ */
51
+ titleVariant?: HeadingProps['variant'];
52
+ /**
53
+ * The HTML heading element used for every step title, independent of its
54
+ * visual style — useful for keeping a correct document outline. A step can
55
+ * override this with its own `titleAs`. Defaults to the value of `titleVariant`.
56
+ */
57
+ titleAs?: HeadingProps['variant'];
45
58
  }
46
59
  /**
47
60
  * A multi-step wizard component that displays a sequence of steps with progress tracking.
@@ -32,7 +32,9 @@ export var Wizard = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef) {
32
32
  _ref$showStepText = _ref.showStepText,
33
33
  showStepText = _ref$showStepText === void 0 ? true : _ref$showStepText,
34
34
  _ref$summaryAs = _ref.summaryAs,
35
- summaryAs = _ref$summaryAs === void 0 ? 'table' : _ref$summaryAs;
35
+ summaryAs = _ref$summaryAs === void 0 ? 'table' : _ref$summaryAs,
36
+ titleVariant = _ref.titleVariant,
37
+ titleAs = _ref.titleAs;
36
38
  var _useState = useState(false),
37
39
  didMount = _useState[0],
38
40
  setDidMount = _useState[1];
@@ -52,7 +54,8 @@ export var Wizard = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef) {
52
54
  var title = _ref2.title,
53
55
  subTitle = _ref2.subTitle,
54
56
  children = _ref2.children,
55
- titleVariant = _ref2.titleVariant,
57
+ stepTitleVariant = _ref2.titleVariant,
58
+ stepTitleAs = _ref2.titleAs,
56
59
  summary = _ref2.summary,
57
60
  onChange = _ref2.onChange,
58
61
  actionLabel = _ref2.actionLabel,
@@ -68,7 +71,8 @@ export var Wizard = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef) {
68
71
  totalSteps: steps.length,
69
72
  subTitle: subTitle,
70
73
  title: title,
71
- titleVariant: titleVariant,
74
+ titleVariant: stepTitleVariant != null ? stepTitleVariant : titleVariant,
75
+ titleAs: stepTitleAs != null ? stepTitleAs : titleAs,
72
76
  summary: summary,
73
77
  onChange: onChange,
74
78
  shouldFocusTitle: titleAutofocus && didMount,
@@ -12,6 +12,13 @@ declare const _default: {
12
12
  export default _default;
13
13
  type Story = StoryObj<typeof Wizard>;
14
14
  export declare const Primary: Story;
15
+ /**
16
+ * `titleVariant` (visual style) and `titleAs` (semantic HTML element) can be set
17
+ * once at the Wizard level so they apply to every step, avoiding repetition. Here
18
+ * all titles render as `h2` elements while keeping `h3` styling, and the last step
19
+ * overrides `titleAs` to `h4` to demonstrate per-step precedence.
20
+ */
21
+ export declare const WizardTitleLevels: Story;
15
22
  export declare const Error: Story;
16
23
  export declare const Default: Story;
17
24
  export declare const WithTitleAutoFocus: Story;
@@ -37,6 +37,32 @@ export var Primary = {
37
37
  }]
38
38
  }
39
39
  };
40
+
41
+ /**
42
+ * `titleVariant` (visual style) and `titleAs` (semantic HTML element) can be set
43
+ * once at the Wizard level so they apply to every step, avoiding repetition. Here
44
+ * all titles render as `h2` elements while keeping `h3` styling, and the last step
45
+ * overrides `titleAs` to `h4` to demonstrate per-step precedence.
46
+ */
47
+ export var WizardTitleLevels = {
48
+ args: {
49
+ activeStep: 0,
50
+ completed: [],
51
+ titleVariant: 'h3',
52
+ titleAs: 'h2',
53
+ steps: [{
54
+ title: 'Step One',
55
+ subTitle: 'Rendered as an h2 element with h3 styling.',
56
+ children: 'Step one content'
57
+ }, {
58
+ title: 'Step Two',
59
+ subTitle: 'Also an h2 element, inheriting the Wizard-level titleAs.'
60
+ }, {
61
+ title: 'Step Three (overrides titleAs)',
62
+ titleAs: 'h4'
63
+ }]
64
+ }
65
+ };
40
66
  export var Error = {
41
67
  render: function render() {
42
68
  var steps = [{
@@ -116,4 +116,78 @@ describe('<Wizard /> error state', function () {
116
116
  }));
117
117
  expect(screen.getByText('Step instructions')).toBeInTheDocument();
118
118
  });
119
+ });
120
+ describe('<Wizard /> title element and style', function () {
121
+ it('renders step titles as h3 elements by default', function () {
122
+ var _renderWithTheme4 = renderWithTheme(_jsx(Wizard, {
123
+ activeStep: 0,
124
+ steps: steps
125
+ })),
126
+ container = _renderWithTheme4.container;
127
+ expect(container.querySelectorAll('h3')).toHaveLength(steps.length);
128
+ });
129
+ it('applies a Wizard-level titleAs element to every step', function () {
130
+ var _renderWithTheme5 = renderWithTheme(_jsx(Wizard, {
131
+ activeStep: 0,
132
+ steps: steps,
133
+ titleAs: "h2"
134
+ })),
135
+ container = _renderWithTheme5.container;
136
+ expect(container.querySelectorAll('h2')).toHaveLength(steps.length);
137
+ expect(container.querySelectorAll('h3')).toHaveLength(0);
138
+ });
139
+ it('lets a step override the Wizard-level titleAs', function () {
140
+ var overrideSteps = [{
141
+ title: 'Step One'
142
+ }, {
143
+ title: 'Step Two',
144
+ titleAs: 'h4'
145
+ }];
146
+ var _renderWithTheme6 = renderWithTheme(_jsx(Wizard, {
147
+ activeStep: 0,
148
+ steps: overrideSteps,
149
+ titleAs: "h2"
150
+ })),
151
+ container = _renderWithTheme6.container;
152
+
153
+ // One inherits the Wizard default (h2), the other overrides to h4.
154
+ expect(container.querySelectorAll('h2')).toHaveLength(1);
155
+ expect(container.querySelectorAll('h4')).toHaveLength(1);
156
+ });
157
+ it('keeps titleVariant styling while titleAs changes the element', function () {
158
+ var _renderWithTheme7 = renderWithTheme(_jsx(Wizard, {
159
+ activeStep: 0,
160
+ steps: [{
161
+ title: 'Only Step'
162
+ }],
163
+ titleVariant: "h3",
164
+ titleAs: "h2"
165
+ })),
166
+ container = _renderWithTheme7.container;
167
+
168
+ // The rendered element is an h2 that still carries the shared heading class
169
+ // (typographic variant is driven by titleVariant, not the element).
170
+ var heading = container.querySelector('h2.vip-heading-component');
171
+ expect(heading).toBeInTheDocument();
172
+ });
173
+ it('has no accessibility violations when using a custom title element', /*#__PURE__*/_asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee2() {
174
+ var _renderWithTheme8, container, _t2;
175
+ return _regenerator().w(function (_context2) {
176
+ while (1) switch (_context2.n) {
177
+ case 0:
178
+ _renderWithTheme8 = renderWithTheme(_jsx(Wizard, {
179
+ activeStep: 0,
180
+ steps: steps,
181
+ titleAs: "h2"
182
+ })), container = _renderWithTheme8.container;
183
+ _t2 = expect;
184
+ _context2.n = 1;
185
+ return axe(container);
186
+ case 1:
187
+ _t2(_context2.v).toHaveNoViolations();
188
+ case 2:
189
+ return _context2.a(2);
190
+ }
191
+ }, _callee2);
192
+ })));
119
193
  });
@@ -25,10 +25,16 @@ export interface WizardStepProps {
25
25
  /** The title content displayed in the step header. */
26
26
  title: React.ReactNode;
27
27
  /**
28
- * The heading variant used for the step title.
28
+ * The heading variant used for the step title. Controls the typographic style.
29
29
  * @default 'h3'
30
30
  */
31
31
  titleVariant?: HeadingProps['variant'];
32
+ /**
33
+ * The HTML heading element used to render the step title, independent of its
34
+ * visual style. Use this to keep a correct document outline (e.g. render an
35
+ * `h2` while keeping `h3` styling). Defaults to the value of `titleVariant`.
36
+ */
37
+ titleAs?: HeadingProps['variant'];
32
38
  /** A subtitle displayed below the title when the step is active. */
33
39
  subTitle?: React.ReactNode;
34
40
  /** The content rendered inside the step when it is active. */
@@ -32,6 +32,7 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
32
32
  shouldFocusTitle = _ref.shouldFocusTitle,
33
33
  _ref$titleVariant = _ref.titleVariant,
34
34
  titleVariant = _ref$titleVariant === void 0 ? 'h3' : _ref$titleVariant,
35
+ titleAs = _ref.titleAs,
35
36
  summary = _ref.summary,
36
37
  summaryTitle = _ref.summaryTitle,
37
38
  _ref$summaryAs = _ref.summaryAs,
@@ -113,6 +114,7 @@ export var WizardStep = /*#__PURE__*/React.forwardRef(function (_ref, forwardRef
113
114
  },
114
115
  children: [_jsxs(Heading, {
115
116
  variant: titleVariant,
117
+ as: titleAs != null ? titleAs : titleVariant,
116
118
  sx: {
117
119
  mb: 0,
118
120
  color: headingColor,
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://react.doctor/schema/config.json",
3
+ "rules": {
4
+ // theme-ui `sx` prop is flagged as an unknown DOM attribute everywhere — pure noise
5
+ "react-doctor/no-unknown-property": "off"
6
+ },
7
+ "ignore": {
8
+ // Glob patterns excluded from ALL rules.
9
+ "files": [
10
+ // Design-token source + Figma tooling: generated/build-time code (incl. the intentional
11
+ // `eval()` in the responsive-type calculator), never shipped in the component bundle.
12
+ "tokens/**"
13
+ ]
14
+ }
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip-design-system",
3
- "version": "2.20.3",
3
+ "version": "2.20.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Automattic/vip-design-system"
@@ -92,7 +92,6 @@ export const Item = ( { children, ...props }: AccordionItemProps ) => (
92
92
  borderBottomLeftRadius: 1,
93
93
  borderBottomRightRadius: 1,
94
94
  },
95
- '&:focus-within': ( theme: AccordionTheme ) => theme.outline,
96
95
  } }
97
96
  >
98
97
  { children }
@@ -136,6 +135,21 @@ export const Trigger = React.forwardRef< HTMLButtonElement, TriggerProps >(
136
135
  '.vip-accordion-trigger-indicator': { transform: 'rotate(270deg)' },
137
136
  },
138
137
  '&:hover': { backgroundColor: 'accordion.background.hover' },
138
+ // Keyboard-only focus ring on the header (the trigger button
139
+ // fills the header). Rendered inset, reusing the shared
140
+ // focus-ring colors, because the parent Item has overflow:hidden
141
+ // for its rounded corners which would clip an outset ring.
142
+ '&:focus-visible': ( theme: AccordionTheme ) => {
143
+ const ring = theme.outline?.boxShadow;
144
+ return ring
145
+ ? {
146
+ boxShadow: ring
147
+ .split( /,(?![^(]*\))/ )
148
+ .map( layer => `inset ${ layer.trim() }` )
149
+ .join( ', ' ),
150
+ }
151
+ : {};
152
+ },
139
153
  ...sx,
140
154
  } }
141
155
  { ...props }
@@ -14,7 +14,6 @@ export const inputBaseText = 'input.text.default';
14
14
  export const inputBaseBackground = 'input.background.default';
15
15
  export const baseControlFocusStyle = {
16
16
  '&:focus-visible': ( theme: InputTheme ) => theme.outline,
17
- '&:focus-within': ( theme: InputTheme ) => theme.outline,
18
17
  };
19
18
 
20
19
  export const baseControlStyle = {
@@ -13,7 +13,7 @@ export const inputStyle = ( variant: string ): ThemeUIStyleObject => ( {
13
13
  clipPath: 'inset(50%)',
14
14
  width: RADIO_SIZE,
15
15
  height: RADIO_SIZE,
16
- '&:focus ~ label:before': {
16
+ '&:focus-visible ~ label:before': {
17
17
  variant: 'outline',
18
18
  content: '""',
19
19
  border: '1px solid',
@@ -34,9 +34,7 @@ export const DialogTitle: React.FC< DialogTitleProps > = ( { title, hidden = fal
34
34
  }
35
35
 
36
36
  return (
37
- <DialogPrimitive.Title
38
- sx={ { margin: 0, fontSize: 3, fontWeight: 'medium', color: 'heading' } }
39
- >
37
+ <DialogPrimitive.Title sx={ { margin: 0, variant: 'text.h3', color: 'heading' } }>
40
38
  { titleNode }
41
39
  </DialogPrimitive.Title>
42
40
  );
@@ -47,6 +47,36 @@ export const Primary: Story = {
47
47
  },
48
48
  };
49
49
 
50
+ /**
51
+ * `titleVariant` (visual style) and `titleAs` (semantic HTML element) can be set
52
+ * once at the Wizard level so they apply to every step, avoiding repetition. Here
53
+ * all titles render as `h2` elements while keeping `h3` styling, and the last step
54
+ * overrides `titleAs` to `h4` to demonstrate per-step precedence.
55
+ */
56
+ export const WizardTitleLevels: Story = {
57
+ args: {
58
+ activeStep: 0,
59
+ completed: [],
60
+ titleVariant: 'h3',
61
+ titleAs: 'h2',
62
+ steps: [
63
+ {
64
+ title: 'Step One',
65
+ subTitle: 'Rendered as an h2 element with h3 styling.',
66
+ children: 'Step one content',
67
+ },
68
+ {
69
+ title: 'Step Two',
70
+ subTitle: 'Also an h2 element, inheriting the Wizard-level titleAs.',
71
+ },
72
+ {
73
+ title: 'Step Three (overrides titleAs)',
74
+ titleAs: 'h4',
75
+ },
76
+ ],
77
+ },
78
+ };
79
+
50
80
  export const Error: Story = {
51
81
  render: () => {
52
82
  const steps: WizardStepProps[] = [
@@ -77,3 +77,55 @@ describe( '<Wizard /> error state', () => {
77
77
  expect( screen.getByText( 'Step instructions' ) ).toBeInTheDocument();
78
78
  } );
79
79
  } );
80
+
81
+ describe( '<Wizard /> title element and style', () => {
82
+ it( 'renders step titles as h3 elements by default', () => {
83
+ const { container } = renderWithTheme( <Wizard activeStep={ 0 } steps={ steps } /> );
84
+
85
+ expect( container.querySelectorAll( 'h3' ) ).toHaveLength( steps.length );
86
+ } );
87
+
88
+ it( 'applies a Wizard-level titleAs element to every step', () => {
89
+ const { container } = renderWithTheme(
90
+ <Wizard activeStep={ 0 } steps={ steps } titleAs="h2" />
91
+ );
92
+
93
+ expect( container.querySelectorAll( 'h2' ) ).toHaveLength( steps.length );
94
+ expect( container.querySelectorAll( 'h3' ) ).toHaveLength( 0 );
95
+ } );
96
+
97
+ it( 'lets a step override the Wizard-level titleAs', () => {
98
+ const overrideSteps = [ { title: 'Step One' }, { title: 'Step Two', titleAs: 'h4' as const } ];
99
+ const { container } = renderWithTheme(
100
+ <Wizard activeStep={ 0 } steps={ overrideSteps } titleAs="h2" />
101
+ );
102
+
103
+ // One inherits the Wizard default (h2), the other overrides to h4.
104
+ expect( container.querySelectorAll( 'h2' ) ).toHaveLength( 1 );
105
+ expect( container.querySelectorAll( 'h4' ) ).toHaveLength( 1 );
106
+ } );
107
+
108
+ it( 'keeps titleVariant styling while titleAs changes the element', () => {
109
+ const { container } = renderWithTheme(
110
+ <Wizard
111
+ activeStep={ 0 }
112
+ steps={ [ { title: 'Only Step' } ] }
113
+ titleVariant="h3"
114
+ titleAs="h2"
115
+ />
116
+ );
117
+
118
+ // The rendered element is an h2 that still carries the shared heading class
119
+ // (typographic variant is driven by titleVariant, not the element).
120
+ const heading = container.querySelector( 'h2.vip-heading-component' );
121
+ expect( heading ).toBeInTheDocument();
122
+ } );
123
+
124
+ it( 'has no accessibility violations when using a custom title element', async () => {
125
+ const { container } = renderWithTheme(
126
+ <Wizard activeStep={ 0 } steps={ steps } titleAs="h2" />
127
+ );
128
+
129
+ expect( await axe( container ) ).toHaveNoViolations();
130
+ } );
131
+ } );
@@ -11,6 +11,7 @@ import React, { useLayoutEffect, useState } from 'react';
11
11
  */
12
12
  import { WizardStepProps } from './WizardStep';
13
13
  import { Box, WizardStep } from '..';
14
+ import { HeadingProps } from '../Heading/Heading';
14
15
 
15
16
  export interface WizardProps {
16
17
  /** The array of step configurations to render. */
@@ -50,6 +51,18 @@ export interface WizardProps {
50
51
  * @default 'table'
51
52
  */
52
53
  summaryAs?: 'table' | 'dl';
54
+ /**
55
+ * The heading variant (typographic style) applied to every step title. A step
56
+ * can override this with its own `titleVariant`.
57
+ * @default 'h3'
58
+ */
59
+ titleVariant?: HeadingProps[ 'variant' ];
60
+ /**
61
+ * The HTML heading element used for every step title, independent of its
62
+ * visual style — useful for keeping a correct document outline. A step can
63
+ * override this with its own `titleAs`. Defaults to the value of `titleVariant`.
64
+ */
65
+ titleAs?: HeadingProps[ 'variant' ];
53
66
  }
54
67
 
55
68
  /**
@@ -68,6 +81,8 @@ export const Wizard = React.forwardRef< HTMLDivElement, WizardProps >(
68
81
  titleAutofocus = false,
69
82
  showStepText = true,
70
83
  summaryAs = 'table',
84
+ titleVariant,
85
+ titleAs,
71
86
  },
72
87
  forwardRef
73
88
  ) => {
@@ -88,7 +103,8 @@ export const Wizard = React.forwardRef< HTMLDivElement, WizardProps >(
88
103
  title,
89
104
  subTitle,
90
105
  children,
91
- titleVariant,
106
+ titleVariant: stepTitleVariant,
107
+ titleAs: stepTitleAs,
92
108
  summary,
93
109
  onChange,
94
110
  actionLabel,
@@ -108,7 +124,8 @@ export const Wizard = React.forwardRef< HTMLDivElement, WizardProps >(
108
124
  totalSteps={ steps.length }
109
125
  subTitle={ subTitle }
110
126
  title={ title }
111
- titleVariant={ titleVariant }
127
+ titleVariant={ stepTitleVariant ?? titleVariant }
128
+ titleAs={ stepTitleAs ?? titleAs }
112
129
  summary={ summary }
113
130
  onChange={ onChange }
114
131
  shouldFocusTitle={ titleAutofocus && didMount }
@@ -35,10 +35,16 @@ export interface WizardStepProps {
35
35
  /** The title content displayed in the step header. */
36
36
  title: React.ReactNode;
37
37
  /**
38
- * The heading variant used for the step title.
38
+ * The heading variant used for the step title. Controls the typographic style.
39
39
  * @default 'h3'
40
40
  */
41
41
  titleVariant?: HeadingProps[ 'variant' ];
42
+ /**
43
+ * The HTML heading element used to render the step title, independent of its
44
+ * visual style. Use this to keep a correct document outline (e.g. render an
45
+ * `h2` while keeping `h3` styling). Defaults to the value of `titleVariant`.
46
+ */
47
+ titleAs?: HeadingProps[ 'variant' ];
42
48
  /** A subtitle displayed below the title when the step is active. */
43
49
  subTitle?: React.ReactNode;
44
50
  /** The content rendered inside the step when it is active. */
@@ -106,6 +112,7 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
106
112
  totalSteps,
107
113
  shouldFocusTitle,
108
114
  titleVariant = 'h3',
115
+ titleAs,
109
116
  summary,
110
117
  summaryTitle,
111
118
  summaryAs = 'table',
@@ -188,6 +195,7 @@ export const WizardStep = React.forwardRef< HTMLDivElement, WizardStepProps >(
188
195
  <Flex sx={ { alignItems: 'center' } }>
189
196
  <Heading
190
197
  variant={ titleVariant }
198
+ as={ titleAs ?? titleVariant }
191
199
  sx={ {
192
200
  mb: 0,
193
201
  color: headingColor,