@kaizen/components 1.35.1 → 1.36.0

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.
Files changed (153) hide show
  1. package/dist/cjs/Button/GenericButton/GenericButton.cjs +1 -1
  2. package/dist/cjs/Button/GenericButton/GenericButton.cjs.map +1 -1
  3. package/dist/cjs/KaizenProvider/KaizenProvider.cjs +6 -1
  4. package/dist/cjs/KaizenProvider/KaizenProvider.cjs.map +1 -1
  5. package/dist/cjs/Modal/ContextModal/ContextModal.cjs +9 -6
  6. package/dist/cjs/Modal/ContextModal/ContextModal.cjs.map +1 -1
  7. package/dist/cjs/MultiSelect/subcomponents/Popover/Popover.cjs +1 -1
  8. package/dist/cjs/MultiSelect/subcomponents/Popover/Popover.cjs.map +1 -1
  9. package/dist/cjs/Notification/ToastNotification/ToastNotification/ToastNotification.cjs +33 -0
  10. package/dist/cjs/Notification/ToastNotification/ToastNotification/ToastNotification.cjs.map +1 -0
  11. package/dist/cjs/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.cjs +36 -0
  12. package/dist/cjs/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.cjs.map +1 -0
  13. package/dist/cjs/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.module.scss.cjs +7 -0
  14. package/dist/cjs/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.module.scss.cjs.map +1 -0
  15. package/dist/cjs/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.cjs +42 -0
  16. package/dist/cjs/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.cjs.map +1 -0
  17. package/dist/cjs/Notification/ToastNotification/context/ToastNotificationContext.cjs +72 -0
  18. package/dist/cjs/Notification/ToastNotification/context/ToastNotificationContext.cjs.map +1 -0
  19. package/dist/cjs/Notification/ToastNotification/hooks/useToastNotification.cjs +9 -0
  20. package/dist/cjs/Notification/ToastNotification/hooks/useToastNotification.cjs.map +1 -0
  21. package/dist/cjs/RichTextEditor/RichTextEditor/RichTextEditor.cjs +6 -2
  22. package/dist/cjs/RichTextEditor/RichTextEditor/RichTextEditor.cjs.map +1 -1
  23. package/dist/cjs/__future__/Select/Select.cjs +24 -2
  24. package/dist/cjs/__future__/Select/Select.cjs.map +1 -1
  25. package/dist/cjs/dts/Modal/ContextModal/ContextModal.d.ts +2 -1
  26. package/dist/cjs/dts/MultiSelect/MultiSelect.d.ts +4 -2
  27. package/dist/cjs/dts/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.d.ts +3 -0
  28. package/dist/cjs/dts/MultiSelect/types.d.ts +6 -0
  29. package/dist/cjs/dts/Notification/ToastNotification/ToastNotification/ToastNotification.d.ts +16 -0
  30. package/dist/cjs/dts/Notification/ToastNotification/ToastNotification/index.d.ts +1 -0
  31. package/dist/cjs/dts/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.d.ts +4 -0
  32. package/dist/cjs/dts/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.d.ts +12 -0
  33. package/dist/cjs/dts/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/index.d.ts +1 -0
  34. package/dist/cjs/dts/Notification/ToastNotification/context/ToastNotificationContext.d.ts +21 -0
  35. package/dist/cjs/dts/Notification/ToastNotification/hooks/useToastNotification.d.ts +2 -0
  36. package/dist/cjs/dts/Notification/ToastNotification/index.d.ts +3 -2
  37. package/dist/cjs/dts/Notification/ToastNotification/types.d.ts +1 -9
  38. package/dist/cjs/dts/Notification/index.d.ts +1 -0
  39. package/dist/cjs/dts/RichTextEditor/RichTextEditor/RichTextEditor.d.ts +1 -1
  40. package/dist/cjs/dts/__future__/Select/Select.d.ts +5 -1
  41. package/dist/cjs/index.cjs +6 -0
  42. package/dist/cjs/index.cjs.map +1 -1
  43. package/dist/cjs/index.css +4 -3
  44. package/dist/esm/Button/GenericButton/GenericButton.mjs +1 -1
  45. package/dist/esm/Button/GenericButton/GenericButton.mjs.map +1 -1
  46. package/dist/esm/KaizenProvider/KaizenProvider.mjs +6 -1
  47. package/dist/esm/KaizenProvider/KaizenProvider.mjs.map +1 -1
  48. package/dist/esm/Modal/ContextModal/ContextModal.mjs +9 -6
  49. package/dist/esm/Modal/ContextModal/ContextModal.mjs.map +1 -1
  50. package/dist/esm/MultiSelect/subcomponents/Popover/Popover.mjs +1 -1
  51. package/dist/esm/MultiSelect/subcomponents/Popover/Popover.mjs.map +1 -1
  52. package/dist/esm/Notification/ToastNotification/ToastNotification/ToastNotification.mjs +31 -0
  53. package/dist/esm/Notification/ToastNotification/ToastNotification/ToastNotification.mjs.map +1 -0
  54. package/dist/esm/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.mjs +34 -0
  55. package/dist/esm/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.mjs.map +1 -0
  56. package/dist/esm/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.module.scss.mjs +5 -0
  57. package/dist/esm/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.module.scss.mjs.map +1 -0
  58. package/dist/esm/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.mjs +40 -0
  59. package/dist/esm/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.mjs.map +1 -0
  60. package/dist/esm/Notification/ToastNotification/context/ToastNotificationContext.mjs +69 -0
  61. package/dist/esm/Notification/ToastNotification/context/ToastNotificationContext.mjs.map +1 -0
  62. package/dist/esm/Notification/ToastNotification/hooks/useToastNotification.mjs +7 -0
  63. package/dist/esm/Notification/ToastNotification/hooks/useToastNotification.mjs.map +1 -0
  64. package/dist/esm/RichTextEditor/RichTextEditor/RichTextEditor.mjs +6 -2
  65. package/dist/esm/RichTextEditor/RichTextEditor/RichTextEditor.mjs.map +1 -1
  66. package/dist/esm/__future__/Select/Select.mjs +25 -3
  67. package/dist/esm/__future__/Select/Select.mjs.map +1 -1
  68. package/dist/esm/dts/Modal/ContextModal/ContextModal.d.ts +2 -1
  69. package/dist/esm/dts/MultiSelect/MultiSelect.d.ts +4 -2
  70. package/dist/esm/dts/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.d.ts +3 -0
  71. package/dist/esm/dts/MultiSelect/types.d.ts +6 -0
  72. package/dist/esm/dts/Notification/ToastNotification/ToastNotification/ToastNotification.d.ts +16 -0
  73. package/dist/esm/dts/Notification/ToastNotification/ToastNotification/index.d.ts +1 -0
  74. package/dist/esm/dts/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.d.ts +4 -0
  75. package/dist/esm/dts/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.d.ts +12 -0
  76. package/dist/esm/dts/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/index.d.ts +1 -0
  77. package/dist/esm/dts/Notification/ToastNotification/context/ToastNotificationContext.d.ts +21 -0
  78. package/dist/esm/dts/Notification/ToastNotification/hooks/useToastNotification.d.ts +2 -0
  79. package/dist/esm/dts/Notification/ToastNotification/index.d.ts +3 -2
  80. package/dist/esm/dts/Notification/ToastNotification/types.d.ts +1 -9
  81. package/dist/esm/dts/Notification/index.d.ts +1 -0
  82. package/dist/esm/dts/RichTextEditor/RichTextEditor/RichTextEditor.d.ts +1 -1
  83. package/dist/esm/dts/__future__/Select/Select.d.ts +5 -1
  84. package/dist/esm/index.css +5 -4
  85. package/dist/esm/index.mjs +3 -0
  86. package/dist/esm/index.mjs.map +1 -1
  87. package/dist/index.d.ts +50 -3
  88. package/dist/styles.css +1 -1
  89. package/package.json +2 -2
  90. package/src/Button/Button/_docs/Button.mdx +5 -0
  91. package/src/Button/Button/_docs/Button.stories.tsx +27 -1
  92. package/src/Button/GenericButton/GenericButton.spec.tsx +69 -0
  93. package/src/Button/GenericButton/GenericButton.tsx +1 -1
  94. package/src/DatePicker/DatePicker.spec.tsx +1 -1
  95. package/src/KaizenProvider/KaizenProvider.tsx +6 -1
  96. package/src/Modal/ContextModal/ContextModal.spec.tsx +3 -3
  97. package/src/Modal/ContextModal/ContextModal.tsx +9 -5
  98. package/src/MultiSelect/MultiSelect.spec.tsx +56 -1
  99. package/src/MultiSelect/MultiSelect.tsx +10 -3
  100. package/src/MultiSelect/_docs/MultiSelect.mdx +10 -0
  101. package/src/MultiSelect/_docs/MultiSelect.stickersheet.stories.tsx +81 -43
  102. package/src/MultiSelect/_docs/MultiSelect.stories.tsx +21 -0
  103. package/src/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.module.scss +9 -0
  104. package/src/MultiSelect/subcomponents/MultiSelectToggle/MultiSelectToggle.tsx +8 -1
  105. package/src/MultiSelect/subcomponents/MultiSelectToggle/_docs/MultiSelectToggle.stickersheet.stories.tsx +17 -0
  106. package/src/MultiSelect/subcomponents/Popover/Popover.tsx +1 -1
  107. package/src/MultiSelect/types.ts +7 -0
  108. package/src/Notification/ToastNotification/ToastNotification/ToastNotification.spec.tsx +33 -0
  109. package/src/Notification/ToastNotification/ToastNotification/ToastNotification.tsx +48 -0
  110. package/src/Notification/ToastNotification/ToastNotification/index.ts +1 -0
  111. package/src/Notification/ToastNotification/{subcomponents/ToastNotificationsList → ToastNotificationsList}/ToastNotificationsList.module.scss +1 -1
  112. package/src/Notification/ToastNotification/ToastNotificationsList/ToastNotificationsList.tsx +40 -0
  113. package/src/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/ToastNotificationsMap.tsx +49 -0
  114. package/src/Notification/ToastNotification/ToastNotificationsList/subcomponents/ToastNotificationsMap/index.ts +1 -0
  115. package/src/Notification/ToastNotification/_docs/ToastNotification.mdx +19 -14
  116. package/src/Notification/ToastNotification/_docs/ToastNotification.stickersheet.stories.tsx +33 -70
  117. package/src/Notification/ToastNotification/_docs/ToastNotification.stories.tsx +123 -93
  118. package/src/Notification/ToastNotification/context/ToastNotificationContext.tsx +96 -0
  119. package/src/Notification/ToastNotification/hooks/useToastNotification.ts +9 -0
  120. package/src/Notification/ToastNotification/index.ts +3 -2
  121. package/src/Notification/ToastNotification/types.ts +1 -18
  122. package/src/Notification/index.ts +1 -0
  123. package/src/RichTextEditor/RichTextEditor/RichTextEditor.tsx +6 -1
  124. package/src/RichTextEditor/utils/commands/addMark.spec.ts +0 -1
  125. package/src/Tooltip/Tooltip.spec.tsx +6 -1
  126. package/src/__future__/Select/Select.spec.tsx +109 -14
  127. package/src/__future__/Select/Select.tsx +32 -3
  128. package/src/__future__/Select/_docs/Select.mdx +8 -0
  129. package/src/__future__/Select/_docs/Select.stories.tsx +29 -0
  130. package/dist/cjs/dts/Notification/ToastNotification/ToastNotification.d.ts +0 -14
  131. package/dist/cjs/dts/Notification/ToastNotification/subcomponents/ToastNotificationManager/ToastNotificationManager.d.ts +0 -7
  132. package/dist/cjs/dts/Notification/ToastNotification/subcomponents/ToastNotificationManager/index.d.ts +0 -1
  133. package/dist/cjs/dts/Notification/ToastNotification/subcomponents/ToastNotificationsList/ToastNotificationsList.d.ts +0 -11
  134. package/dist/cjs/dts/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/ToastNotificationsListContainer.d.ts +0 -7
  135. package/dist/cjs/dts/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/index.d.ts +0 -1
  136. package/dist/esm/dts/Notification/ToastNotification/ToastNotification.d.ts +0 -14
  137. package/dist/esm/dts/Notification/ToastNotification/subcomponents/ToastNotificationManager/ToastNotificationManager.d.ts +0 -7
  138. package/dist/esm/dts/Notification/ToastNotification/subcomponents/ToastNotificationManager/index.d.ts +0 -1
  139. package/dist/esm/dts/Notification/ToastNotification/subcomponents/ToastNotificationsList/ToastNotificationsList.d.ts +0 -11
  140. package/dist/esm/dts/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/ToastNotificationsListContainer.d.ts +0 -7
  141. package/dist/esm/dts/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/index.d.ts +0 -1
  142. package/src/Notification/ToastNotification/ToastNotification.spec.tsx +0 -31
  143. package/src/Notification/ToastNotification/ToastNotification.tsx +0 -43
  144. package/src/Notification/ToastNotification/subcomponents/ToastNotificationManager/ToastNotificationManager.spec.tsx +0 -144
  145. package/src/Notification/ToastNotification/subcomponents/ToastNotificationManager/ToastNotificationManager.tsx +0 -135
  146. package/src/Notification/ToastNotification/subcomponents/ToastNotificationManager/index.ts +0 -1
  147. package/src/Notification/ToastNotification/subcomponents/ToastNotificationsList/ToastNotificationsList.tsx +0 -40
  148. package/src/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/ToastNotificationsListContainer.spec.tsx +0 -73
  149. package/src/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/ToastNotificationsListContainer.tsx +0 -31
  150. package/src/Notification/ToastNotification/subcomponents/ToastNotificationsListContainer/index.ts +0 -1
  151. /package/dist/cjs/dts/Notification/ToastNotification/{subcomponents/ToastNotificationsList → ToastNotificationsList}/index.d.ts +0 -0
  152. /package/dist/esm/dts/Notification/ToastNotification/{subcomponents/ToastNotificationsList → ToastNotificationsList}/index.d.ts +0 -0
  153. /package/src/Notification/ToastNotification/{subcomponents/ToastNotificationsList → ToastNotificationsList}/index.ts +0 -0
@@ -0,0 +1,96 @@
1
+ import React, { useContext, useState } from "react"
2
+ import { v4 as uuidv4 } from "uuid"
3
+ import { ToastNotificationObj } from "../types"
4
+
5
+ type ToastNotificationObjOptionalId = Omit<ToastNotificationObj, "id"> & {
6
+ id?: string
7
+ }
8
+
9
+ export type ToastNotificationContextValue = {
10
+ notifications: ToastNotificationObj[]
11
+ addToastNotification: (notification: ToastNotificationObjOptionalId) => void
12
+ updateToastNotification: (notification: ToastNotificationObj) => void
13
+ removeToastNotification: (notificationId: string) => void
14
+ clearToastNotifications: () => void
15
+ }
16
+
17
+ const ToastNotificationContext =
18
+ React.createContext<ToastNotificationContextValue | null>(null)
19
+
20
+ export const useToastNotificationContext =
21
+ (): ToastNotificationContextValue => {
22
+ const context = useContext(ToastNotificationContext)
23
+
24
+ if (!context) {
25
+ throw new Error(
26
+ "useToastNotificationContext must be used within the ToastNotificationContext.Provider"
27
+ )
28
+ }
29
+
30
+ return context
31
+ }
32
+
33
+ type ToastNotificationProviderProps = {
34
+ children: React.ReactNode
35
+ }
36
+
37
+ export const ToastNotificationProvider = ({
38
+ children,
39
+ }: ToastNotificationProviderProps): JSX.Element | null => {
40
+ const [notifications, setNotifications] = useState<ToastNotificationObj[]>([])
41
+
42
+ const addToastNotification: ToastNotificationContextValue["addToastNotification"] =
43
+ notification => {
44
+ const uuid = uuidv4()
45
+ const notificationWithId = { id: uuid, ...notification }
46
+
47
+ const notificationExists = notifications.find(
48
+ ({ id }) => id === notification.id
49
+ )
50
+
51
+ if (!notificationExists) {
52
+ setNotifications(existing => [...existing, notificationWithId])
53
+ }
54
+ }
55
+
56
+ const updateToastNotification = (
57
+ notification: ToastNotificationObj
58
+ ): void => {
59
+ const notificationIndex = notifications.findIndex(
60
+ ({ id }) => id === notification.id
61
+ )
62
+
63
+ const copy = notifications.slice()
64
+ copy.splice(notificationIndex, 1, notification) // Mutation to insert notification over itself
65
+ setNotifications(copy)
66
+ }
67
+
68
+ const removeToastNotification = (notificationID: string): void => {
69
+ const notificationIndex = notifications.findIndex(
70
+ ({ id }) => id === notificationID
71
+ )
72
+ const copy = notifications.slice()
73
+ copy.splice(notificationIndex, 1) // Mutation
74
+ setNotifications(copy)
75
+ }
76
+
77
+ const clearToastNotifications = (): void => {
78
+ setNotifications([])
79
+ }
80
+
81
+ const value = {
82
+ notifications,
83
+ addToastNotification,
84
+ updateToastNotification,
85
+ removeToastNotification,
86
+ clearToastNotifications,
87
+ } satisfies ToastNotificationContextValue
88
+
89
+ return (
90
+ <ToastNotificationContext.Provider value={value}>
91
+ {children}
92
+ </ToastNotificationContext.Provider>
93
+ )
94
+ }
95
+
96
+ ToastNotificationProvider.displayName = "ToastNotificationProvider"
@@ -0,0 +1,9 @@
1
+ import {
2
+ ToastNotificationContextValue,
3
+ useToastNotificationContext,
4
+ } from "../context/ToastNotificationContext"
5
+
6
+ export const useToastNotification = (): ToastNotificationContextValue => {
7
+ const context = useToastNotificationContext()
8
+ return context
9
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./ToastNotification"
2
- export * from "./subcomponents/ToastNotificationManager"
3
- export * from "./subcomponents/ToastNotificationsList"
2
+ export * from "./ToastNotificationsList"
3
+ export * from "./hooks/useToastNotification"
4
+ export * from "./types"
@@ -1,9 +1,7 @@
1
1
  import { DataAttributes } from "~types/DataAttributes"
2
2
  import { NotificationType } from "../types"
3
3
 
4
- type Modify<T, R> = Omit<T, keyof R> & R
5
-
6
- export type ToastNotification = {
4
+ export type ToastNotificationObj = {
7
5
  id: string
8
6
  type: NotificationType
9
7
  title: string
@@ -15,18 +13,3 @@ export type ToastNotification = {
15
13
  */
16
14
  persistent?: boolean
17
15
  } & DataAttributes
18
-
19
- export type ToastNotificationWithOptionals = Modify<
20
- ToastNotification,
21
- {
22
- id?: string
23
- }
24
- >
25
-
26
- export type AddToastNotification = (
27
- notification: ToastNotificationWithOptionals
28
- ) => void
29
-
30
- export type RemoveToastNotification = (notificationId: string) => void
31
-
32
- export type ClearToastNotifications = () => void
@@ -1,2 +1,3 @@
1
1
  export * from "./InlineNotification"
2
2
  export * from "./GlobalNotification"
3
+ export * from "./ToastNotification"
@@ -74,6 +74,7 @@ export const RichTextEditor = ({
74
74
  defaultValue,
75
75
  labelText,
76
76
  "aria-labelledby": labelledBy,
77
+ "aria-describedby": describedBy,
77
78
  classNameOverride,
78
79
  controls,
79
80
  rows = 3,
@@ -144,7 +145,11 @@ export const RichTextEditor = ({
144
145
  : ""
145
146
  const descriptionAria = description ? `${editorId}-rte-description` : ""
146
147
 
147
- const ariaDescribedBy = `${validationMessageAria} ${descriptionAria}`
148
+ const ariaDescribedBy = classnames(
149
+ validationMessageAria,
150
+ descriptionAria,
151
+ describedBy
152
+ )
148
153
 
149
154
  return (
150
155
  <>
@@ -1,4 +1,3 @@
1
- import { describe, expect, it, jest } from "@jest/globals"
2
1
  import { findByText, waitFor } from "@testing-library/dom"
3
2
  import { createRichTextEditor } from "../core"
4
3
  import { addMark } from "./addMark"
@@ -48,6 +48,8 @@ describe("<Tooltip />", () => {
48
48
  // Non-semantic elements without roles should not have aria-description on them.
49
49
  // They won't read to all screen readers as expected and may be reported in Storybook's accessibility tab (which uses Axe under the hood)
50
50
  it("doesn't add an accessible description when wrapping a non-semantic element", async () => {
51
+ const warn = jest.spyOn(console, "warn").mockImplementation()
52
+
51
53
  render(
52
54
  <Tooltip
53
55
  text="Tooltip popup description for div"
@@ -62,6 +64,9 @@ describe("<Tooltip />", () => {
62
64
  expect(screen.getByText("Non semantic element")).not.toHaveAttribute(
63
65
  "aria-describedby"
64
66
  )
67
+ expect(warn).toHaveBeenCalledWith(
68
+ "<Tooltip /> is not directly wrapping a semantic element, screen reader users will not be able to access the tooltip info. To ensure accessibility, Tooltip should be wrapping a semantic and focusable element directly."
69
+ )
65
70
  })
66
71
  })
67
72
  })
@@ -74,7 +79,7 @@ describe("<Tooltip />", () => {
74
79
  isInitiallyVisible
75
80
  position="below"
76
81
  >
77
- <div role="textbox" contentEditable="true" aria-multiline="true"></div>
82
+ <div role="textbox" contentEditable="true" aria-multiline="true" />
78
83
  </Tooltip>
79
84
  )
80
85
  await waitFor(() => {
@@ -1,5 +1,5 @@
1
1
  import React from "react"
2
- import { render, waitFor } from "@testing-library/react"
2
+ import { render, waitFor, screen, within } from "@testing-library/react"
3
3
  import userEvent from "@testing-library/user-event"
4
4
  import { Select, SelectProps } from "./Select"
5
5
  import { singleMockItems } from "./_docs/mockData"
@@ -17,7 +17,6 @@ const SelectWrapper = ({
17
17
  )
18
18
  return (
19
19
  <Select
20
- id="id--select"
21
20
  label="Mock Label"
22
21
  items={items}
23
22
  description="This is a description"
@@ -33,14 +32,35 @@ const SelectWrapper = ({
33
32
 
34
33
  describe("<Select />", () => {
35
34
  describe("Trigger", () => {
36
- it("makes sure the menu to be labelled by trigger", () => {
35
+ it("has the label as the accessible name", () => {
36
+ const { getByRole } = render(<SelectWrapper />)
37
+ const menu = getByRole("combobox", {
38
+ name: "Mock Label",
39
+ })
40
+ expect(menu).toBeInTheDocument()
41
+ })
42
+
43
+ it("has the value when an item is selected", () => {
37
44
  const { getByRole } = render(<SelectWrapper selectedKey="batch-brew" />)
38
45
  const menu = getByRole("combobox", {
39
- name: "Batch brew Mock Label",
46
+ name: "Mock Label",
40
47
  })
41
48
  expect(menu).toHaveTextContent("Batch brew")
42
49
  })
43
50
 
51
+ it("allows more aria-labelledby references to be sent in", () => {
52
+ const { getByRole } = render(
53
+ <>
54
+ <div id="extra-label">extra label stuff</div>
55
+ <SelectWrapper aria-labelledby="extra-label" />
56
+ </>
57
+ )
58
+ const menu = getByRole("combobox", {
59
+ name: "Mock Label extra label stuff",
60
+ })
61
+ expect(menu).toBeInTheDocument()
62
+ })
63
+
44
64
  describe("when uncontrolled", () => {
45
65
  it("does not show the menu initially", () => {
46
66
  const { queryByRole } = render(<SelectWrapper />)
@@ -76,7 +96,7 @@ describe("<Select />", () => {
76
96
  />
77
97
  )
78
98
  const trigger = getByRole("combobox", {
79
- name: "Batch brew Mock Label",
99
+ name: "Mock Label",
80
100
  })
81
101
  await user.click(trigger)
82
102
  await waitFor(() => {
@@ -93,7 +113,7 @@ describe("<Select />", () => {
93
113
  <SelectWrapper selectedKey="batch-brew" />
94
114
  )
95
115
  const trigger = getByRole("combobox", {
96
- name: "Batch brew Mock Label",
116
+ name: "Mock Label",
97
117
  })
98
118
  await user.click(trigger)
99
119
  await waitFor(() => {
@@ -108,7 +128,7 @@ describe("<Select />", () => {
108
128
  <SelectWrapper selectedKey="batch-brew" defaultOpen />
109
129
  )
110
130
  const trigger = getByRole("combobox", {
111
- name: "Batch brew Mock Label",
131
+ name: "Mock Label",
112
132
  })
113
133
 
114
134
  await user.click(trigger)
@@ -147,7 +167,7 @@ describe("<Select />", () => {
147
167
  <SelectWrapper selectedKey="batch-brew" />
148
168
  )
149
169
  const trigger = getByRole("combobox", {
150
- name: "Batch brew Mock Label",
170
+ name: "Mock Label",
151
171
  })
152
172
  await user.tab()
153
173
  await waitFor(() => {
@@ -160,7 +180,7 @@ describe("<Select />", () => {
160
180
  <SelectWrapper selectedKey="batch-brew" />
161
181
  )
162
182
  const trigger = getByRole("combobox", {
163
- name: "Batch brew Mock Label",
183
+ name: "Mock Label",
164
184
  })
165
185
  await user.tab()
166
186
  await waitFor(() => {
@@ -305,9 +325,7 @@ describe("<Select />", () => {
305
325
  })
306
326
  await user.keyboard("{Enter}")
307
327
 
308
- await user.click(
309
- getByRole("combobox", { name: "Short black Mock Label" })
310
- )
328
+ await user.click(getByRole("combobox", { name: "Mock Label" }))
311
329
  await waitFor(() => {
312
330
  expect(
313
331
  getByRole("option", { name: "Short black", selected: true })
@@ -320,7 +338,7 @@ describe("<Select />", () => {
320
338
  const { getByRole } = render(
321
339
  <SelectWrapper onSelectionChange={spy} defaultOpen />
322
340
  )
323
- const trigger = getByRole("combobox", { name: "Select Mock Label" })
341
+ const trigger = getByRole("combobox", { name: "Mock Label" })
324
342
 
325
343
  await user.tab()
326
344
  await waitFor(() => {
@@ -332,9 +350,86 @@ describe("<Select />", () => {
332
350
  await user.keyboard("{Enter}")
333
351
  await waitFor(() => {
334
352
  expect(spy).toHaveBeenCalledTimes(1)
335
- expect(trigger).toHaveAccessibleName("Short black Mock Label")
353
+ expect(trigger).toHaveAccessibleName("Mock Label")
336
354
  })
337
355
  })
338
356
  })
339
357
  })
358
+
359
+ describe("Popover portal", () => {
360
+ it("has accessible trigger controls", async () => {
361
+ render(<SelectWrapper isOpen />)
362
+
363
+ const trigger = screen.getByRole("combobox", {
364
+ name: "Mock Label",
365
+ })
366
+
367
+ await waitFor(() => {
368
+ expect(trigger).toHaveAttribute("aria-controls")
369
+ })
370
+ })
371
+
372
+ it("will portal to the document body by default", async () => {
373
+ render(<SelectWrapper selectedKey="batch-brew" isOpen />)
374
+
375
+ const popover = screen.getByRole("dialog")
376
+ // expected div that FocusOn adds to the popover
377
+ const popoverFocusWrapper = popover.parentNode
378
+
379
+ await waitFor(() => {
380
+ const expectedBodyTag = popoverFocusWrapper?.parentNode
381
+ expect(expectedBodyTag?.nodeName).toEqual("BODY")
382
+ })
383
+ })
384
+
385
+ it("will render as a descendant of the element matching the id", async () => {
386
+ const SelectWithPortal = (): JSX.Element => {
387
+ const portalContainerId = "id--portal-container"
388
+ return (
389
+ <>
390
+ <div
391
+ id={portalContainerId}
392
+ data-testid="id--portal-container-test"
393
+ ></div>
394
+ <SelectWrapper
395
+ selectedKey="batch-brew"
396
+ isOpen
397
+ portalContainerId={portalContainerId}
398
+ />
399
+ </>
400
+ )
401
+ }
402
+ render(<SelectWithPortal />)
403
+
404
+ await waitFor(() => {
405
+ const newPortalRegion = screen.getByTestId("id--portal-container-test")
406
+ const popover = within(newPortalRegion).getByRole("dialog")
407
+
408
+ expect(popover).toBeInTheDocument()
409
+ })
410
+ })
411
+
412
+ it("will portal to the document body if the id does not match", async () => {
413
+ const SelectWithPortal = (): JSX.Element => {
414
+ const expectedContainerId = "id--portal-container"
415
+ return (
416
+ <>
417
+ <div id="id--wrong-id"></div>
418
+ <SelectWrapper
419
+ selectedKey="batch-brew"
420
+ isOpen
421
+ portalContainerId={expectedContainerId}
422
+ />
423
+ </>
424
+ )
425
+ }
426
+ render(<SelectWithPortal />)
427
+
428
+ await waitFor(() => {
429
+ const popover = within(document.body).getByRole("dialog")
430
+
431
+ expect(popover).toBeInTheDocument()
432
+ })
433
+ })
434
+ })
340
435
  })
@@ -1,4 +1,4 @@
1
- import React, { useId } from "react"
1
+ import React, { useEffect, useId, useState } from "react"
2
2
  import { UseFloatingReturn } from "@floating-ui/react-dom"
3
3
  import { useButton } from "@react-aria/button"
4
4
  import { HiddenSelect, useSelect } from "@react-aria/select"
@@ -67,6 +67,10 @@ export type SelectProps<Option extends SelectOption = SelectOption> = {
67
67
  * @deprecated: Either define `disabled` in your `Option` (in `items`), or use `disabledKeys`
68
68
  */
69
69
  disabledValues?: Key[]
70
+ /**
71
+ * Creates a portal for the Popover to the matching element id
72
+ */
73
+ portalContainerId?: string
70
74
  } & OverrideClassName<Omit<AriaSelectProps<Option>, OmittedAriaSelectProps>>
71
75
 
72
76
  /**
@@ -89,13 +93,14 @@ export const Select = <Option extends SelectOption = SelectOption>({
89
93
  description,
90
94
  placeholder,
91
95
  isDisabled,
96
+ portalContainerId,
92
97
  ...restProps
93
98
  }: SelectProps<Option>): JSX.Element => {
94
99
  const { refs } = useFloating<HTMLButtonElement>()
95
100
  const triggerRef = refs.reference
96
-
97
101
  const id = propsId ?? useId()
98
102
  const descriptionId = `${id}--description`
103
+ const popoverId = `${id}--popover`
99
104
 
100
105
  const disabledKeys = getDisabledKeysFromItems(items)
101
106
 
@@ -116,13 +121,26 @@ export const Select = <Option extends SelectOption = SelectOption>({
116
121
 
117
122
  const {
118
123
  labelProps,
119
- triggerProps,
124
+ triggerProps: reactAriaTriggerProps,
120
125
  valueProps,
121
126
  menuProps,
122
127
  errorMessageProps,
123
128
  descriptionProps,
124
129
  } = useSelect(ariaSelectProps, state, triggerRef)
125
130
 
131
+ // Hack incoming:
132
+ // react-aria/useSelect wants to prefix the combobox's accessible name with the value of the select.
133
+ // We use role=combobox, meaning screen readers will read the value.
134
+ // So we're modifying the `aria-labelledby` property to remove the value element id.
135
+ // Issue: https://github.com/adobe/react-spectrum/issues/4091
136
+ const reactAriaLabelledBy = reactAriaTriggerProps["aria-labelledby"]
137
+ const triggerProps = {
138
+ ...reactAriaTriggerProps,
139
+ "aria-labelledby": reactAriaLabelledBy?.substring(
140
+ reactAriaLabelledBy.indexOf(" ") + 1
141
+ ),
142
+ }
143
+
126
144
  const { buttonProps } = useButton(triggerProps, triggerRef)
127
145
  const selectToggleProps = {
128
146
  ...buttonProps,
@@ -138,6 +156,15 @@ export const Select = <Option extends SelectOption = SelectOption>({
138
156
  ref: refs.setReference,
139
157
  }
140
158
 
159
+ const [portalContainer, setPortalContainer] = useState<HTMLElement>()
160
+
161
+ useEffect(() => {
162
+ if (portalContainerId) {
163
+ const portalElement = document.getElementById(portalContainerId)
164
+ portalElement && setPortalContainer(portalElement)
165
+ }
166
+ }, [])
167
+
141
168
  return (
142
169
  <div
143
170
  className={classnames(
@@ -160,6 +187,8 @@ export const Select = <Option extends SelectOption = SelectOption>({
160
187
  )}
161
188
  {state.isOpen && (
162
189
  <Popover
190
+ id={popoverId}
191
+ portalContainer={portalContainer}
163
192
  refs={refs}
164
193
  focusOnProps={{
165
194
  onEscapeKey: state.close,
@@ -98,3 +98,11 @@ Add validation messages using `status` and `validationMessage`.
98
98
 
99
99
  Set `isFullWidth` to `true` to have the Select span the full width of its container.
100
100
  <Canvas of={SelectStories.FullWidth} />
101
+
102
+ ### Portals
103
+
104
+ By default, the Select's popover will attach itself to the `body` of the document using React's `createPortal`.
105
+
106
+ You can change the default behaviour by providing a `portalContainerId` to attach this to different element in the DOM. This can help to resolve issues that may arise with `z-index` or having a Select in a modal.
107
+
108
+ <Canvas of={SelectStories.PortalContainer} />
@@ -161,3 +161,32 @@ export const Validation: Story = {
161
161
  export const FullWidth: Story = {
162
162
  args: { isFullWidth: true },
163
163
  }
164
+
165
+ export const PortalContainer: Story = {
166
+ render: args => {
167
+ const portalContainerId = "id--portal-container"
168
+ return (
169
+ <>
170
+ <div
171
+ id={portalContainerId}
172
+ className="flex gap-24 bg-gray-200 p-12 overflow-hidden h-[200px] relative"
173
+ >
174
+ <Select
175
+ {...args}
176
+ label="Default"
177
+ selectedKey="batch-brew"
178
+ id="id--select-default"
179
+ />
180
+ <Select
181
+ {...args}
182
+ label="Inner portal"
183
+ selectedKey="batch-brew"
184
+ id="id--select-inner"
185
+ portalContainerId={portalContainerId}
186
+ />
187
+ </div>
188
+ </>
189
+ )
190
+ },
191
+ parameters: { docs: { source: { type: "code" } } },
192
+ }
@@ -1,14 +0,0 @@
1
- import React from "react";
2
- import { ToastNotificationWithOptionals } from "./types";
3
- export type ToastNotificationProps = Omit<ToastNotificationWithOptionals, "message" | "persistent"> & {
4
- /**
5
- * Removes the dismiss trigger. functions the same as `persistent` in `addToastNotification`. If this is true you will need to manage the removal of notifications manually.
6
- * @default false
7
- */
8
- hideCloseIcon?: boolean;
9
- children: React.ReactNode;
10
- };
11
- export declare const ToastNotification: {
12
- ({ id: propsId, hideCloseIcon, type, title, onHide, children, ...restProps }: ToastNotificationProps): null;
13
- displayName: string;
14
- };
@@ -1,7 +0,0 @@
1
- import { AddToastNotification, ClearToastNotifications, RemoveToastNotification } from "../../types";
2
- /**
3
- * Export the curried API methods
4
- */
5
- export declare const addToastNotification: AddToastNotification;
6
- export declare const clearToastNotifications: ClearToastNotifications;
7
- export declare const removeToastNotification: RemoveToastNotification;
@@ -1 +0,0 @@
1
- export * from "./ToastNotificationManager";
@@ -1,11 +0,0 @@
1
- import { HTMLAttributes } from "react";
2
- import { OverrideClassName } from "../../../../types/OverrideClassName";
3
- import { RemoveToastNotification, ToastNotification } from "../../types";
4
- export type ToastNotificationsListProps = {
5
- notifications: ToastNotification[];
6
- onHide: RemoveToastNotification;
7
- } & OverrideClassName<HTMLAttributes<HTMLDivElement>>;
8
- export declare const ToastNotificationsList: {
9
- ({ notifications, onHide: defaultOnHide, }: ToastNotificationsListProps): JSX.Element;
10
- displayName: string;
11
- };
@@ -1,7 +0,0 @@
1
- import React from "react";
2
- import { RemoveToastNotification, ToastNotification } from "../../types";
3
- export type ToastNotificationsListContainerProps = {
4
- removeToastNotification: RemoveToastNotification;
5
- registerSetNotificationsCallback: (callback: React.Dispatch<React.SetStateAction<ToastNotification[]>>) => void;
6
- };
7
- export declare const ToastNotificationsListContainer: ({ removeToastNotification, registerSetNotificationsCallback, }: ToastNotificationsListContainerProps) => JSX.Element;
@@ -1 +0,0 @@
1
- export * from "./ToastNotificationsListContainer";
@@ -1,14 +0,0 @@
1
- import React from "react";
2
- import { ToastNotificationWithOptionals } from "./types";
3
- export type ToastNotificationProps = Omit<ToastNotificationWithOptionals, "message" | "persistent"> & {
4
- /**
5
- * Removes the dismiss trigger. functions the same as `persistent` in `addToastNotification`. If this is true you will need to manage the removal of notifications manually.
6
- * @default false
7
- */
8
- hideCloseIcon?: boolean;
9
- children: React.ReactNode;
10
- };
11
- export declare const ToastNotification: {
12
- ({ id: propsId, hideCloseIcon, type, title, onHide, children, ...restProps }: ToastNotificationProps): null;
13
- displayName: string;
14
- };
@@ -1,7 +0,0 @@
1
- import { AddToastNotification, ClearToastNotifications, RemoveToastNotification } from "../../types";
2
- /**
3
- * Export the curried API methods
4
- */
5
- export declare const addToastNotification: AddToastNotification;
6
- export declare const clearToastNotifications: ClearToastNotifications;
7
- export declare const removeToastNotification: RemoveToastNotification;
@@ -1 +0,0 @@
1
- export * from "./ToastNotificationManager";
@@ -1,11 +0,0 @@
1
- import { HTMLAttributes } from "react";
2
- import { OverrideClassName } from "../../../../types/OverrideClassName";
3
- import { RemoveToastNotification, ToastNotification } from "../../types";
4
- export type ToastNotificationsListProps = {
5
- notifications: ToastNotification[];
6
- onHide: RemoveToastNotification;
7
- } & OverrideClassName<HTMLAttributes<HTMLDivElement>>;
8
- export declare const ToastNotificationsList: {
9
- ({ notifications, onHide: defaultOnHide, }: ToastNotificationsListProps): JSX.Element;
10
- displayName: string;
11
- };
@@ -1,7 +0,0 @@
1
- import React from "react";
2
- import { RemoveToastNotification, ToastNotification } from "../../types";
3
- export type ToastNotificationsListContainerProps = {
4
- removeToastNotification: RemoveToastNotification;
5
- registerSetNotificationsCallback: (callback: React.Dispatch<React.SetStateAction<ToastNotification[]>>) => void;
6
- };
7
- export declare const ToastNotificationsListContainer: ({ removeToastNotification, registerSetNotificationsCallback, }: ToastNotificationsListContainerProps) => JSX.Element;
@@ -1 +0,0 @@
1
- export * from "./ToastNotificationsListContainer";