@kaizen/components 1.79.4 → 1.79.6
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/cjs/src/LikertScaleLegacy/LikertScaleLegacy.cjs +5 -3
- package/dist/cjs/src/__next__/Select/Select.cjs +18 -14
- package/dist/esm/src/LikertScaleLegacy/LikertScaleLegacy.mjs +5 -3
- package/dist/esm/src/__next__/Select/Select.mjs +18 -14
- package/dist/styles.css +3 -1
- package/dist/types/LikertScaleLegacy/LikertScaleLegacy.d.ts +5 -1
- package/dist/types/__next__/Select/Select.d.ts +1 -1
- package/package.json +1 -1
- package/src/LikertScaleLegacy/LikertScaleLegacy.spec.tsx +1 -0
- package/src/LikertScaleLegacy/LikertScaleLegacy.tsx +7 -1
- package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.mdx +8 -0
- package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.stories.tsx +30 -1
- package/src/__next__/Menu/_docs/Menu--migration-guide.mdx +91 -0
- package/src/__next__/Select/Select.tsx +3 -0
- package/src/__next__/Select/_docs/Select.mdx +8 -0
- package/src/__next__/Select/_docs/Select.stories.tsx +93 -0
- package/src/__next__/Tabs/subcomponents/TabList/TabList.module.css +3 -1
|
@@ -36,7 +36,8 @@ var LikertScaleLegacy = function (_a) {
|
|
|
36
36
|
onSelect = _a.onSelect,
|
|
37
37
|
validationMessage = _a.validationMessage,
|
|
38
38
|
status = _a.status,
|
|
39
|
-
labelId = _a.labelId
|
|
39
|
+
labelId = _a.labelId,
|
|
40
|
+
isRequired = _a.isRequired;
|
|
40
41
|
var _e = React.useState(null),
|
|
41
42
|
hoveredItem = _e[0],
|
|
42
43
|
setHoveredItem = _e[1];
|
|
@@ -83,11 +84,12 @@ var LikertScaleLegacy = function (_a) {
|
|
|
83
84
|
var isRated = selectedItem && selectedItem.value > 0;
|
|
84
85
|
return React__default.default.createElement("div", {
|
|
85
86
|
className: classnames__default.default(LikertScaleLegacy_module.container, isRated && LikertScaleLegacy_module.rated, reversed && [LikertScaleLegacy_module.reversed], hoveredItem !== null && LikertScaleLegacy_module.hovered),
|
|
86
|
-
"aria-labelledby": labelId,
|
|
87
|
+
"aria-labelledby": isRequired ? "".concat(labelId) : labelId,
|
|
87
88
|
role: "radiogroup",
|
|
88
89
|
tabIndex: -1,
|
|
89
90
|
"aria-describedby": validationMessageId,
|
|
90
|
-
"data-testid": dataTestId
|
|
91
|
+
"data-testid": dataTestId,
|
|
92
|
+
"aria-required": isRequired
|
|
91
93
|
}, React__default.default.createElement("div", {
|
|
92
94
|
className: LikertScaleLegacy_module.legend,
|
|
93
95
|
"data-testid": dataTestId && "".concat(dataTestId, "-legend")
|
|
@@ -44,17 +44,19 @@ var Select = function (_a) {
|
|
|
44
44
|
status = _a.status,
|
|
45
45
|
validationMessage = _a.validationMessage,
|
|
46
46
|
isReversed = _a.isReversed,
|
|
47
|
+
_c = _a.isRequired,
|
|
48
|
+
isRequired = _c === void 0 ? false : _c,
|
|
47
49
|
isFullWidth = _a.isFullWidth,
|
|
48
50
|
disabledValues = _a.disabledValues,
|
|
49
51
|
classNameOverride = _a.classNameOverride,
|
|
50
52
|
selectedKey = _a.selectedKey,
|
|
51
53
|
description = _a.description,
|
|
52
|
-
|
|
53
|
-
placeholder =
|
|
54
|
+
_d = _a.placeholder,
|
|
55
|
+
placeholder = _d === void 0 ? '' : _d,
|
|
54
56
|
isDisabled = _a.isDisabled,
|
|
55
57
|
onSelectionChange = _a.onSelectionChange,
|
|
56
58
|
portalContainerId = _a.portalContainerId,
|
|
57
|
-
restProps = tslib.__rest(_a, ["label", "items", "id", "trigger", "children", "status", "validationMessage", "isReversed", "isFullWidth", "disabledValues", "classNameOverride", "selectedKey", "description", "placeholder", "isDisabled", "onSelectionChange", "portalContainerId"]);
|
|
59
|
+
restProps = tslib.__rest(_a, ["label", "items", "id", "trigger", "children", "status", "validationMessage", "isReversed", "isRequired", "isFullWidth", "disabledValues", "classNameOverride", "selectedKey", "description", "placeholder", "isDisabled", "onSelectionChange", "portalContainerId"]);
|
|
58
60
|
var refs = reactDom.useFloating().refs;
|
|
59
61
|
var triggerRef = refs.reference;
|
|
60
62
|
var fallbackId = React.useId();
|
|
@@ -72,18 +74,19 @@ var Select = function (_a) {
|
|
|
72
74
|
description: description,
|
|
73
75
|
placeholder: placeholder,
|
|
74
76
|
isDisabled: isDisabled,
|
|
77
|
+
isRequired: isRequired,
|
|
75
78
|
onSelectionChange: onSelectionChange ? function (key) {
|
|
76
79
|
return onSelectionChange(key);
|
|
77
80
|
} : undefined
|
|
78
81
|
}, restProps);
|
|
79
82
|
var state = select.useSelectState(ariaSelectProps);
|
|
80
|
-
var
|
|
81
|
-
labelProps =
|
|
82
|
-
reactAriaTriggerProps =
|
|
83
|
-
valueProps =
|
|
84
|
-
menuProps =
|
|
85
|
-
errorMessageProps =
|
|
86
|
-
descriptionProps =
|
|
83
|
+
var _e = select$1.useSelect(ariaSelectProps, state, triggerRef),
|
|
84
|
+
labelProps = _e.labelProps,
|
|
85
|
+
reactAriaTriggerProps = _e.triggerProps,
|
|
86
|
+
valueProps = _e.valueProps,
|
|
87
|
+
menuProps = _e.menuProps,
|
|
88
|
+
errorMessageProps = _e.errorMessageProps,
|
|
89
|
+
descriptionProps = _e.descriptionProps;
|
|
87
90
|
// Hack incoming:
|
|
88
91
|
// react-aria/useSelect wants to prefix the combobox's accessible name with the value of the select.
|
|
89
92
|
// We use role=combobox, meaning screen readers will read the value.
|
|
@@ -105,11 +108,12 @@ var Select = function (_a) {
|
|
|
105
108
|
'isDisabled': triggerProps.isDisabled,
|
|
106
109
|
isReversed: isReversed,
|
|
107
110
|
'ref': refs.setReference,
|
|
108
|
-
'aria-describedby': classnames__default.default(validationMessage && validationId, description && descriptionId)
|
|
111
|
+
'aria-describedby': classnames__default.default(validationMessage && validationId, description && descriptionId),
|
|
112
|
+
'aria-required': isRequired
|
|
109
113
|
});
|
|
110
|
-
var
|
|
111
|
-
portalContainer =
|
|
112
|
-
setPortalContainer =
|
|
114
|
+
var _f = React.useState(),
|
|
115
|
+
portalContainer = _f[0],
|
|
116
|
+
setPortalContainer = _f[1];
|
|
113
117
|
React.useEffect(function () {
|
|
114
118
|
if (portalContainerId) {
|
|
115
119
|
var portalElement = document.getElementById(portalContainerId);
|
|
@@ -27,7 +27,8 @@ var LikertScaleLegacy = function (_a) {
|
|
|
27
27
|
onSelect = _a.onSelect,
|
|
28
28
|
validationMessage = _a.validationMessage,
|
|
29
29
|
status = _a.status,
|
|
30
|
-
labelId = _a.labelId
|
|
30
|
+
labelId = _a.labelId,
|
|
31
|
+
isRequired = _a.isRequired;
|
|
31
32
|
var _e = useState(null),
|
|
32
33
|
hoveredItem = _e[0],
|
|
33
34
|
setHoveredItem = _e[1];
|
|
@@ -74,11 +75,12 @@ var LikertScaleLegacy = function (_a) {
|
|
|
74
75
|
var isRated = selectedItem && selectedItem.value > 0;
|
|
75
76
|
return /*#__PURE__*/React.createElement("div", {
|
|
76
77
|
className: classnames(styles.container, isRated && styles.rated, reversed && [styles.reversed], hoveredItem !== null && styles.hovered),
|
|
77
|
-
"aria-labelledby": labelId,
|
|
78
|
+
"aria-labelledby": isRequired ? "".concat(labelId) : labelId,
|
|
78
79
|
role: "radiogroup",
|
|
79
80
|
tabIndex: -1,
|
|
80
81
|
"aria-describedby": validationMessageId,
|
|
81
|
-
"data-testid": dataTestId
|
|
82
|
+
"data-testid": dataTestId,
|
|
83
|
+
"aria-required": isRequired
|
|
82
84
|
}, /*#__PURE__*/React.createElement("div", {
|
|
83
85
|
className: styles.legend,
|
|
84
86
|
"data-testid": dataTestId && "".concat(dataTestId, "-legend")
|
|
@@ -36,17 +36,19 @@ const Select = /*#__PURE__*/function () {
|
|
|
36
36
|
status = _a.status,
|
|
37
37
|
validationMessage = _a.validationMessage,
|
|
38
38
|
isReversed = _a.isReversed,
|
|
39
|
+
_c = _a.isRequired,
|
|
40
|
+
isRequired = _c === void 0 ? false : _c,
|
|
39
41
|
isFullWidth = _a.isFullWidth,
|
|
40
42
|
disabledValues = _a.disabledValues,
|
|
41
43
|
classNameOverride = _a.classNameOverride,
|
|
42
44
|
selectedKey = _a.selectedKey,
|
|
43
45
|
description = _a.description,
|
|
44
|
-
|
|
45
|
-
placeholder =
|
|
46
|
+
_d = _a.placeholder,
|
|
47
|
+
placeholder = _d === void 0 ? '' : _d,
|
|
46
48
|
isDisabled = _a.isDisabled,
|
|
47
49
|
onSelectionChange = _a.onSelectionChange,
|
|
48
50
|
portalContainerId = _a.portalContainerId,
|
|
49
|
-
restProps = __rest(_a, ["label", "items", "id", "trigger", "children", "status", "validationMessage", "isReversed", "isFullWidth", "disabledValues", "classNameOverride", "selectedKey", "description", "placeholder", "isDisabled", "onSelectionChange", "portalContainerId"]);
|
|
51
|
+
restProps = __rest(_a, ["label", "items", "id", "trigger", "children", "status", "validationMessage", "isReversed", "isRequired", "isFullWidth", "disabledValues", "classNameOverride", "selectedKey", "description", "placeholder", "isDisabled", "onSelectionChange", "portalContainerId"]);
|
|
50
52
|
var refs = useFloating().refs;
|
|
51
53
|
var triggerRef = refs.reference;
|
|
52
54
|
var fallbackId = useId();
|
|
@@ -64,18 +66,19 @@ const Select = /*#__PURE__*/function () {
|
|
|
64
66
|
description: description,
|
|
65
67
|
placeholder: placeholder,
|
|
66
68
|
isDisabled: isDisabled,
|
|
69
|
+
isRequired: isRequired,
|
|
67
70
|
onSelectionChange: onSelectionChange ? function (key) {
|
|
68
71
|
return onSelectionChange(key);
|
|
69
72
|
} : undefined
|
|
70
73
|
}, restProps);
|
|
71
74
|
var state = useSelectState(ariaSelectProps);
|
|
72
|
-
var
|
|
73
|
-
labelProps =
|
|
74
|
-
reactAriaTriggerProps =
|
|
75
|
-
valueProps =
|
|
76
|
-
menuProps =
|
|
77
|
-
errorMessageProps =
|
|
78
|
-
descriptionProps =
|
|
75
|
+
var _e = useSelect(ariaSelectProps, state, triggerRef),
|
|
76
|
+
labelProps = _e.labelProps,
|
|
77
|
+
reactAriaTriggerProps = _e.triggerProps,
|
|
78
|
+
valueProps = _e.valueProps,
|
|
79
|
+
menuProps = _e.menuProps,
|
|
80
|
+
errorMessageProps = _e.errorMessageProps,
|
|
81
|
+
descriptionProps = _e.descriptionProps;
|
|
79
82
|
// Hack incoming:
|
|
80
83
|
// react-aria/useSelect wants to prefix the combobox's accessible name with the value of the select.
|
|
81
84
|
// We use role=combobox, meaning screen readers will read the value.
|
|
@@ -97,11 +100,12 @@ const Select = /*#__PURE__*/function () {
|
|
|
97
100
|
'isDisabled': triggerProps.isDisabled,
|
|
98
101
|
isReversed: isReversed,
|
|
99
102
|
'ref': refs.setReference,
|
|
100
|
-
'aria-describedby': classnames(validationMessage && validationId, description && descriptionId)
|
|
103
|
+
'aria-describedby': classnames(validationMessage && validationId, description && descriptionId),
|
|
104
|
+
'aria-required': isRequired
|
|
101
105
|
});
|
|
102
|
-
var
|
|
103
|
-
portalContainer =
|
|
104
|
-
setPortalContainer =
|
|
106
|
+
var _f = useState(),
|
|
107
|
+
portalContainer = _f[0],
|
|
108
|
+
setPortalContainer = _f[1];
|
|
105
109
|
useEffect(function () {
|
|
106
110
|
if (portalContainerId) {
|
|
107
111
|
var portalElement = document.getElementById(portalContainerId);
|
package/dist/styles.css
CHANGED
|
@@ -5062,7 +5062,9 @@
|
|
|
5062
5062
|
align-items: center;
|
|
5063
5063
|
justify-content: center;
|
|
5064
5064
|
position: absolute;
|
|
5065
|
-
|
|
5065
|
+
|
|
5066
|
+
/* from $ca-z-index-fixed */
|
|
5067
|
+
z-index: 1030;
|
|
5066
5068
|
background: var(--color-white);
|
|
5067
5069
|
inset-block: 0 1px;
|
|
5068
5070
|
width: 48px;
|
|
@@ -12,10 +12,14 @@ export type LikertScaleProps = {
|
|
|
12
12
|
'colorSchema'?: ColorSchema | 'classical';
|
|
13
13
|
'validationMessage'?: string;
|
|
14
14
|
'status'?: 'default' | 'error';
|
|
15
|
+
/**
|
|
16
|
+
* Sets aria-required value on radiogroup for assistive technologies. Validation must still be handled.
|
|
17
|
+
*/
|
|
18
|
+
'isRequired'?: boolean;
|
|
15
19
|
'onSelect': (value: ScaleItem | null) => void;
|
|
16
20
|
};
|
|
17
21
|
/**
|
|
18
22
|
* {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3082060201/Likert+Scale Guidance} |
|
|
19
23
|
* {@link https://cultureamp.design/?path=/docs/components-likertscalelegacy--docs Storybook}
|
|
20
24
|
*/
|
|
21
|
-
export declare const LikertScaleLegacy: ({ scale, selectedItem, reversed, colorSchema, "data-testid": dataTestId, onSelect, validationMessage, status, labelId, }: LikertScaleProps) => JSX.Element;
|
|
25
|
+
export declare const LikertScaleLegacy: ({ scale, selectedItem, reversed, colorSchema, "data-testid": dataTestId, onSelect, validationMessage, status, labelId, isRequired, }: LikertScaleProps) => JSX.Element;
|
|
@@ -55,7 +55,7 @@ export type SelectProps<Option extends SelectOption = SelectOption> = {
|
|
|
55
55
|
* {@link https://cultureamp.design/?path=/docs/components-select--docs Storybook}
|
|
56
56
|
*/
|
|
57
57
|
export declare const Select: {
|
|
58
|
-
<Option extends SelectOption = SelectOption>({ label, items, id: propsId, trigger, children, status, validationMessage, isReversed, isFullWidth, disabledValues, classNameOverride, selectedKey, description, placeholder, isDisabled, onSelectionChange, portalContainerId, ...restProps }: SelectProps<Option>): JSX.Element;
|
|
58
|
+
<Option extends SelectOption = SelectOption>({ label, items, id: propsId, trigger, children, status, validationMessage, isReversed, isRequired, isFullWidth, disabledValues, classNameOverride, selectedKey, description, placeholder, isDisabled, onSelectionChange, portalContainerId, ...restProps }: SelectProps<Option>): JSX.Element;
|
|
59
59
|
displayName: string;
|
|
60
60
|
Section: {
|
|
61
61
|
<Option extends SelectOption = SelectOption>({ section, }: import("./subcomponents").ListBoxSectionProps<Option>): JSX.Element;
|
package/package.json
CHANGED
|
@@ -25,6 +25,10 @@ export type LikertScaleProps = {
|
|
|
25
25
|
'colorSchema'?: ColorSchema | 'classical'
|
|
26
26
|
'validationMessage'?: string
|
|
27
27
|
'status'?: 'default' | 'error'
|
|
28
|
+
/**
|
|
29
|
+
* Sets aria-required value on radiogroup for assistive technologies. Validation must still be handled.
|
|
30
|
+
*/
|
|
31
|
+
'isRequired'?: boolean
|
|
28
32
|
'onSelect': (value: ScaleItem | null) => void
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -46,6 +50,7 @@ export const LikertScaleLegacy = ({
|
|
|
46
50
|
validationMessage,
|
|
47
51
|
status,
|
|
48
52
|
labelId,
|
|
53
|
+
isRequired,
|
|
49
54
|
}: LikertScaleProps): JSX.Element => {
|
|
50
55
|
const [hoveredItem, setHoveredItem] = useState<ScaleItem | null>(null)
|
|
51
56
|
const itemRefs: ItemRefs = scale.map((s) => ({
|
|
@@ -104,11 +109,12 @@ export const LikertScaleLegacy = ({
|
|
|
104
109
|
reversed && [styles.reversed],
|
|
105
110
|
hoveredItem !== null && styles.hovered,
|
|
106
111
|
)}
|
|
107
|
-
aria-labelledby={labelId}
|
|
112
|
+
aria-labelledby={isRequired ? `${labelId}` : labelId}
|
|
108
113
|
role="radiogroup"
|
|
109
114
|
tabIndex={-1}
|
|
110
115
|
aria-describedby={validationMessageId}
|
|
111
116
|
data-testid={dataTestId}
|
|
117
|
+
aria-required={isRequired}
|
|
112
118
|
>
|
|
113
119
|
<div className={styles.legend} data-testid={dataTestId && `${dataTestId}-legend`}>
|
|
114
120
|
<Text variant="small" color={reversed ? 'white' : 'dark'}>
|
|
@@ -21,3 +21,11 @@ Likert scale radio buttons let people select one option in a Likert scale rangin
|
|
|
21
21
|
|
|
22
22
|
<Canvas of={LikertScaleLegacyStories.Playground} />
|
|
23
23
|
<Controls of={LikertScaleLegacyStories.Playground} />
|
|
24
|
+
|
|
25
|
+
## API
|
|
26
|
+
|
|
27
|
+
### isRequired
|
|
28
|
+
|
|
29
|
+
Sets aria-required value on radiogroup for assistive technologies. An accessible label must be provided and validation must still be handled within implementations.
|
|
30
|
+
|
|
31
|
+
<Canvas of={LikertScaleLegacyStories.IsRequired} />
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/react'
|
|
3
|
+
import { expect, within } from '@storybook/test'
|
|
4
|
+
import { VisuallyHidden } from '~components/VisuallyHidden'
|
|
3
5
|
import { LikertScaleLegacy } from '../index'
|
|
4
6
|
import { type Scale, type ScaleItem } from '../types'
|
|
5
7
|
|
|
@@ -57,7 +59,7 @@ export const Playground: Story = {
|
|
|
57
59
|
code: `
|
|
58
60
|
const SatisfactionExample = () => {
|
|
59
61
|
const [selectedItem, setSelectedItem] = useState<ScaleItem | null>(null)
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
return (
|
|
62
64
|
<LikertScaleLegacy
|
|
63
65
|
scale={[
|
|
@@ -82,3 +84,30 @@ export const Playground: Story = {
|
|
|
82
84
|
},
|
|
83
85
|
},
|
|
84
86
|
}
|
|
87
|
+
|
|
88
|
+
export const IsRequired: Story = {
|
|
89
|
+
render: (args) => {
|
|
90
|
+
const [selectedItem, setSelectedItem] = useState<ScaleItem | null>(null)
|
|
91
|
+
const labelId = React.useId()
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<VisuallyHidden id={labelId}>Likert scale label</VisuallyHidden>
|
|
95
|
+
<LikertScaleLegacy
|
|
96
|
+
{...args}
|
|
97
|
+
labelId={labelId}
|
|
98
|
+
selectedItem={selectedItem}
|
|
99
|
+
onSelect={setSelectedItem}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
args: {
|
|
105
|
+
isRequired: true,
|
|
106
|
+
},
|
|
107
|
+
play: async ({ canvasElement }) => {
|
|
108
|
+
const canvas = within(canvasElement.parentElement!)
|
|
109
|
+
const likertScale = canvas.getByRole('radiogroup', { name: 'Likert scale label' })
|
|
110
|
+
|
|
111
|
+
expect(likertScale).toHaveAttribute('aria-required', 'true')
|
|
112
|
+
},
|
|
113
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Meta } from '@storybook/blocks'
|
|
2
|
+
|
|
3
|
+
<Meta title="Components/Menu/Migration guide" />
|
|
4
|
+
|
|
5
|
+
# Menu migration guide
|
|
6
|
+
|
|
7
|
+
## Audience
|
|
8
|
+
|
|
9
|
+
This guide is relevant for Kaizen All-In-One (KAIO) v1 consumers.
|
|
10
|
+
|
|
11
|
+
## Purpose
|
|
12
|
+
|
|
13
|
+
This guide provides instructions for migrating menu usage from the `deprecated` (`@kaizen/components`) `Menu` component to the `next` (`@kaizen/components/next`) `Menu` component.
|
|
14
|
+
|
|
15
|
+
This migration is a prerequisite for [migrating to KAIO v2](/docs/releases-upcoming-major-releases--docs).
|
|
16
|
+
|
|
17
|
+
## Key API changes
|
|
18
|
+
|
|
19
|
+
`next/Menu` separates its functionality into the following components:
|
|
20
|
+
|
|
21
|
+
- `MenuTrigger` wraps the `MenuPopover` component and its trigger element.
|
|
22
|
+
- `MenuPopover` contains a `Menu` component, and controls the popover placement and open and close interactions.
|
|
23
|
+
- `Menu` contains one or more `MenuItem` and `MenuSection` components.
|
|
24
|
+
- `MenuSection` enables menu items to be grouped into sections.
|
|
25
|
+
- `MenuHeader` provides a section's header content.
|
|
26
|
+
- `MenuItem` provides a menu item's content, and handles item selection.
|
|
27
|
+
|
|
28
|
+
Other notable changes:
|
|
29
|
+
|
|
30
|
+
- `Menu.align` prop becomes MenuPopover.placement, and values are mapped as follows:
|
|
31
|
+
- `left` becomes `start`
|
|
32
|
+
- `right` becomes `end`
|
|
33
|
+
- `Menu.autoHide` prop is retired
|
|
34
|
+
- `Menu.button` prop becomes `MenuTrigger.children`
|
|
35
|
+
- The trigger element must be a `next/Button`
|
|
36
|
+
- `Menu.dropdownWidth` prop is retired
|
|
37
|
+
- `Menu.portalSelector` prop is retired
|
|
38
|
+
- Where needed, [PortalProvider](https://react-spectrum.adobe.com/react-aria/PortalProvider.html) can be used to control portalling behaviour
|
|
39
|
+
- `MenuItem.destructive` prop is retired
|
|
40
|
+
- This change aligns with a broader move towards more judicious use of colour
|
|
41
|
+
- `MenuItem.disabled` prop becomes `MenuItem.isDisabled`
|
|
42
|
+
- `MenuItem.label` prop becomes `MenuItem.children`
|
|
43
|
+
- `MenuItem.onClick` prop becomes `MenuItem.onAction`
|
|
44
|
+
- React Aria's `Menu` does not expose native click events, e.g. `MenuItem.onAction` cannot call `e.preventDefault()`
|
|
45
|
+
- See React Aria [Menu documentation](https://react-spectrum.adobe.com/react-aria/Menu.html) for more details on working with `Menu` and `MenuItem` events
|
|
46
|
+
- `MenuList.heading` prop becomes `MenuHeader` in a `MenuSection`
|
|
47
|
+
|
|
48
|
+
## Migration example
|
|
49
|
+
|
|
50
|
+
### Before
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<Menu button={<Button>Trigger</Button>}>
|
|
54
|
+
<MenuList>
|
|
55
|
+
<MenuList heading={<MenuHeading>Section One</MenuHeading>}>
|
|
56
|
+
<MenuItem onClick={() => alert('1')} label="Item 1" />
|
|
57
|
+
<MenuItem onClick={() => alert('2')} label="Item 2" />
|
|
58
|
+
</MenuList>
|
|
59
|
+
<MenuList heading={<MenuHeading>Section Two</MenuHeading>}>
|
|
60
|
+
<MenuItem onClick={() => alert('3')} label="Item 3" />
|
|
61
|
+
<MenuItem onClick={() => alert('4')} label="Item 4" />
|
|
62
|
+
</MenuList>
|
|
63
|
+
</MenuList>
|
|
64
|
+
</Menu>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### After
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
<MenuTrigger>
|
|
71
|
+
<Button>Trigger</Button>
|
|
72
|
+
<MenuPopover>
|
|
73
|
+
<Menu>
|
|
74
|
+
<MenuSection>
|
|
75
|
+
<MenuHeader>Section One</MenuHeader>
|
|
76
|
+
<MenuItem onAction={() => alert('1')}>Item 1</MenuItem>
|
|
77
|
+
<MenuItem onAction={() => alert('2')}>Item 2</MenuItem>
|
|
78
|
+
</MenuSection>
|
|
79
|
+
<MenuSection>
|
|
80
|
+
<MenuHeader>Section Two</MenuHeader>
|
|
81
|
+
<MenuItem onAction={() => alert('3')}>Item 3</MenuItem>
|
|
82
|
+
<MenuItem onAction={() => alert('4')}>Item 4</MenuItem>
|
|
83
|
+
</MenuSection>
|
|
84
|
+
</Menu>
|
|
85
|
+
</MenuPopover>
|
|
86
|
+
</MenuTrigger>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## More information
|
|
90
|
+
|
|
91
|
+
More information about `next/Menu` can be found at [API Specification](/docs/components-menu-menu-next-api-specification--docs) and [Usage Guidelines](/docs/components-menu-menu-next-usage-guidelines--docs).
|
|
@@ -89,6 +89,7 @@ export const Select = <Option extends SelectOption = SelectOption>({
|
|
|
89
89
|
status,
|
|
90
90
|
validationMessage,
|
|
91
91
|
isReversed,
|
|
92
|
+
isRequired = false,
|
|
92
93
|
isFullWidth,
|
|
93
94
|
disabledValues,
|
|
94
95
|
classNameOverride,
|
|
@@ -119,6 +120,7 @@ export const Select = <Option extends SelectOption = SelectOption>({
|
|
|
119
120
|
description,
|
|
120
121
|
placeholder,
|
|
121
122
|
isDisabled,
|
|
123
|
+
isRequired,
|
|
122
124
|
onSelectionChange: onSelectionChange ? (key) => onSelectionChange(key!) : undefined,
|
|
123
125
|
...restProps,
|
|
124
126
|
}
|
|
@@ -159,6 +161,7 @@ export const Select = <Option extends SelectOption = SelectOption>({
|
|
|
159
161
|
isReversed,
|
|
160
162
|
'ref': refs.setReference,
|
|
161
163
|
'aria-describedby': classnames(validationMessage && validationId, description && descriptionId),
|
|
164
|
+
'aria-required': isRequired,
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const [portalContainer, setPortalContainer] = useState<HTMLElement>()
|
|
@@ -92,6 +92,14 @@ Add validation messages using `status` and `validationMessage`.
|
|
|
92
92
|
|
|
93
93
|
<Canvas of={SelectStories.Validation} />
|
|
94
94
|
|
|
95
|
+
#### isRequired and validationBehavior
|
|
96
|
+
|
|
97
|
+
When using the `isRequired` property you can also specify the `validationBehavior` to change from `aria` to `native` form validation.
|
|
98
|
+
|
|
99
|
+
<Canvas of={SelectStories.SelectNativeValidationBehavior} />
|
|
100
|
+
|
|
101
|
+
While both use `aria-required` to announce whether the field has to have a value to assistive technologies, the `native` will option will prevent form submissions if the `selectedKey` is `undefined`.
|
|
102
|
+
|
|
95
103
|
### Full width
|
|
96
104
|
|
|
97
105
|
Set `isFullWidth` to `true` to have the Select span the full width of its container.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/react'
|
|
3
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test'
|
|
4
|
+
import { FieldMessage } from '~components/FieldMessage'
|
|
3
5
|
import { ContextModal } from '~components/Modal'
|
|
4
6
|
import { RadioField, RadioGroup } from '~components/Radio'
|
|
7
|
+
import { Button } from '~components/__next__'
|
|
5
8
|
import { Select } from '../Select'
|
|
6
9
|
import { type SelectOption } from '../types'
|
|
7
10
|
import { groupedMockItems, mixedMockItemsDisabled, singleMockItems } from './mockData'
|
|
@@ -229,3 +232,93 @@ export const TouchDeviceTest: Story = {
|
|
|
229
232
|
)
|
|
230
233
|
},
|
|
231
234
|
}
|
|
235
|
+
|
|
236
|
+
export const RequiredSelect: Story = {
|
|
237
|
+
args: {
|
|
238
|
+
label: 'Required Select',
|
|
239
|
+
isRequired: true,
|
|
240
|
+
validationBehavior: 'native',
|
|
241
|
+
},
|
|
242
|
+
render: (args) => <Select {...args} />,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export const SelectNativeValidationBehavior: Story = {
|
|
246
|
+
parameters: {
|
|
247
|
+
name: 'Required Select with native form validation',
|
|
248
|
+
},
|
|
249
|
+
args: {
|
|
250
|
+
label: 'Required Select',
|
|
251
|
+
isRequired: true,
|
|
252
|
+
validationBehavior: 'native',
|
|
253
|
+
},
|
|
254
|
+
render: (args) => {
|
|
255
|
+
const [hasSubmitted, setHasSubmitted] = React.useState(false)
|
|
256
|
+
return (
|
|
257
|
+
<div>
|
|
258
|
+
<form
|
|
259
|
+
className="flex flex-col gap-16"
|
|
260
|
+
name="form-with-required-select"
|
|
261
|
+
aria-describedby={hasSubmitted ? 'id--field-message-form' : undefined}
|
|
262
|
+
onSubmit={(e) => {
|
|
263
|
+
e.preventDefault()
|
|
264
|
+
setHasSubmitted(true)
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<Select {...args} isRequired />
|
|
268
|
+
<div>
|
|
269
|
+
<Button type="submit">Submit</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</form>
|
|
272
|
+
{hasSubmitted && (
|
|
273
|
+
<FieldMessage
|
|
274
|
+
id="id--field-message-form"
|
|
275
|
+
classNameOverride="mt-8"
|
|
276
|
+
status="success"
|
|
277
|
+
message={'Form submitted!'}
|
|
278
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export const NativeFormValidationWithoutSelectedVal: Story = {
|
|
286
|
+
...SelectNativeValidationBehavior,
|
|
287
|
+
play: async ({ canvasElement, step }) => {
|
|
288
|
+
const canvas = within(canvasElement.parentElement!)
|
|
289
|
+
const submitButton = canvas.getByRole('button', { name: 'Submit' })
|
|
290
|
+
const requiredSelect = canvas.getByRole('combobox', { name: 'Required Select' })
|
|
291
|
+
const form = await canvas.findByRole('form')
|
|
292
|
+
|
|
293
|
+
await step('Select has aria-required attribute', async () => {
|
|
294
|
+
expect(requiredSelect).toHaveAttribute('aria-required', 'true')
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await step('Submit will not call onSubmit without a selected value', async () => {
|
|
298
|
+
await userEvent.click(submitButton)
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(form).toHaveAccessibleDescription('')
|
|
301
|
+
})
|
|
302
|
+
})
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const NativeFormValidationWithSelectedVal: Story = {
|
|
307
|
+
...SelectNativeValidationBehavior,
|
|
308
|
+
args: {
|
|
309
|
+
selectedKey: 'short-black',
|
|
310
|
+
},
|
|
311
|
+
play: async ({ canvasElement, step }) => {
|
|
312
|
+
const canvas = within(canvasElement.parentElement!)
|
|
313
|
+
const submitButton = canvas.getByRole('button', { name: 'Submit' })
|
|
314
|
+
const form = await canvas.findByRole('form')
|
|
315
|
+
|
|
316
|
+
await step('Submit will call onSubmit with a selected value', async () => {
|
|
317
|
+
await userEvent.click(submitButton)
|
|
318
|
+
|
|
319
|
+
await waitFor(() => {
|
|
320
|
+
expect(form).toHaveAccessibleDescription('Form submitted!')
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
},
|
|
324
|
+
}
|