@kaizen/components 1.35.1 → 1.35.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Button/GenericButton/GenericButton.cjs +1 -1
- package/dist/cjs/Button/GenericButton/GenericButton.cjs.map +1 -1
- package/dist/cjs/MultiSelect/subcomponents/Popover/Popover.cjs +1 -1
- package/dist/cjs/MultiSelect/subcomponents/Popover/Popover.cjs.map +1 -1
- package/dist/cjs/__future__/Select/Select.cjs +10 -1
- package/dist/cjs/__future__/Select/Select.cjs.map +1 -1
- package/dist/cjs/dts/MultiSelect/MultiSelect.d.ts +4 -2
- package/dist/cjs/dts/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.d.ts +3 -0
- package/dist/cjs/dts/MultiSelect/types.d.ts +6 -0
- package/dist/cjs/index.css +3 -3
- package/dist/esm/Button/GenericButton/GenericButton.mjs +1 -1
- package/dist/esm/Button/GenericButton/GenericButton.mjs.map +1 -1
- package/dist/esm/MultiSelect/subcomponents/Popover/Popover.mjs +1 -1
- package/dist/esm/MultiSelect/subcomponents/Popover/Popover.mjs.map +1 -1
- package/dist/esm/__future__/Select/Select.mjs +10 -1
- package/dist/esm/__future__/Select/Select.mjs.map +1 -1
- package/dist/esm/dts/MultiSelect/MultiSelect.d.ts +4 -2
- package/dist/esm/dts/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.d.ts +3 -0
- package/dist/esm/dts/MultiSelect/types.d.ts +6 -0
- package/dist/esm/index.css +3 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/Button/Button/_docs/Button.mdx +5 -0
- package/src/Button/Button/_docs/Button.stories.tsx +27 -1
- package/src/Button/GenericButton/GenericButton.spec.tsx +69 -0
- package/src/Button/GenericButton/GenericButton.tsx +1 -1
- package/src/MultiSelect/MultiSelect.spec.tsx +56 -1
- package/src/MultiSelect/MultiSelect.tsx +10 -3
- package/src/MultiSelect/_docs/MultiSelect.mdx +10 -0
- package/src/MultiSelect/_docs/MultiSelect.stickersheet.stories.tsx +81 -43
- package/src/MultiSelect/_docs/MultiSelect.stories.tsx +21 -0
- package/src/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.module.scss +9 -0
- package/src/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.tsx +8 -1
- package/src/MultiSelect/subcomponents/MultiSelectToggle/_docs/MultiSelectToggle.stickersheet.stories.tsx +17 -0
- package/src/MultiSelect/subcomponents/Popover/Popover.tsx +1 -1
- package/src/MultiSelect/types.ts +7 -0
- package/src/__future__/Select/Select.spec.tsx +31 -12
- package/src/__future__/Select/Select.tsx +14 -1
package/package.json
CHANGED
|
@@ -82,6 +82,11 @@ Alternatively use the `workingLabelHidden` prop to hide the button label all tog
|
|
|
82
82
|
|
|
83
83
|
<Canvas of={ButtonStories.Working} />
|
|
84
84
|
|
|
85
|
+
### Controlling the working state
|
|
86
|
+
Here is an example of controlling whether the button is 'working' or not, using state.
|
|
87
|
+
|
|
88
|
+
<Canvas of={ButtonStories.ResolveWorking} />
|
|
89
|
+
|
|
85
90
|
### Loading
|
|
86
91
|
|
|
87
92
|
Use the <LinkTo pageId="components-loading-states-loadinginput">LoadingInput</LinkTo> component for button placeholders.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react"
|
|
1
|
+
import React, { useState } from "react"
|
|
2
2
|
import { StoryObj, Meta } from "@storybook/react"
|
|
3
3
|
import { AddIcon, ArrowRightIcon } from "~components/Icon"
|
|
4
4
|
import { LoadingInput } from "~components/Loading"
|
|
@@ -142,3 +142,29 @@ export const NativeFormButton: Story = {
|
|
|
142
142
|
),
|
|
143
143
|
parameters: { controls: { disable: true } },
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
export const ResolveWorking: Story = {
|
|
147
|
+
render: () => {
|
|
148
|
+
const [state, setState] = useState<"Ready" | "Working" | "Completed">(
|
|
149
|
+
"Ready"
|
|
150
|
+
)
|
|
151
|
+
const handleClick = (): void => {
|
|
152
|
+
if (state === "Ready") {
|
|
153
|
+
setState("Working")
|
|
154
|
+
setTimeout(() => setState("Completed"), 3000)
|
|
155
|
+
} else {
|
|
156
|
+
setState("Ready")
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<Button
|
|
162
|
+
label={state}
|
|
163
|
+
working={state === "Working"}
|
|
164
|
+
workingLabel="Button is doing some work"
|
|
165
|
+
workingLabelHidden
|
|
166
|
+
onClick={handleClick}
|
|
167
|
+
/>
|
|
168
|
+
)
|
|
169
|
+
},
|
|
170
|
+
}
|
|
@@ -111,3 +111,72 @@ describe("<GenericButton /> with native HTML `form` attributes", () => {
|
|
|
111
111
|
).toHaveAttribute("form", buttonFormAttributes.form)
|
|
112
112
|
})
|
|
113
113
|
})
|
|
114
|
+
|
|
115
|
+
describe("<GenericButton /> `working` accessible states", () => {
|
|
116
|
+
it("renders a button without aria-live by default", () => {
|
|
117
|
+
const { getByTestId } = render(
|
|
118
|
+
<GenericButton
|
|
119
|
+
data-testid="id--generic-test"
|
|
120
|
+
id="id--button"
|
|
121
|
+
label="button label"
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const button = getByTestId("id--generic-test")
|
|
126
|
+
// The id is passed to the element not the container so we have to get its parent
|
|
127
|
+
const buttonContainer = button.parentElement
|
|
128
|
+
|
|
129
|
+
expect(buttonContainer).not.toHaveAttribute("aria-live", "")
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it("renders a button with aria-live if working label if provided", () => {
|
|
133
|
+
const { getByTestId } = render(
|
|
134
|
+
<GenericButton
|
|
135
|
+
data-testid="id--generic-test"
|
|
136
|
+
id="id--button"
|
|
137
|
+
label="button label"
|
|
138
|
+
workingLabel="Loading"
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
const button = getByTestId("id--generic-test")
|
|
142
|
+
const buttonContainer = button.parentElement
|
|
143
|
+
|
|
144
|
+
expect(buttonContainer).toHaveAttribute("aria-live", "polite")
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("renders a link button with aria-live if working label if provided", () => {
|
|
148
|
+
const { getByTestId } = render(
|
|
149
|
+
<GenericButton
|
|
150
|
+
data-testid="id--generic-test"
|
|
151
|
+
id="id--button"
|
|
152
|
+
label="button label"
|
|
153
|
+
workingLabel="Loading"
|
|
154
|
+
href="/"
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
const button = getByTestId("id--generic-test")
|
|
158
|
+
const buttonContainer = button.parentElement
|
|
159
|
+
|
|
160
|
+
expect(buttonContainer).toHaveAttribute("aria-live", "polite")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it("renders a custom button with aria-live if working label if provided", () => {
|
|
164
|
+
const { getByTestId } = render(
|
|
165
|
+
<GenericButton
|
|
166
|
+
data-testid="id--generic-test"
|
|
167
|
+
id="id--button"
|
|
168
|
+
label="button label"
|
|
169
|
+
workingLabel="Loading"
|
|
170
|
+
component={props => (
|
|
171
|
+
<button type="button" {...props}>
|
|
172
|
+
Custom button
|
|
173
|
+
</button>
|
|
174
|
+
)}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
const button = getByTestId("id--generic-test")
|
|
178
|
+
const buttonContainer = button.parentElement
|
|
179
|
+
|
|
180
|
+
expect(buttonContainer).toHaveAttribute("aria-live", "polite")
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react"
|
|
2
|
-
import { render, waitFor } from "@testing-library/react"
|
|
2
|
+
import { render, waitFor, screen } from "@testing-library/react"
|
|
3
3
|
import userEvent from "@testing-library/user-event"
|
|
4
4
|
import { MultiSelect, MultiSelectProps } from "./MultiSelect"
|
|
5
5
|
|
|
@@ -331,3 +331,58 @@ describe("Removing all options", () => {
|
|
|
331
331
|
})
|
|
332
332
|
})
|
|
333
333
|
})
|
|
334
|
+
|
|
335
|
+
describe("Has validation status", () => {
|
|
336
|
+
it("renders a validation message", () => {
|
|
337
|
+
render(
|
|
338
|
+
<MultiSelectWrapper
|
|
339
|
+
selectedValues={new Set(["waffle"])}
|
|
340
|
+
validationMessage={{
|
|
341
|
+
status: "error",
|
|
342
|
+
message: "No waffles are available",
|
|
343
|
+
}}
|
|
344
|
+
/>
|
|
345
|
+
)
|
|
346
|
+
expect(screen.getByText("No waffles are available")).toBeInTheDocument()
|
|
347
|
+
})
|
|
348
|
+
it("describes the Toggle", () => {
|
|
349
|
+
render(
|
|
350
|
+
<MultiSelectWrapper
|
|
351
|
+
selectedValues={new Set(["waffle"])}
|
|
352
|
+
label="Breakfast menu"
|
|
353
|
+
validationMessage={{
|
|
354
|
+
status: "caution",
|
|
355
|
+
message: "Only four waffles remain",
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
358
|
+
)
|
|
359
|
+
expect(
|
|
360
|
+
screen.getByRole("button", {
|
|
361
|
+
name: "Breakfast menu",
|
|
362
|
+
description: "Only four waffles remain",
|
|
363
|
+
})
|
|
364
|
+
).toBeInTheDocument()
|
|
365
|
+
})
|
|
366
|
+
it("announces the validation message before the Toggle's description", () => {
|
|
367
|
+
const description = "Choose you breakfast."
|
|
368
|
+
const validationMessage = "Only four waffles remain."
|
|
369
|
+
|
|
370
|
+
render(
|
|
371
|
+
<MultiSelectWrapper
|
|
372
|
+
selectedValues={new Set(["waffle"])}
|
|
373
|
+
label="Breakfast menu"
|
|
374
|
+
description={description}
|
|
375
|
+
validationMessage={{
|
|
376
|
+
status: "caution",
|
|
377
|
+
message: "Only four waffles remain.",
|
|
378
|
+
}}
|
|
379
|
+
/>
|
|
380
|
+
)
|
|
381
|
+
expect(
|
|
382
|
+
screen.getByRole("button", {
|
|
383
|
+
name: "Breakfast menu",
|
|
384
|
+
description: `${validationMessage} ${description}`,
|
|
385
|
+
})
|
|
386
|
+
).toBeInTheDocument()
|
|
387
|
+
})
|
|
388
|
+
})
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./subcomponents/MultiSelectOptions"
|
|
11
11
|
import { MultiSelectToggle } from "./subcomponents/MultiSelectToggle"
|
|
12
12
|
import { Popover, useFloating } from "./subcomponents/Popover"
|
|
13
|
-
import { MultiSelectOption } from "./types"
|
|
13
|
+
import { MultiSelectOption, ValidationMessage } from "./types"
|
|
14
14
|
import styles from "./MultiSelect.module.scss"
|
|
15
15
|
|
|
16
16
|
export type MultiSelectProps = {
|
|
@@ -24,6 +24,8 @@ export type MultiSelectProps = {
|
|
|
24
24
|
onSelectedValuesChange: MultiSelectOptionsProps["onChange"]
|
|
25
25
|
isOpen: boolean
|
|
26
26
|
onOpenChange: (isOpen: boolean) => void
|
|
27
|
+
/** A status and message to provide context to the validation issue */
|
|
28
|
+
validationMessage?: ValidationMessage
|
|
27
29
|
} & OverrideClassName<HTMLAttributes<HTMLDivElement>>
|
|
28
30
|
|
|
29
31
|
export const MultiSelect = ({
|
|
@@ -36,10 +38,12 @@ export const MultiSelect = ({
|
|
|
36
38
|
isOpen,
|
|
37
39
|
onOpenChange,
|
|
38
40
|
classNameOverride,
|
|
41
|
+
validationMessage,
|
|
39
42
|
...restProps
|
|
40
43
|
}: MultiSelectProps): JSX.Element => {
|
|
41
44
|
const id = propsId ?? useId()
|
|
42
45
|
const descriptionId = `${id}-description`
|
|
46
|
+
const validationId = `${id}-validation-message`
|
|
43
47
|
|
|
44
48
|
const toggleButtonRef = useRef<HTMLButtonElement>(null)
|
|
45
49
|
const { refs } = useFloating()
|
|
@@ -88,20 +92,23 @@ export const MultiSelect = ({
|
|
|
88
92
|
ref={toggleButtonRef}
|
|
89
93
|
id={`${id}--toggle`}
|
|
90
94
|
aria-labelledby={`${id}--label`}
|
|
91
|
-
aria-describedby={descriptionId}
|
|
95
|
+
aria-describedby={`${validationId} ${descriptionId}`}
|
|
92
96
|
aria-controls={`${id}--popover`}
|
|
93
97
|
onClick={handleToggleClick}
|
|
94
98
|
isOpen={isOpen}
|
|
95
99
|
selectedOptions={Array.from(selectedValues).map(
|
|
96
100
|
value => itemsMap[value]
|
|
97
101
|
)}
|
|
102
|
+
status={validationMessage?.status}
|
|
98
103
|
onRemoveOption={handleOnRemoveOption}
|
|
99
104
|
onRemoveAllOptions={handleRemoveAllOptions}
|
|
100
105
|
/>
|
|
101
106
|
</div>
|
|
102
107
|
|
|
108
|
+
{validationMessage && (
|
|
109
|
+
<FieldMessage id={validationId} {...validationMessage} />
|
|
110
|
+
)}
|
|
103
111
|
{description && <FieldMessage id={descriptionId} message={description} />}
|
|
104
|
-
{/* ValidationMessage */}
|
|
105
112
|
|
|
106
113
|
{isOpen && (
|
|
107
114
|
<Popover
|
|
@@ -64,3 +64,13 @@ The open state is controlled by the consumer through the `isOpen` and `onOpenCha
|
|
|
64
64
|
You can add a description which will be linked to the trigger button with aria-describedby.
|
|
65
65
|
|
|
66
66
|
<Canvas of={MultiSelectStories.Description} />
|
|
67
|
+
|
|
68
|
+
### Validation
|
|
69
|
+
|
|
70
|
+
Validation states are defined in the `validationMessage` object, containing a `status` and `message`.
|
|
71
|
+
|
|
72
|
+
<Canvas of={MultiSelectStories.ValidationMessage} />
|
|
73
|
+
|
|
74
|
+
A validation message will render below the MultiSelect and will be read to assistive technologies before the `description` content.
|
|
75
|
+
|
|
76
|
+
<Canvas of={MultiSelectStories.ValidationMessageWithDescription} />
|
|
@@ -51,49 +51,87 @@ const StickerSheetTemplate: StickerSheetStory = {
|
|
|
51
51
|
>(new Set())
|
|
52
52
|
|
|
53
53
|
return (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<StickerSheet.
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
54
|
+
<>
|
|
55
|
+
<StickerSheet
|
|
56
|
+
heading="MultiSelect"
|
|
57
|
+
className={classnames("w-full", IS_CHROMATIC && "pb-160")}
|
|
58
|
+
>
|
|
59
|
+
<StickerSheet.Header
|
|
60
|
+
headings={["Closed", "Open", "No items"]}
|
|
61
|
+
headingsWidth="30%"
|
|
62
|
+
/>
|
|
63
|
+
<StickerSheet.Body>
|
|
64
|
+
<StickerSheet.Row>
|
|
65
|
+
<MultiSelect
|
|
66
|
+
id="id--multi-select-options--closed"
|
|
67
|
+
label="Label"
|
|
68
|
+
description="A short description"
|
|
69
|
+
isOpen={isOpenClosed}
|
|
70
|
+
onOpenChange={setIsOpenClosed}
|
|
71
|
+
onSelectedValuesChange={setSelectedValuesClosed}
|
|
72
|
+
selectedValues={selectedValuesClosed}
|
|
73
|
+
items={options}
|
|
74
|
+
/>
|
|
75
|
+
<MultiSelect
|
|
76
|
+
id="id--multi-select-options--open"
|
|
77
|
+
label="Label"
|
|
78
|
+
description="A short description"
|
|
79
|
+
isOpen={isOpenOpen}
|
|
80
|
+
onOpenChange={setIsOpenOpen}
|
|
81
|
+
onSelectedValuesChange={setSelectedValuesOpen}
|
|
82
|
+
selectedValues={selectedValuesOpen}
|
|
83
|
+
items={options}
|
|
84
|
+
/>
|
|
85
|
+
<MultiSelect
|
|
86
|
+
id="id--multi-select-options--no-items"
|
|
87
|
+
label="Label"
|
|
88
|
+
description="A short description"
|
|
89
|
+
isOpen={isOpenNoItems}
|
|
90
|
+
onOpenChange={setIsOpenNoItems}
|
|
91
|
+
selectedValues={selectedValuesNoItems}
|
|
92
|
+
onSelectedValuesChange={setSelectedValuesNoItems}
|
|
93
|
+
items={[]}
|
|
94
|
+
/>
|
|
95
|
+
</StickerSheet.Row>
|
|
96
|
+
</StickerSheet.Body>
|
|
97
|
+
</StickerSheet>
|
|
98
|
+
<StickerSheet
|
|
99
|
+
heading="Validation"
|
|
100
|
+
className={classnames("w-full", IS_CHROMATIC && "pb-160")}
|
|
101
|
+
>
|
|
102
|
+
<StickerSheet.Header headings={["Error", "Caution"]} />
|
|
103
|
+
<StickerSheet.Body>
|
|
104
|
+
<StickerSheet.Row>
|
|
105
|
+
<MultiSelect
|
|
106
|
+
id="id--multi-select--error"
|
|
107
|
+
label="Label"
|
|
108
|
+
isOpen={false}
|
|
109
|
+
onOpenChange={() => undefined}
|
|
110
|
+
onSelectedValuesChange={() => undefined}
|
|
111
|
+
selectedValues={selectedValuesOpen}
|
|
112
|
+
items={options}
|
|
113
|
+
validationMessage={{
|
|
114
|
+
status: "error",
|
|
115
|
+
message: "There are no waffles left.",
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
<MultiSelect
|
|
119
|
+
id="id--multi-select--caution"
|
|
120
|
+
label="Label"
|
|
121
|
+
isOpen={false}
|
|
122
|
+
onOpenChange={() => undefined}
|
|
123
|
+
onSelectedValuesChange={() => undefined}
|
|
124
|
+
selectedValues={selectedValuesNoItems}
|
|
125
|
+
items={options}
|
|
126
|
+
validationMessage={{
|
|
127
|
+
status: "error",
|
|
128
|
+
message: "There are only four pancakes left.",
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
</StickerSheet.Row>
|
|
132
|
+
</StickerSheet.Body>
|
|
133
|
+
</StickerSheet>
|
|
134
|
+
</>
|
|
97
135
|
)
|
|
98
136
|
},
|
|
99
137
|
}
|
|
@@ -150,3 +150,24 @@ export const Description: Story = {
|
|
|
150
150
|
description: "Breakfast will be delivered directly to your house",
|
|
151
151
|
},
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
export const ValidationMessage: Story = {
|
|
155
|
+
...MultiSelectTemplate,
|
|
156
|
+
args: {
|
|
157
|
+
validationMessage: {
|
|
158
|
+
status: "error",
|
|
159
|
+
message: "There are no more waffles",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const ValidationMessageWithDescription: Story = {
|
|
165
|
+
...MultiSelectTemplate,
|
|
166
|
+
args: {
|
|
167
|
+
description: "Breakfast will be delivered to your house.",
|
|
168
|
+
validationMessage: {
|
|
169
|
+
status: "caution",
|
|
170
|
+
message: "Only five more waffles remain.",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
border-color: $color-gray-600;
|
|
19
19
|
background-color: $color-gray-200;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
&.error {
|
|
23
|
+
border: $border-solid-border-style $border-solid-border-width $color-red-500;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.caution {
|
|
27
|
+
border: $border-solid-border-style $border-solid-border-width
|
|
28
|
+
$color-yellow-600;
|
|
29
|
+
}
|
|
21
30
|
}
|
|
22
31
|
|
|
23
32
|
.toggleButton {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { HTMLAttributes, forwardRef } from "react"
|
|
2
2
|
import classnames from "classnames"
|
|
3
3
|
import { ClearButton } from "~components/ClearButton"
|
|
4
|
+
import { FieldMessageProps } from "~components/FieldMessage"
|
|
4
5
|
import { ChevronDownIcon, ChevronUpIcon } from "~components/Icon"
|
|
5
6
|
import { RemovableTag } from "~components/__future__/Tag"
|
|
6
7
|
import { OverrideClassName } from "~types/OverrideClassName"
|
|
@@ -13,6 +14,7 @@ export type MultiSelectToggleProps = {
|
|
|
13
14
|
["aria-controls"]: string
|
|
14
15
|
selectedOptions: MultiSelectOption[]
|
|
15
16
|
isOpen?: boolean
|
|
17
|
+
status?: FieldMessageProps["status"]
|
|
16
18
|
onRemoveOption: (optionValue: MultiSelectOption["value"]) => void
|
|
17
19
|
onRemoveAllOptions: () => void
|
|
18
20
|
} & OverrideClassName<HTMLAttributes<HTMLDivElement>>
|
|
@@ -32,6 +34,7 @@ export const MultiSelectToggle = forwardRef<
|
|
|
32
34
|
selectedOptions,
|
|
33
35
|
onRemoveOption,
|
|
34
36
|
onRemoveAllOptions,
|
|
37
|
+
status,
|
|
35
38
|
...restProps
|
|
36
39
|
},
|
|
37
40
|
ref
|
|
@@ -43,7 +46,11 @@ export const MultiSelectToggle = forwardRef<
|
|
|
43
46
|
*/}
|
|
44
47
|
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
|
45
48
|
<div
|
|
46
|
-
className={classnames(
|
|
49
|
+
className={classnames(
|
|
50
|
+
styles.multiSelectToggle,
|
|
51
|
+
classNameOverride,
|
|
52
|
+
status && styles[status]
|
|
53
|
+
)}
|
|
47
54
|
onClick={onClick}
|
|
48
55
|
{...restProps}
|
|
49
56
|
>
|
|
@@ -92,6 +92,23 @@ const StickerSheetTemplate: StickerSheetStory = {
|
|
|
92
92
|
</StickerSheet.Body>
|
|
93
93
|
</StickerSheet>
|
|
94
94
|
|
|
95
|
+
<StickerSheet
|
|
96
|
+
isReversed={isReversed}
|
|
97
|
+
heading="Validation states"
|
|
98
|
+
className="w-full"
|
|
99
|
+
>
|
|
100
|
+
<StickerSheet.Header
|
|
101
|
+
headings={["Error", "Caution"]}
|
|
102
|
+
verticalHeadingsWidth="10rem"
|
|
103
|
+
/>
|
|
104
|
+
<StickerSheet.Body>
|
|
105
|
+
<StickerSheet.Row>
|
|
106
|
+
<MultiSelectToggle status="error" {...defaultProps} />
|
|
107
|
+
<MultiSelectToggle status="caution" {...defaultProps} />
|
|
108
|
+
</StickerSheet.Row>
|
|
109
|
+
</StickerSheet.Body>
|
|
110
|
+
</StickerSheet>
|
|
111
|
+
|
|
95
112
|
<StickerSheet
|
|
96
113
|
isReversed={isReversed}
|
|
97
114
|
heading="Has selected values"
|
package/src/MultiSelect/types.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { FieldMessageStatus } from "~components/FieldMessage"
|
|
2
|
+
|
|
1
3
|
export type MultiSelectOption = {
|
|
2
4
|
label: string
|
|
3
5
|
// Used for "value" attribute in option - must be unique
|
|
4
6
|
value: string | number
|
|
5
7
|
}
|
|
8
|
+
|
|
9
|
+
export type ValidationMessage = {
|
|
10
|
+
status?: Extract<FieldMessageStatus, "error" | "caution">
|
|
11
|
+
message: string | React.ReactElement
|
|
12
|
+
}
|
|
@@ -33,14 +33,35 @@ const SelectWrapper = ({
|
|
|
33
33
|
|
|
34
34
|
describe("<Select />", () => {
|
|
35
35
|
describe("Trigger", () => {
|
|
36
|
-
it("
|
|
36
|
+
it("has the label as the accessible name", () => {
|
|
37
|
+
const { getByRole } = render(<SelectWrapper />)
|
|
38
|
+
const menu = getByRole("combobox", {
|
|
39
|
+
name: "Mock Label",
|
|
40
|
+
})
|
|
41
|
+
expect(menu).toBeInTheDocument()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("has the value when an item is selected", () => {
|
|
37
45
|
const { getByRole } = render(<SelectWrapper selectedKey="batch-brew" />)
|
|
38
46
|
const menu = getByRole("combobox", {
|
|
39
|
-
name: "
|
|
47
|
+
name: "Mock Label",
|
|
40
48
|
})
|
|
41
49
|
expect(menu).toHaveTextContent("Batch brew")
|
|
42
50
|
})
|
|
43
51
|
|
|
52
|
+
it("allows more aria-labelledby references to be sent in", () => {
|
|
53
|
+
const { getByRole } = render(
|
|
54
|
+
<>
|
|
55
|
+
<div id="extra-label">extra label stuff</div>
|
|
56
|
+
<SelectWrapper aria-labelledby="extra-label" />
|
|
57
|
+
</>
|
|
58
|
+
)
|
|
59
|
+
const menu = getByRole("combobox", {
|
|
60
|
+
name: "Mock Label extra label stuff",
|
|
61
|
+
})
|
|
62
|
+
expect(menu).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
44
65
|
describe("when uncontrolled", () => {
|
|
45
66
|
it("does not show the menu initially", () => {
|
|
46
67
|
const { queryByRole } = render(<SelectWrapper />)
|
|
@@ -76,7 +97,7 @@ describe("<Select />", () => {
|
|
|
76
97
|
/>
|
|
77
98
|
)
|
|
78
99
|
const trigger = getByRole("combobox", {
|
|
79
|
-
name: "
|
|
100
|
+
name: "Mock Label",
|
|
80
101
|
})
|
|
81
102
|
await user.click(trigger)
|
|
82
103
|
await waitFor(() => {
|
|
@@ -93,7 +114,7 @@ describe("<Select />", () => {
|
|
|
93
114
|
<SelectWrapper selectedKey="batch-brew" />
|
|
94
115
|
)
|
|
95
116
|
const trigger = getByRole("combobox", {
|
|
96
|
-
name: "
|
|
117
|
+
name: "Mock Label",
|
|
97
118
|
})
|
|
98
119
|
await user.click(trigger)
|
|
99
120
|
await waitFor(() => {
|
|
@@ -108,7 +129,7 @@ describe("<Select />", () => {
|
|
|
108
129
|
<SelectWrapper selectedKey="batch-brew" defaultOpen />
|
|
109
130
|
)
|
|
110
131
|
const trigger = getByRole("combobox", {
|
|
111
|
-
name: "
|
|
132
|
+
name: "Mock Label",
|
|
112
133
|
})
|
|
113
134
|
|
|
114
135
|
await user.click(trigger)
|
|
@@ -147,7 +168,7 @@ describe("<Select />", () => {
|
|
|
147
168
|
<SelectWrapper selectedKey="batch-brew" />
|
|
148
169
|
)
|
|
149
170
|
const trigger = getByRole("combobox", {
|
|
150
|
-
name: "
|
|
171
|
+
name: "Mock Label",
|
|
151
172
|
})
|
|
152
173
|
await user.tab()
|
|
153
174
|
await waitFor(() => {
|
|
@@ -160,7 +181,7 @@ describe("<Select />", () => {
|
|
|
160
181
|
<SelectWrapper selectedKey="batch-brew" />
|
|
161
182
|
)
|
|
162
183
|
const trigger = getByRole("combobox", {
|
|
163
|
-
name: "
|
|
184
|
+
name: "Mock Label",
|
|
164
185
|
})
|
|
165
186
|
await user.tab()
|
|
166
187
|
await waitFor(() => {
|
|
@@ -305,9 +326,7 @@ describe("<Select />", () => {
|
|
|
305
326
|
})
|
|
306
327
|
await user.keyboard("{Enter}")
|
|
307
328
|
|
|
308
|
-
await user.click(
|
|
309
|
-
getByRole("combobox", { name: "Short black Mock Label" })
|
|
310
|
-
)
|
|
329
|
+
await user.click(getByRole("combobox", { name: "Mock Label" }))
|
|
311
330
|
await waitFor(() => {
|
|
312
331
|
expect(
|
|
313
332
|
getByRole("option", { name: "Short black", selected: true })
|
|
@@ -320,7 +339,7 @@ describe("<Select />", () => {
|
|
|
320
339
|
const { getByRole } = render(
|
|
321
340
|
<SelectWrapper onSelectionChange={spy} defaultOpen />
|
|
322
341
|
)
|
|
323
|
-
const trigger = getByRole("combobox", { name: "
|
|
342
|
+
const trigger = getByRole("combobox", { name: "Mock Label" })
|
|
324
343
|
|
|
325
344
|
await user.tab()
|
|
326
345
|
await waitFor(() => {
|
|
@@ -332,7 +351,7 @@ describe("<Select />", () => {
|
|
|
332
351
|
await user.keyboard("{Enter}")
|
|
333
352
|
await waitFor(() => {
|
|
334
353
|
expect(spy).toHaveBeenCalledTimes(1)
|
|
335
|
-
expect(trigger).toHaveAccessibleName("
|
|
354
|
+
expect(trigger).toHaveAccessibleName("Mock Label")
|
|
336
355
|
})
|
|
337
356
|
})
|
|
338
357
|
})
|