@kaizen/components 0.0.0-canary-v2-20250901045936 → 0.0.0-canary-alpha-release-20250918043833

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 (114) hide show
  1. package/alpha/README.md +28 -0
  2. package/alpha/package.json +5 -0
  3. package/dist/cjs/alpha.cjs +1 -0
  4. package/dist/cjs/src/Modal/GenericModal/GenericModal.cjs +33 -65
  5. package/dist/cjs/src/Modal/GenericModal/GenericModal.module.scss.cjs +1 -3
  6. package/dist/cjs/src/Notification/InlineNotification/InlineNotification.cjs +1 -1
  7. package/dist/cjs/src/__alpha__/SingleSelect/SingleSelect.cjs +35 -74
  8. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.cjs +105 -0
  9. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css.cjs +11 -0
  10. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.cjs +112 -0
  11. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css.cjs +16 -0
  12. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/List/List.cjs +35 -10
  13. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.cjs +61 -8
  14. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css.cjs +10 -1
  15. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.cjs +38 -9
  16. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css.cjs +4 -1
  17. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.cjs +60 -30
  18. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.cjs +2 -1
  19. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.cjs +2 -1
  20. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.cjs +4 -2
  21. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Select/Select.cjs +87 -0
  22. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css.cjs +11 -0
  23. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.cjs +52 -0
  24. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css.cjs +13 -0
  25. package/dist/esm/alpha.mjs +1 -1
  26. package/dist/esm/src/Modal/GenericModal/GenericModal.mjs +34 -65
  27. package/dist/esm/src/Modal/GenericModal/GenericModal.module.scss.mjs +1 -3
  28. package/dist/esm/src/Notification/InlineNotification/InlineNotification.mjs +1 -1
  29. package/dist/esm/src/__alpha__/SingleSelect/SingleSelect.mjs +39 -73
  30. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.mjs +96 -0
  31. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css.mjs +9 -0
  32. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.mjs +103 -0
  33. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css.mjs +14 -0
  34. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/List/List.mjs +37 -14
  35. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.mjs +63 -13
  36. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css.mjs +10 -1
  37. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.mjs +41 -15
  38. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css.mjs +4 -1
  39. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.mjs +69 -43
  40. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css.mjs +2 -1
  41. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.mjs +2 -1
  42. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.mjs +4 -2
  43. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Select/Select.mjs +78 -0
  44. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css.mjs +9 -0
  45. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.mjs +43 -0
  46. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css.mjs +11 -0
  47. package/dist/styles.css +385 -21
  48. package/dist/types/__alpha__/SingleSelect/SingleSelect.d.ts +14 -19
  49. package/dist/types/__alpha__/SingleSelect/_docs/mockData.d.ts +3 -0
  50. package/dist/types/__alpha__/SingleSelect/context/SingleSelectContext.d.ts +15 -7
  51. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.d.ts +2 -0
  52. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBox/index.d.ts +1 -0
  53. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.d.ts +2 -0
  54. package/dist/types/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/index.d.ts +1 -0
  55. package/dist/types/__alpha__/SingleSelect/subcomponents/List/List.d.ts +2 -7
  56. package/dist/types/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.d.ts +2 -7
  57. package/dist/types/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.d.ts +2 -9
  58. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/Popover.d.ts +3 -6
  59. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/index.d.ts +1 -0
  60. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.d.ts +1 -0
  61. package/dist/types/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.d.ts +1 -0
  62. package/dist/types/__alpha__/SingleSelect/subcomponents/Select/Select.d.ts +2 -0
  63. package/dist/types/__alpha__/SingleSelect/subcomponents/Select/index.d.ts +1 -0
  64. package/dist/types/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.d.ts +2 -0
  65. package/dist/types/__alpha__/SingleSelect/subcomponents/SelectTrigger/index.d.ts +1 -0
  66. package/dist/types/__alpha__/SingleSelect/subcomponents/index.d.ts +4 -1
  67. package/dist/types/__alpha__/SingleSelect/types.d.ts +68 -11
  68. package/locales/en.json +9 -1
  69. package/package.json +10 -3
  70. package/src/Modal/GenericModal/GenericModal.spec.tsx +1 -1
  71. package/src/Modal/GenericModal/GenericModal.tsx +38 -70
  72. package/src/Notification/InlineNotification/InlineNotification.tsx +1 -1
  73. package/src/RichTextEditor/RichTextEditor/RichTextEditor.spec.stories.tsx +10 -3
  74. package/src/__alpha__/SingleSelect/SingleSelect.tsx +35 -88
  75. package/src/__alpha__/SingleSelect/_docs/SingleSelect.mdx +96 -6
  76. package/src/__alpha__/SingleSelect/_docs/SingleSelect.spec.stories.tsx +22 -24
  77. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stickersheet.stories.tsx +389 -33
  78. package/src/__alpha__/SingleSelect/_docs/SingleSelect.stories.tsx +41 -22
  79. package/src/__alpha__/SingleSelect/_docs/mockData.ts +20 -14
  80. package/src/__alpha__/SingleSelect/context/SingleSelectContext.tsx +18 -7
  81. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.module.css +35 -0
  82. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/ComboBox.tsx +106 -0
  83. package/src/__alpha__/SingleSelect/subcomponents/ComboBox/index.ts +1 -0
  84. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.module.css +130 -0
  85. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/ComboBoxTrigger.tsx +121 -0
  86. package/src/__alpha__/SingleSelect/subcomponents/ComboBoxTrigger/index.ts +1 -0
  87. package/src/__alpha__/SingleSelect/subcomponents/List/List.module.css +5 -0
  88. package/src/__alpha__/SingleSelect/subcomponents/List/List.tsx +36 -13
  89. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.module.css +84 -3
  90. package/src/__alpha__/SingleSelect/subcomponents/ListItem/ListItem.tsx +67 -11
  91. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.module.css +20 -5
  92. package/src/__alpha__/SingleSelect/subcomponents/ListSection/ListSection.tsx +46 -19
  93. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.module.css +7 -5
  94. package/src/__alpha__/SingleSelect/subcomponents/Popover/Popover.tsx +90 -37
  95. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/index.ts +1 -0
  96. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePopoverPositioning.ts +2 -2
  97. package/src/__alpha__/SingleSelect/subcomponents/Popover/utils/usePositioningStyles.ts +9 -8
  98. package/src/__alpha__/SingleSelect/subcomponents/Select/Select.module.css +35 -0
  99. package/src/__alpha__/SingleSelect/subcomponents/Select/Select.tsx +84 -0
  100. package/src/__alpha__/SingleSelect/subcomponents/Select/index.ts +1 -0
  101. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.module.css +77 -0
  102. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/SelectTrigger.tsx +52 -0
  103. package/src/__alpha__/SingleSelect/subcomponents/SelectTrigger/index.ts +1 -0
  104. package/src/__alpha__/SingleSelect/subcomponents/index.ts +4 -1
  105. package/src/__alpha__/SingleSelect/types.ts +94 -14
  106. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.cjs +0 -57
  107. package/dist/cjs/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css.cjs +0 -6
  108. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.mjs +0 -49
  109. package/dist/esm/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css.mjs +0 -4
  110. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.d.ts +0 -2
  111. package/dist/types/__alpha__/SingleSelect/subcomponents/Trigger/index.d.ts +0 -1
  112. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.module.css +0 -19
  113. package/src/__alpha__/SingleSelect/subcomponents/Trigger/Trigger.tsx +0 -35
  114. package/src/__alpha__/SingleSelect/subcomponents/Trigger/index.ts +0 -1
@@ -1,5 +1,8 @@
1
1
  export * from './List';
2
2
  export * from './ListSection';
3
3
  export * from './ListItem';
4
- export * from './Trigger';
4
+ export * from './SelectTrigger';
5
+ export * from './ComboBoxTrigger';
5
6
  export * from './Popover';
7
+ export * from './Select';
8
+ export * from './ComboBox';
@@ -1,25 +1,66 @@
1
- import { type RefObject } from 'react';
2
- import { type Key } from '@react-types/shared';
1
+ import type { DOMAttributes, RefObject } from 'react';
2
+ import type React from 'react';
3
+ import { type ComboBoxState, type ComboBoxStateOptions } from '@react-stately/combobox';
4
+ import type { ListState } from '@react-stately/list';
5
+ import { type SelectState, type SelectStateOptions } from '@react-stately/select';
6
+ import { type Key, type Node } from '@react-types/shared';
7
+ import { type FocusableElement } from '@react-types/shared/src/dom';
8
+ import type { AriaButtonProps, AriaListBoxOptions, AriaPopoverProps } from 'react-aria';
3
9
  export type SelectItem = {
4
10
  label: string;
5
11
  value: string;
12
+ key: Key;
6
13
  };
7
14
  export type SelectSection = {
8
15
  label: string;
9
16
  options: SelectItem[];
10
17
  };
11
- export type SingleSelectProps = {
12
- children?: React.ReactNode;
13
- items: (SelectItem | SelectSection)[];
14
- onSelectionChange?: (key: Key | null) => void;
18
+ export type SelectLabel = {
19
+ labelHidden: true;
20
+ label: string;
21
+ } | {
22
+ labelHidden?: false;
23
+ label: React.ReactNode;
24
+ };
25
+ export type SelectBaseProps = {
26
+ variant?: 'primary' | 'secondary';
27
+ size?: 'small' | 'medium' | 'large';
28
+ labelPosition?: 'top' | 'side';
29
+ isReadOnly?: boolean;
30
+ } & SelectLabel;
31
+ export type SelectProps<T extends SelectItem> = Omit<SelectStateOptions<T>, 'label' | 'defaultFilter' | 'menuTrigger' | 'allowsCustomValue'> & SelectBaseProps;
32
+ export type ComboBoxProps<T extends SelectItem> = Omit<ComboBoxStateOptions<T>, 'label' | 'defaultFilter' | 'menuTrigger' | 'allowsCustomValue'> & SelectBaseProps;
33
+ export type SingleSelectProps<T extends SelectItem> = (ComboBoxProps<T> & {
34
+ isComboBox?: true;
35
+ }) | (SelectProps<T> & {
36
+ isComboBox?: false;
37
+ });
38
+ export type SelectTriggerProps = {
39
+ triggerProps: AriaButtonProps<'button'>;
40
+ valueProps: DOMAttributes<FocusableElement>;
41
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
42
+ };
43
+ export type ComboBoxTriggerProps = {
44
+ inputProps: React.InputHTMLAttributes<HTMLInputElement>;
45
+ inputRef: React.MutableRefObject<HTMLInputElement | null>;
46
+ buttonProps: AriaButtonProps<'button'>;
47
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
48
+ triggerWrapperRef: React.MutableRefObject<HTMLDivElement | null>;
49
+ clearButtonRef: React.MutableRefObject<HTMLButtonElement | null>;
50
+ };
51
+ export type ChevronButtonProps = AriaButtonProps<'button'> & {
52
+ buttonRef: React.MutableRefObject<HTMLButtonElement | null>;
15
53
  };
16
- export type TriggerProps = {
17
- buttonRef: React.RefObject<HTMLButtonElement>;
54
+ export type ClearButtonProps = {
55
+ clearButtonRef: React.RefObject<HTMLButtonElement>;
56
+ inputRef: React.RefObject<HTMLInputElement>;
18
57
  };
19
- export type PopoverProps = {
20
- buttonRef: React.RefObject<HTMLElement>;
58
+ export type PopoverProps<T extends SelectItem> = AriaPopoverProps & {
59
+ state: ComboBoxState<T> | SelectState<T>;
60
+ triggerRef: React.RefObject<HTMLElement>;
21
61
  popoverRef: React.RefObject<HTMLDivElement>;
22
- racPopoverRef: React.Ref<any>;
62
+ clearButtonRef?: React.RefObject<HTMLButtonElement>;
63
+ children: React.ReactNode;
23
64
  };
24
65
  type PositionDataProp = number | string | undefined;
25
66
  export type PositionData = {
@@ -42,4 +83,20 @@ export type UsePopoverPositioningProps = {
42
83
  offset?: number;
43
84
  preferredPlacement?: 'top' | 'bottom';
44
85
  };
86
+ export type ListProps<T extends SelectItem> = {
87
+ state: ComboBoxState<T> | SelectState<T>;
88
+ listBoxOptions: AriaListBoxOptions<T>;
89
+ listBoxRef: React.RefObject<HTMLUListElement>;
90
+ };
91
+ export type ListItemProps<T extends SelectItem> = {
92
+ item: Node<T>;
93
+ state: ListState<T>;
94
+ selectedIcon?: 'check' | 'radio';
95
+ selectedPosition?: 'start' | 'end';
96
+ className?: string;
97
+ };
98
+ export type ListSectionProps<T extends SelectItem> = {
99
+ section: Node<T>;
100
+ state: ComboBoxState<T> | SelectState<T>;
101
+ };
45
102
  export {};
package/locales/en.json CHANGED
@@ -185,8 +185,16 @@
185
185
  "description": "Prompts user to interact with button to hide information",
186
186
  "message": "Hide information:"
187
187
  },
188
+ "singleSelect.chevronButton": {
189
+ "description": "Aria label text for the SingleSelect button to open and close suggestions list",
190
+ "message": "Show suggestions for {field}"
191
+ },
192
+ "singleSelect.clearButtonAlt": {
193
+ "description": "Alt text for the clear selection button",
194
+ "message": "Clear {field} selection"
195
+ },
188
196
  "splitButton.dropdownButton.label": {
189
197
  "description": "Label for a dropdown menu holding additional actions",
190
198
  "message": "Additional actions"
191
199
  }
192
- }
200
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "0.0.0-canary-v2-20250901045936",
3
+ "version": "0.0.0-canary-alpha-release-20250918043833",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "bin",
17
17
  "codemods",
18
+ "alpha",
18
19
  "future",
19
20
  "next",
20
21
  "libs",
@@ -36,6 +37,11 @@
36
37
  "import": "./dist/esm/index.mjs",
37
38
  "require": "./dist/cjs/index.cjs"
38
39
  },
40
+ "./alpha": {
41
+ "types": "./dist/types/__alpha__/index.d.ts",
42
+ "import": "./dist/esm/alpha.mjs",
43
+ "require": "./dist/cjs/alpha.cjs"
44
+ },
39
45
  "./next": {
40
46
  "types": "./dist/types/__next__/index.d.ts",
41
47
  "import": "./dist/esm/next.mjs",
@@ -86,6 +92,7 @@
86
92
  "@react-aria/select": "^3.15.7",
87
93
  "@react-aria/utils": "^3.29.1",
88
94
  "@react-stately/collections": "^3.12.5",
95
+ "@react-stately/combobox": "^3.11.1",
89
96
  "@react-stately/datepicker": "^3.14.2",
90
97
  "@react-stately/list": "^3.12.3",
91
98
  "@react-stately/menu": "^3.9.5",
@@ -152,10 +159,10 @@
152
159
  "sass": "1.79.6",
153
160
  "serialize-query-params": "^2.0.2",
154
161
  "svgo": "^3.3.2",
155
- "tslib": "^2.8.1",
156
162
  "ts-patch": "^3.3.0",
163
+ "tslib": "^2.8.1",
157
164
  "tsx": "^4.20.3",
158
- "@kaizen/design-tokens": "0.0.0-canary-v2-20250901045936"
165
+ "@kaizen/design-tokens": "11.0.0"
159
166
  },
160
167
  "devDependenciesComments": {
161
168
  "sass": "Prevent deprecation warnings introduced in 1.80 as we plan to move away from sass",
@@ -71,7 +71,7 @@ describe('<GenericModal />', () => {
71
71
 
72
72
  it('closes the modal when a click is outside of the modal content', async () => {
73
73
  const handleDismiss = vi.fn()
74
- render(<GenericModalWrapper onOutsideModalClick={handleDismiss} />)
74
+ render(<GenericModalWrapper onOutsideModalClick={handleDismiss()} />)
75
75
 
76
76
  await user.click(screen.getByTestId('GenericModalTestId-scrollLayer'))
77
77
  await waitFor(() => {
@@ -1,9 +1,10 @@
1
- import React, { useCallback, useEffect, useId, useState } from 'react'
1
+ import React, { useId, useRef } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { Transition } from '@headlessui/react'
4
4
  import classnames from 'classnames'
5
- import FocusLock from 'react-focus-lock'
5
+ import { FocusOn } from 'react-focus-on'
6
6
  import { useIsClientReady } from '../../utils/useIsClientReady'
7
+
7
8
  import { warn } from '../util/console'
8
9
  import { ModalContext } from './context/ModalContext'
9
10
  import styles from './GenericModal.module.scss'
@@ -38,39 +39,22 @@ export const GenericModal = ({
38
39
 
39
40
  const labelledByID = useId()
40
41
  const describedByID = useId()
41
-
42
42
  const isClientReady = useIsClientReady()
43
43
 
44
- const [scrollLayer, setScrollLayer] = useState<HTMLDivElement | null>(null)
45
- const [modalLayer, setModalLayer] = useState<HTMLDivElement | null>(null)
44
+ const scrollLayerRef = useRef<HTMLDivElement | null>(null)
45
+ const modalLayerRef = useRef<HTMLDivElement | null>(null)
46
46
 
47
47
  const scrollModalToTop = (): void => {
48
48
  // If we have a really long modal, the autofocus could land on an element down below
49
49
  // causing the modal to scroll down and skipping over the content near the modal's top.
50
50
  // Ensure that when the modal opens, we are at the top of its content.
51
51
  requestAnimationFrame(() => {
52
- if (!scrollLayer) return
53
- scrollLayer.scrollTop = 0
54
- })
55
- }
56
-
57
- const outsideModalClickHandler = (event: React.MouseEvent): void => {
58
- if (event.target === scrollLayer || event.target === modalLayer) {
59
- onOutsideModalClick?.(event)
60
- }
61
- }
62
-
63
- const focusOnAccessibleLabel = (): void => {
64
- if (!isClientReady) return
65
-
66
- // Check if focus already exists within the modal
67
- if (modalLayer?.contains(document.activeElement)) {
68
- return
69
- }
52
+ const scrollElement = scrollLayerRef.current
70
53
 
71
- const labelElement: HTMLElement | null = document.getElementById(labelledByID)
72
-
73
- labelElement?.focus()
54
+ // This little verbose of a check but this ensures that the element is attached to the DOM as it animates in. This additional check aims to avoid race conditions
55
+ if (!scrollElement?.isConnected) return
56
+ scrollElement.scrollTop = 0
57
+ })
74
58
  }
75
59
 
76
60
  const a11yWarn = (): void => {
@@ -86,60 +70,46 @@ export const GenericModal = ({
86
70
  }
87
71
  }
88
72
 
89
- const preventBodyScroll = (): void => {
73
+ const focusOnAccessibleLabel = (): void => {
90
74
  if (!isClientReady) return
75
+ const modalElement = modalLayerRef.current
76
+ if (!modalElement?.isConnected) return
91
77
 
92
- const hasScrollbar = window.innerWidth > document.documentElement.clientWidth
93
- const scrollStyles = [styles.unscrollable]
78
+ // Check if focus already exists within the modal
79
+ if (modalElement.contains(document.activeElement)) {
80
+ return
81
+ }
82
+
83
+ const labelElement: HTMLElement | null = document.getElementById(labelledByID)
94
84
 
95
- if (hasScrollbar) {
96
- scrollStyles.push(styles.pseudoScrollbar)
85
+ if (labelElement?.isConnected) {
86
+ labelElement.focus()
97
87
  }
88
+ }
98
89
 
99
- document.documentElement.classList.add(...scrollStyles)
90
+ const onEscapeKeyHandler = (e: Event): void => {
91
+ if (e instanceof KeyboardEvent) {
92
+ onEscapeKeyup?.(e)
93
+ }
100
94
  }
101
95
 
102
96
  const onAfterEnterHandler = (): void => {
103
97
  scrollModalToTop()
104
- if (modalLayer) {
98
+ const modalElement = modalLayerRef.current
99
+ if (modalElement) {
105
100
  onAfterEnter?.()
106
101
  focusOnAccessibleLabel()
107
102
  a11yWarn()
108
103
  }
109
104
  }
110
105
 
111
- const escapeKeyHandler = useCallback(
112
- (event: KeyboardEvent): void => {
113
- if (event.key === 'Escape') {
114
- onEscapeKeyup?.(event)
115
- }
116
- },
117
- [onEscapeKeyup],
118
- )
119
-
120
- const onBeforeEnterHandler = (): void => {
121
- preventBodyScroll()
122
-
123
- if (onEscapeKeyup && isClientReady) {
124
- document.addEventListener('keyup', escapeKeyHandler)
106
+ const outsideModalClickHandler = (e: React.MouseEvent): void => {
107
+ if (e.target === scrollLayerRef.current || e.target === modalLayerRef.current) {
108
+ onOutsideModalClick?.(e)
125
109
  }
126
110
  }
127
111
 
128
- const cleanUpAfterClose = useCallback(() => {
129
- if (!isClientReady) return
130
-
131
- document.documentElement.classList.remove(styles.unscrollable, styles.pseudoScrollbar)
132
-
133
- if (onEscapeKeyup) {
134
- document.removeEventListener('keyup', escapeKeyHandler)
135
- }
136
- }, [escapeKeyHandler, onEscapeKeyup, isClientReady])
137
-
138
- /* Ensure sure add-on styles (e.g. unscrollable) and key event is cleaned up when the modal is unmounted*/
139
- useEffect(() => () => cleanUpAfterClose(), [cleanUpAfterClose])
140
-
141
112
  const onAfterLeaveHandler = (): void => {
142
- cleanUpAfterClose()
143
113
  propsOnAfterLeave?.()
144
114
  }
145
115
 
@@ -152,7 +122,6 @@ export const GenericModal = ({
152
122
  <Transition
153
123
  appear={true}
154
124
  show={isOpen}
155
- beforeEnter={onBeforeEnterHandler}
156
125
  afterEnter={onAfterEnterHandler}
157
126
  afterLeave={onAfterLeaveHandler}
158
127
  data-generic-modal-transition-wrapper
@@ -161,9 +130,10 @@ export const GenericModal = ({
161
130
  as="div"
162
131
  className={classnames(styles.transitionLayer, className)}
163
132
  >
164
- <FocusLock
165
- disabled={focusLockDisabled}
133
+ <FocusOn
134
+ focusLock={focusLockDisabled}
166
135
  returnFocus={true}
136
+ onEscapeKey={onEscapeKeyHandler}
167
137
  // Disabling false positive
168
138
  // eslint-disable-next-line jsx-a11y/no-autofocus
169
139
  autoFocus={false}
@@ -174,11 +144,9 @@ export const GenericModal = ({
174
144
  {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
175
145
  <div
176
146
  className={styles.scrollLayer}
177
- ref={(scrollLayerRef): void => {
178
- setScrollLayer(scrollLayerRef)
179
- }}
180
- onClick={outsideModalClickHandler}
147
+ ref={scrollLayerRef}
181
148
  data-testid={`${id}-scrollLayer`}
149
+ onClick={outsideModalClickHandler}
182
150
  >
183
151
  <ModalContext.Provider
184
152
  value={{
@@ -191,14 +159,14 @@ export const GenericModal = ({
191
159
  className={styles.modalLayer}
192
160
  aria-labelledby={labelledByID}
193
161
  aria-describedby={describedByID}
194
- ref={(modalLayerRef): void => setModalLayer(modalLayerRef)}
162
+ ref={modalLayerRef}
195
163
  data-testid={id}
196
164
  >
197
165
  {children}
198
166
  </div>
199
167
  </ModalContext.Provider>
200
168
  </div>
201
- </FocusLock>
169
+ </FocusOn>
202
170
  </Transition>,
203
171
  document.body,
204
172
  )
@@ -35,7 +35,7 @@ export const InlineNotification = forwardRef<HTMLDivElement, InlineNotificationP
35
35
  ): JSX.Element => (
36
36
  <GenericNotification
37
37
  style="inline"
38
- persistent={persistent ?? hideCloseIcon}
38
+ persistent={persistent || hideCloseIcon}
39
39
  classNameOverride={classnames(classNameOverride, [isSubtle && styles.subtle])}
40
40
  ref={ref}
41
41
  {...otherProps}
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react'
2
2
  import { type StoryObj } from '@storybook/react'
3
- import { expect, userEvent, within } from '@storybook/test'
3
+ import { expect, userEvent, waitFor, within } from '@storybook/test'
4
4
  import { type EditorContentArray } from '../types'
5
5
  import { RichTextEditor, type RichTextEditorProps } from './RichTextEditor'
6
6
 
@@ -149,8 +149,9 @@ export const CreateALink: Story = {
149
149
  name: 'Create a link',
150
150
  play: async (context) => {
151
151
  const { canvasElement, step } = context
152
- const { getByRole, getByText } = within(canvasElement)
152
+ const { getByRole, getByText, queryByRole, findByRole } = within(canvasElement)
153
153
  const editor = getByRole('textbox')
154
+
154
155
  await step('Focus on editor', async () => {
155
156
  await userEvent.click(editor)
156
157
  expect(editor).toHaveFocus()
@@ -185,8 +186,14 @@ export const CreateALink: Story = {
185
186
  await userEvent.keyboard('{Tab}{Enter}')
186
187
  })
187
188
 
189
+ await step('The Link Modal closes', async () => {
190
+ await waitFor(() => {
191
+ expect(queryByRole('dialog')).not.toBeInTheDocument()
192
+ })
193
+ })
194
+
188
195
  await step('Link exists in the RTE', async () => {
189
- const link = getByRole('link', { name: 'Link' })
196
+ const link = await findByRole('link', { name: 'Link' })
190
197
  expect(link).toBeInTheDocument()
191
198
  })
192
199
  },
@@ -1,92 +1,39 @@
1
- import React, { cloneElement, isValidElement, useId, useMemo, type PropsWithChildren } from 'react'
2
- import { useSelectState } from '@react-stately/select'
3
- import { type Key, type Selection } from '@react-types/shared'
4
- import { Select as RACSelect, type ListBoxProps } from 'react-aria-components'
5
- import { SingleSelectContext } from './context'
6
- import { List, ListItem, ListSection, Popover, Trigger } from './subcomponents'
7
- import { type SelectItem, type SelectSection, type SingleSelectProps } from './types'
8
-
9
- export const SingleSelect = ({
10
- items,
11
- onSelectionChange,
12
- children,
13
- ...restProps
14
- }: PropsWithChildren<SingleSelectProps>): JSX.Element => {
15
- const buttonRef = React.useRef<HTMLButtonElement>(null)
16
- const popoverRef = React.useRef<HTMLDivElement>(null)
17
- const racPopoverRef = React.useRef<HTMLElement>(null)
18
- const uniqueId = useId()
19
- const anchorName = `--trigger-${uniqueId}`
20
-
21
- const state = useSelectState({
22
- items,
23
- })
24
-
25
- const handleOnSelectionChange = React.useCallback(
26
- (keys: Selection): void => {
27
- let key: Key | null = null
28
-
29
- if (keys instanceof Set && keys.size > 0) {
30
- key = Array.from(keys)[0]
31
- }
32
-
33
- state.setSelectedKey(key)
34
- if (onSelectionChange) {
35
- onSelectionChange(key)
36
- }
37
- },
38
- [state, onSelectionChange],
39
- )
40
-
41
- // Cloning children here to allow users to pass in a custom ListItem or ListSection
42
- // and still have the SingleSelect handle selection state
43
- const injectedChildren = useMemo(() => {
44
- if (!isValidElement(children)) return null
45
-
46
- const selectedKeys: Iterable<Key> = state.selectedKey
47
- ? new Set<Key>([state.selectedKey])
48
- : new Set()
1
+ import React from 'react'
2
+ import { Item as StatelyItem, Section } from '@react-stately/collections'
3
+ import { ComboBox, Select } from './subcomponents'
4
+ import {
5
+ type ComboBoxProps,
6
+ type SelectItem,
7
+ type SelectProps,
8
+ type SingleSelectProps,
9
+ } from './types'
10
+
11
+ export const SingleSelect = <T extends SelectItem>(props: SingleSelectProps<T>): JSX.Element => {
12
+ const { isComboBox, children, ...rest } = props
13
+
14
+ if (isComboBox) {
15
+ return <ComboBox {...(rest as ComboBoxProps<T>)}>{children}</ComboBox>
16
+ }
17
+
18
+ return <Select {...(rest as SelectProps<T>)}>{children}</Select>
19
+ }
49
20
 
50
- return cloneElement(children as React.ReactElement<ListBoxProps<SelectItem | SelectSection>>, {
51
- selectionMode: 'single',
52
- selectedKeys,
53
- onSelectionChange: handleOnSelectionChange,
54
- autoFocus: 'first',
55
- })
56
- }, [children, handleOnSelectionChange, state.selectedKey])
21
+ type CustomItemProps = {
22
+ selectedIcon?: 'check' | 'radio'
23
+ selectedPosition?: 'start' | 'end'
24
+ key: string
25
+ children?: React.ReactNode
26
+ [key: string]: any
27
+ }
57
28
 
58
- return (
59
- <SingleSelectContext.Provider
60
- value={{
61
- isOpen: state.isOpen,
62
- setOpen: state.setOpen,
63
- selectedKey: state.selectedKey,
64
- items: items,
65
- anchorName,
66
- }}
67
- >
68
- <RACSelect
69
- // TODO: allow user to pass in label
70
- aria-label={'single-select'}
71
- onSelectionChange={(key) =>
72
- handleOnSelectionChange(key != null ? new Set([key]) : new Set())
73
- }
74
- placeholder=""
75
- {...restProps}
76
- >
77
- <Trigger buttonRef={buttonRef} />
29
+ export const Item = React.forwardRef<HTMLElement, CustomItemProps>((props, ref) => {
30
+ // @ts-expect-error: StatelyItem doesn't know about our internal item props
31
+ return <StatelyItem {...props} ref={ref} />
32
+ })
78
33
 
79
- {state.isOpen && (
80
- <Popover buttonRef={buttonRef} popoverRef={popoverRef} racPopoverRef={racPopoverRef}>
81
- {injectedChildren}
82
- </Popover>
83
- )}
84
- </RACSelect>
85
- </SingleSelectContext.Provider>
86
- )
87
- }
34
+ // @ts-expect-error: doesn't know that the Item can have this static property
35
+ Item.getCollectionNode = StatelyItem.getCollectionNode
36
+ Item.displayName = 'SingleSelectItem'
88
37
 
89
- SingleSelect.displayName = 'SingleSelect'
90
- SingleSelect.List = List
91
- SingleSelect.ListItem = ListItem
92
- SingleSelect.ListSection = ListSection
38
+ SingleSelect.Item = Item
39
+ SingleSelect.Section = Section