@carbon/react 1.91.0 → 1.92.0-rc.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 (161) hide show
  1. package/.playwright/INTERNAL_AVT_REPORT_DO_NOT_USE.json +951 -976
  2. package/es/components/Accordion/AccordionItem.d.ts +12 -1
  3. package/es/components/Accordion/AccordionItem.js +9 -2
  4. package/es/components/Breadcrumb/BreadcrumbItem.js +1 -1
  5. package/es/components/Checkbox/Checkbox.js +2 -2
  6. package/es/components/ComboBox/ComboBox.js +39 -23
  7. package/es/components/ComboButton/index.js +1 -1
  8. package/es/components/ComposedModal/ComposedModal.js +66 -17
  9. package/es/components/ComposedModal/ComposedModalPresence.d.ts +34 -0
  10. package/es/components/ComposedModal/ComposedModalPresence.js +42 -0
  11. package/es/components/ComposedModal/index.d.ts +1 -0
  12. package/es/components/ComposedModal/useComposedModalState.d.ts +7 -0
  13. package/es/components/ComposedModal/useComposedModalState.js +24 -0
  14. package/es/components/DataTable/TableBatchActions.js +2 -2
  15. package/es/components/DatePickerInput/DatePickerInput.js +2 -2
  16. package/es/components/Dialog/Dialog.js +2 -2
  17. package/es/components/Dropdown/Dropdown.js +5 -5
  18. package/es/components/ExpandableSearch/ExpandableSearch.d.ts +1 -1
  19. package/es/components/ExpandableSearch/ExpandableSearch.js +1 -1
  20. package/es/components/FeatureFlags/index.d.ts +2 -1
  21. package/es/components/FeatureFlags/index.js +3 -1
  22. package/es/components/FileUploader/FileUploader.js +2 -2
  23. package/es/components/FileUploader/FileUploaderItem.js +2 -2
  24. package/es/components/FluidTextInput/FluidPasswordInput.js +24 -5
  25. package/es/components/FluidTextInput/index.js +1 -1
  26. package/es/components/FormLabel/FormLabel.js +1 -1
  27. package/es/components/ListBox/ListBox.d.ts +1 -1
  28. package/es/components/ListBox/ListBox.js +1 -2
  29. package/es/components/ListItem/ListItem.js +1 -1
  30. package/es/components/Menu/MenuItem.js +2 -2
  31. package/es/components/MenuButton/index.d.ts +1 -1
  32. package/es/components/MenuButton/index.js +1 -1
  33. package/es/components/Modal/Modal.js +60 -10
  34. package/es/components/Modal/ModalPresence.d.ts +32 -0
  35. package/es/components/Modal/ModalPresence.js +37 -0
  36. package/es/components/Modal/index.d.ts +2 -1
  37. package/es/components/Modal/index.js +1 -0
  38. package/es/components/MultiSelect/FilterableMultiSelect.js +3 -3
  39. package/es/components/MultiSelect/MultiSelect.js +6 -5
  40. package/es/components/Notification/Notification.js +2 -2
  41. package/es/components/NumberInput/NumberInput.d.ts +21 -11
  42. package/es/components/NumberInput/NumberInput.js +40 -26
  43. package/es/components/OverflowMenu/OverflowMenu.js +2 -3
  44. package/es/components/OverflowMenu/next/index.js +1 -1
  45. package/es/components/OverflowMenuItem/OverflowMenuItem.js +1 -1
  46. package/es/components/PageHeader/PageHeader.js +2 -2
  47. package/es/components/ProgressIndicator/ProgressIndicator.js +1 -1
  48. package/es/components/RadioButton/RadioButton.js +3 -3
  49. package/es/components/RadioButtonGroup/RadioButtonGroup.js +2 -2
  50. package/es/components/RadioTile/RadioTile.js +2 -2
  51. package/es/components/Select/Select.js +2 -2
  52. package/es/components/Slider/Slider.js +2 -2
  53. package/es/components/StructuredList/StructuredList.js +2 -2
  54. package/es/components/Tabs/Tabs.js +2 -2
  55. package/es/components/Tag/DismissibleTag.js +3 -3
  56. package/es/components/Tag/OperationalTag.js +3 -3
  57. package/es/components/Tag/SelectableTag.js +3 -3
  58. package/es/components/Tag/Tag.js +2 -2
  59. package/es/components/Text/Text.d.ts +1 -1
  60. package/es/components/Text/Text.js +0 -1
  61. package/es/components/Text/TextDirection.d.ts +1 -1
  62. package/es/components/Text/TextDirection.js +0 -1
  63. package/es/components/Text/createTextComponent.d.ts +2 -8
  64. package/es/components/Text/createTextComponent.js +2 -2
  65. package/es/components/Text/index.d.ts +0 -8
  66. package/es/components/TextArea/TextArea.js +2 -2
  67. package/es/components/TextInput/TextInput.js +2 -2
  68. package/es/components/Tile/Tile.js +2 -2
  69. package/es/components/Toggle/Toggle.js +2 -2
  70. package/es/components/UIShell/Switcher.js +0 -26
  71. package/es/index.d.ts +1 -1
  72. package/es/index.js +6 -4
  73. package/es/internal/useNormalizedInputProps.js +2 -2
  74. package/es/internal/usePresence.d.ts +17 -0
  75. package/es/internal/usePresence.js +66 -0
  76. package/es/internal/usePresenceContext.d.ts +25 -0
  77. package/es/internal/usePresenceContext.js +46 -0
  78. package/es/tools/mergeRefs.d.ts +5 -5
  79. package/es/tools/mergeRefs.js +16 -12
  80. package/lib/components/Accordion/AccordionItem.d.ts +12 -1
  81. package/lib/components/Accordion/AccordionItem.js +9 -2
  82. package/lib/components/Breadcrumb/BreadcrumbItem.js +1 -1
  83. package/lib/components/Checkbox/Checkbox.js +2 -2
  84. package/lib/components/ComboBox/ComboBox.js +39 -23
  85. package/lib/components/ComboButton/index.js +1 -1
  86. package/lib/components/ComposedModal/ComposedModal.js +65 -16
  87. package/lib/components/ComposedModal/ComposedModalPresence.d.ts +34 -0
  88. package/lib/components/ComposedModal/ComposedModalPresence.js +46 -0
  89. package/lib/components/ComposedModal/index.d.ts +1 -0
  90. package/lib/components/ComposedModal/useComposedModalState.d.ts +7 -0
  91. package/lib/components/ComposedModal/useComposedModalState.js +26 -0
  92. package/lib/components/DataTable/TableBatchActions.js +2 -2
  93. package/lib/components/DatePickerInput/DatePickerInput.js +2 -2
  94. package/lib/components/Dialog/Dialog.js +2 -2
  95. package/lib/components/Dropdown/Dropdown.js +3 -3
  96. package/lib/components/ExpandableSearch/ExpandableSearch.d.ts +1 -1
  97. package/lib/components/ExpandableSearch/ExpandableSearch.js +1 -1
  98. package/lib/components/FeatureFlags/index.d.ts +2 -1
  99. package/lib/components/FeatureFlags/index.js +3 -1
  100. package/lib/components/FileUploader/FileUploader.js +2 -2
  101. package/lib/components/FileUploader/FileUploaderItem.js +2 -2
  102. package/lib/components/FluidTextInput/FluidPasswordInput.js +26 -5
  103. package/lib/components/FluidTextInput/index.js +2 -1
  104. package/lib/components/FormLabel/FormLabel.js +1 -1
  105. package/lib/components/ListBox/ListBox.d.ts +1 -1
  106. package/lib/components/ListBox/ListBox.js +1 -2
  107. package/lib/components/ListItem/ListItem.js +1 -1
  108. package/lib/components/Menu/MenuItem.js +2 -2
  109. package/lib/components/MenuButton/index.d.ts +1 -1
  110. package/lib/components/MenuButton/index.js +1 -1
  111. package/lib/components/Modal/Modal.js +59 -9
  112. package/lib/components/Modal/ModalPresence.d.ts +32 -0
  113. package/lib/components/Modal/ModalPresence.js +41 -0
  114. package/lib/components/Modal/index.d.ts +2 -1
  115. package/lib/components/Modal/index.js +1 -0
  116. package/lib/components/MultiSelect/FilterableMultiSelect.js +3 -3
  117. package/lib/components/MultiSelect/MultiSelect.js +4 -3
  118. package/lib/components/Notification/Notification.js +2 -2
  119. package/lib/components/NumberInput/NumberInput.d.ts +21 -11
  120. package/lib/components/NumberInput/NumberInput.js +40 -26
  121. package/lib/components/OverflowMenu/OverflowMenu.js +2 -3
  122. package/lib/components/OverflowMenu/next/index.js +1 -1
  123. package/lib/components/OverflowMenuItem/OverflowMenuItem.js +1 -1
  124. package/lib/components/PageHeader/PageHeader.js +2 -2
  125. package/lib/components/ProgressIndicator/ProgressIndicator.js +1 -1
  126. package/lib/components/RadioButton/RadioButton.js +3 -3
  127. package/lib/components/RadioButtonGroup/RadioButtonGroup.js +3 -3
  128. package/lib/components/RadioTile/RadioTile.js +2 -2
  129. package/lib/components/Select/Select.js +2 -2
  130. package/lib/components/Slider/Slider.js +2 -2
  131. package/lib/components/StructuredList/StructuredList.js +2 -2
  132. package/lib/components/Tabs/Tabs.js +2 -2
  133. package/lib/components/Tag/DismissibleTag.js +3 -3
  134. package/lib/components/Tag/OperationalTag.js +3 -3
  135. package/lib/components/Tag/SelectableTag.js +3 -3
  136. package/lib/components/Tag/Tag.js +2 -2
  137. package/lib/components/Text/Text.d.ts +1 -1
  138. package/lib/components/Text/Text.js +0 -1
  139. package/lib/components/Text/TextDirection.d.ts +1 -1
  140. package/lib/components/Text/TextDirection.js +0 -1
  141. package/lib/components/Text/createTextComponent.d.ts +2 -8
  142. package/lib/components/Text/createTextComponent.js +2 -2
  143. package/lib/components/Text/index.d.ts +0 -8
  144. package/lib/components/TextArea/TextArea.js +2 -2
  145. package/lib/components/TextInput/TextInput.js +2 -2
  146. package/lib/components/Tile/Tile.js +2 -2
  147. package/lib/components/Toggle/Toggle.js +2 -2
  148. package/lib/components/UIShell/Switcher.js +0 -26
  149. package/lib/index.d.ts +1 -1
  150. package/lib/index.js +13 -8
  151. package/lib/internal/useNormalizedInputProps.js +2 -2
  152. package/lib/internal/usePresence.d.ts +17 -0
  153. package/lib/internal/usePresence.js +68 -0
  154. package/lib/internal/usePresenceContext.d.ts +25 -0
  155. package/lib/internal/usePresenceContext.js +48 -0
  156. package/lib/tools/mergeRefs.d.ts +5 -5
  157. package/lib/tools/mergeRefs.js +16 -14
  158. package/package.json +7 -7
  159. package/telemetry.yml +4 -0
  160. package/es/components/Text/index.js +0 -16
  161. package/lib/components/Text/index.js +0 -20
@@ -12,6 +12,11 @@ export interface AccordionItemProps {
12
12
  * this value will be managed by the parent Accordion.
13
13
  */
14
14
  disabled?: boolean;
15
+ /**
16
+ * Specify a custom label for the accordion button.
17
+ * This is important for accessibility when the accordion has no visible title.
18
+ */
19
+ 'aria-label'?: AriaAttributes['aria-label'];
15
20
  /**
16
21
  * The handler of the massaged `click` event.
17
22
  */
@@ -52,6 +57,7 @@ export interface AccordionItemProps {
52
57
  export interface AccordionToggleProps {
53
58
  'aria-controls'?: AriaAttributes['aria-controls'];
54
59
  'aria-expanded'?: AriaAttributes['aria-expanded'];
60
+ 'aria-label'?: AriaAttributes['aria-label'];
55
61
  className?: string;
56
62
  disabled?: boolean;
57
63
  onClick?: MouseEventHandler<HTMLButtonElement>;
@@ -59,7 +65,7 @@ export interface AccordionToggleProps {
59
65
  type?: 'button';
60
66
  }
61
67
  declare function AccordionItem({ children, className: customClassName, open, onHeadingClick, renderExpando, // remove renderExpando in next major release
62
- renderToggle, title, disabled: controlledDisabled, handleAnimationEnd, ...rest }: PropsWithChildren<AccordionItemProps>): import("react/jsx-runtime").JSX.Element;
68
+ renderToggle, title, disabled: controlledDisabled, handleAnimationEnd, 'aria-label': ariaLabel, ...rest }: PropsWithChildren<AccordionItemProps>): import("react/jsx-runtime").JSX.Element;
63
69
  declare namespace AccordionItem {
64
70
  var propTypes: {
65
71
  /**
@@ -74,6 +80,11 @@ declare namespace AccordionItem {
74
80
  * Specify whether an individual AccordionItem should be disabled
75
81
  */
76
82
  disabled: PropTypes.Requireable<boolean>;
83
+ /**
84
+ * Specify a custom label for the accordion button.
85
+ * This is important for accessibility when the accordion has no visible title.
86
+ */
87
+ 'aria-label': PropTypes.Requireable<string>;
77
88
  /**
78
89
  * The handler of the massaged `click` event.
79
90
  */
@@ -10,14 +10,14 @@ import { ChevronRight } from '@carbon/icons-react';
10
10
  import cx from 'classnames';
11
11
  import PropTypes from 'prop-types';
12
12
  import React, { useState, useContext } from 'react';
13
- import '../Text/index.js';
13
+ import { Text } from '../Text/Text.js';
14
+ import '../Text/TextDirection.js';
14
15
  import { Escape } from '../../internal/keyboard/keys.js';
15
16
  import { match } from '../../internal/keyboard/match.js';
16
17
  import { useId } from '../../internal/useId.js';
17
18
  import { deprecate } from '../../prop-types/deprecate.js';
18
19
  import { usePrefix } from '../../internal/usePrefix.js';
19
20
  import { AccordionContext } from './AccordionProvider.js';
20
- import { Text } from '../Text/Text.js';
21
21
 
22
22
  const defaultRenderToggle = props => /*#__PURE__*/React.createElement("button", _extends({
23
23
  type: "button"
@@ -33,6 +33,7 @@ function AccordionItem({
33
33
  title = 'title',
34
34
  disabled: controlledDisabled,
35
35
  handleAnimationEnd,
36
+ 'aria-label': ariaLabel,
36
37
  ...rest
37
38
  }) {
38
39
  const [isOpen, setIsOpen] = useState(open);
@@ -96,6 +97,7 @@ function AccordionItem({
96
97
  disabled: disabled,
97
98
  "aria-controls": id,
98
99
  "aria-expanded": isOpen,
100
+ "aria-label": ariaLabel,
99
101
  className: `${prefix}--accordion__heading`,
100
102
  onClick: onClick,
101
103
  onKeyDown: onKeyDown,
@@ -127,6 +129,11 @@ AccordionItem.propTypes = {
127
129
  * Specify whether an individual AccordionItem should be disabled
128
130
  */
129
131
  disabled: PropTypes.bool,
132
+ /**
133
+ * Specify a custom label for the accordion button.
134
+ * This is important for accessibility when the accordion has no visible title.
135
+ */
136
+ 'aria-label': PropTypes.string,
130
137
  /**
131
138
  * The handler of the massaged `click` event.
132
139
  */
@@ -12,8 +12,8 @@ import cx from 'classnames';
12
12
  import Link from '../Link/Link.js';
13
13
  import { OverflowMenuHorizontal } from '@carbon/icons-react';
14
14
  import { usePrefix } from '../../internal/usePrefix.js';
15
- import '../Text/index.js';
16
15
  import { Text } from '../Text/Text.js';
16
+ import '../Text/TextDirection.js';
17
17
 
18
18
  const frFn = forwardRef;
19
19
  const BreadcrumbItem = frFn((props, ref) => {
@@ -9,7 +9,8 @@ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js
9
9
  import PropTypes from 'prop-types';
10
10
  import React, { cloneElement } from 'react';
11
11
  import cx from 'classnames';
12
- import '../Text/index.js';
12
+ import { Text } from '../Text/Text.js';
13
+ import '../Text/TextDirection.js';
13
14
  import { deprecate } from '../../prop-types/deprecate.js';
14
15
  import { usePrefix } from '../../internal/usePrefix.js';
15
16
  import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
@@ -17,7 +18,6 @@ import { useId } from '../../internal/useId.js';
17
18
  import { noopFn } from '../../internal/noopFn.js';
18
19
  import { AILabel } from '../AILabel/index.js';
19
20
  import { isComponentElement } from '../../internal/utils.js';
20
- import { Text } from '../Text/Text.js';
21
21
 
22
22
  const Checkbox = /*#__PURE__*/React.forwardRef(({
23
23
  className,
@@ -10,7 +10,8 @@ import cx from 'classnames';
10
10
  import { useCombobox } from 'downshift';
11
11
  import PropTypes from 'prop-types';
12
12
  import React, { forwardRef, useRef, useEffect, useState, useContext, useCallback, useMemo, cloneElement } from 'react';
13
- import '../Text/index.js';
13
+ import { Text } from '../Text/Text.js';
14
+ import '../Text/TextDirection.js';
14
15
  import { WarningFilled, WarningAltFilled, Checkmark } from '@carbon/icons-react';
15
16
  import isEqual from 'react-fast-compare';
16
17
  import ListBox from '../ListBox/index.js';
@@ -19,7 +20,7 @@ import ListBoxTrigger from '../ListBox/next/ListBoxTrigger.js';
19
20
  import { Space, Enter, Escape, Home, End } from '../../internal/keyboard/keys.js';
20
21
  import { match } from '../../internal/keyboard/match.js';
21
22
  import { useId } from '../../internal/useId.js';
22
- import mergeRefs from '../../tools/mergeRefs.js';
23
+ import { mergeRefs } from '../../tools/mergeRefs.js';
23
24
  import { deprecate } from '../../prop-types/deprecate.js';
24
25
  import { usePrefix } from '../../internal/usePrefix.js';
25
26
  import '../FluidForm/FluidForm.js';
@@ -30,7 +31,6 @@ import { AILabel } from '../AILabel/index.js';
30
31
  import { defaultItemToString } from '../../internal/defaultItemToString.js';
31
32
  import { isComponentElement } from '../../internal/utils.js';
32
33
  import { ListBoxSizePropType } from '../ListBox/ListBoxPropTypes.js';
33
- import { Text } from '../Text/Text.js';
34
34
 
35
35
  const {
36
36
  InputBlur,
@@ -281,9 +281,13 @@ const ComboBox = /*#__PURE__*/forwardRef((props, ref) => {
281
281
  switch (type) {
282
282
  case InputBlur:
283
283
  {
284
- if (allowCustomValue && highlightedIndex == '-1') {
285
- const customValue = inputValue;
286
- changes.selectedItem = customValue;
284
+ // If custom values are allowed, treat whatever the user typed as
285
+ // the value.
286
+ if (allowCustomValue && highlightedIndex === -1) {
287
+ const {
288
+ inputValue
289
+ } = state;
290
+ changes.selectedItem = inputValue;
287
291
  if (onChange) {
288
292
  onChange({
289
293
  selectedItem: inputValue,
@@ -292,17 +296,28 @@ const ComboBox = /*#__PURE__*/forwardRef((props, ref) => {
292
296
  }
293
297
  return changes;
294
298
  }
295
- if (state.inputValue && highlightedIndex == '-1' && changes.selectedItem) {
299
+
300
+ // If a new item was selected, keep its label in the input.
301
+ if (state.inputValue && highlightedIndex === -1 && changes.selectedItem) {
296
302
  return {
297
303
  ...changes,
298
304
  inputValue: itemToString(changes.selectedItem)
299
305
  };
300
306
  }
301
- if (state.inputValue && highlightedIndex == '-1' && !allowCustomValue && !changes.selectedItem) {
302
- return {
303
- ...changes,
304
- inputValue: ''
305
- };
307
+
308
+ // If custom values are not allowed, normalize any non-matching
309
+ // text. If the input isn’t an exact item label, restore the
310
+ // selected label if there is one, or clear it.
311
+ if (!allowCustomValue) {
312
+ const currentInput = state.inputValue ?? '';
313
+ const hasExactMatch = !!currentInput && items.some(item => itemToString(item) === currentInput);
314
+ if (!hasExactMatch) {
315
+ const restoredInput = state.selectedItem !== null ? itemToString(state.selectedItem) : '';
316
+ return {
317
+ ...changes,
318
+ inputValue: restoredInput
319
+ };
320
+ }
306
321
  }
307
322
  return changes;
308
323
  }
@@ -355,16 +370,17 @@ const ComboBox = /*#__PURE__*/forwardRef((props, ref) => {
355
370
  };
356
371
  case FunctionToggleMenu:
357
372
  case ToggleButtonClick:
358
- if (!changes.isOpen && state.inputValue && highlightedIndex === -1 && !allowCustomValue) {
359
- return {
360
- ...changes,
361
- inputValue: '' // Clear the input
362
- };
363
- }
364
- if (changes.isOpen && !changes.selectedItem) {
365
- return {
366
- ...changes
367
- };
373
+ // When closing the menu, apply the same normalization as blur.
374
+ if (state.isOpen && !changes.isOpen && !allowCustomValue) {
375
+ const currentInput = state.inputValue ?? '';
376
+ const hasExactMatch = !!currentInput && items.some(item => itemToString(item) === currentInput);
377
+ if (!hasExactMatch) {
378
+ const restoredInput = state.selectedItem !== null ? itemToString(state.selectedItem) : '';
379
+ return {
380
+ ...changes,
381
+ inputValue: restoredInput
382
+ };
383
+ }
368
384
  }
369
385
  return changes;
370
386
  case MenuMouseLeave:
@@ -391,7 +407,7 @@ const ComboBox = /*#__PURE__*/forwardRef((props, ref) => {
391
407
  }
392
408
  },
393
409
  // eslint-disable-next-line react-hooks/exhaustive-deps
394
- [allowCustomValue, inputValue, onChange]);
410
+ [allowCustomValue, inputValue, itemToString, items, onChange]);
395
411
  const handleToggleClick = isOpen => event => {
396
412
  if (onToggleClick) {
397
413
  onToggleClick(event);
@@ -20,7 +20,7 @@ import { useId } from '../../internal/useId.js';
20
20
  import { usePrefix } from '../../internal/usePrefix.js';
21
21
  import { flip, hide, size, useFloating, autoUpdate } from '@floating-ui/react';
22
22
  import { useFeatureFlag } from '../FeatureFlags/index.js';
23
- import mergeRefs from '../../tools/mergeRefs.js';
23
+ import { mergeRefs } from '../../tools/mergeRefs.js';
24
24
  import { deprecateValuesWithin } from '../../prop-types/deprecateValuesWithin.js';
25
25
  import { mapPopoverAlign } from '../../tools/mapPopoverAlign.js';
26
26
 
@@ -6,14 +6,14 @@
6
6
  */
7
7
 
8
8
  import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
9
- import React, { useRef, useState, useEffect, Children, cloneElement } from 'react';
9
+ import React, { useRef, useState, useContext, useEffect, Children, cloneElement } from 'react';
10
10
  import { isElement } from 'react-is';
11
11
  import PropTypes from 'prop-types';
12
12
  import { Layer } from '../Layer/index.js';
13
13
  import { ModalHeader } from './ModalHeader.js';
14
14
  import { ModalFooter } from './ModalFooter.js';
15
15
  import useIsomorphicEffect from '../../internal/useIsomorphicEffect.js';
16
- import mergeRefs from '../../tools/mergeRefs.js';
16
+ import { mergeRefs } from '../../tools/mergeRefs.js';
17
17
  import cx from 'classnames';
18
18
  import { toggleClass } from '../../tools/toggleClass.js';
19
19
  import { requiredIfGivenPropIsTruthy } from '../../prop-types/requiredIfGivenPropIsTruthy.js';
@@ -28,6 +28,10 @@ import { Dialog } from '../Dialog/Dialog.js';
28
28
  import { warning } from '../../internal/warning.js';
29
29
  import { AILabel } from '../AILabel/index.js';
30
30
  import { isComponentElement } from '../../internal/utils.js';
31
+ import { useMergeRefs } from '@floating-ui/react';
32
+ import { ComposedModalPresenceContext, useExclusiveComposedModalPresenceContext, ComposedModalPresence } from './ComposedModalPresence.js';
33
+ import { useId } from '../../internal/useId.js';
34
+ import { useComposedModalState } from './useComposedModalState.js';
31
35
  import { debounce } from '../../node_modules/es-toolkit/dist/compat/function/debounce.js';
32
36
 
33
37
  const ModalBody = /*#__PURE__*/React.forwardRef(function ModalBody({
@@ -95,6 +99,34 @@ ModalBody.propTypes = {
95
99
  hasScrollingContent: PropTypes.bool
96
100
  };
97
101
  const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
102
+ open,
103
+ ...props
104
+ }, ref) {
105
+ const id = useId();
106
+ const enablePresence = useFeatureFlag('enable-presence');
107
+ const hasPresenceContext = Boolean(useContext(ComposedModalPresenceContext));
108
+ const hasPresenceOptIn = enablePresence || hasPresenceContext;
109
+ const exclusivePresenceContext = useExclusiveComposedModalPresenceContext(id);
110
+
111
+ // if opt in and not exclusive to a presence context, wrap with presence
112
+ if (hasPresenceOptIn && !exclusivePresenceContext) {
113
+ return /*#__PURE__*/React.createElement(ComposedModalPresence, {
114
+ open: open ?? false,
115
+ _presenceId: id
116
+ // do not auto enable styles for opt-in by feature flag
117
+ ,
118
+ _autoEnablePresence: hasPresenceContext
119
+ }, /*#__PURE__*/React.createElement(ComposedModalDialog, _extends({
120
+ open: true,
121
+ ref: ref
122
+ }, props)));
123
+ }
124
+ return /*#__PURE__*/React.createElement(ComposedModalDialog, _extends({
125
+ ref: ref,
126
+ open: open
127
+ }, props));
128
+ });
129
+ const ComposedModalDialog = /*#__PURE__*/React.forwardRef(function ComposedModalDialog({
98
130
  ['aria-labelledby']: ariaLabelledBy,
99
131
  ['aria-label']: ariaLabel,
100
132
  children,
@@ -105,7 +137,7 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
105
137
  isFullWidth,
106
138
  onClose,
107
139
  onKeyDown,
108
- open,
140
+ open: externalOpen,
109
141
  preventCloseOnClickOutside,
110
142
  selectorPrimaryFocus = '[data-modal-primary-focus]',
111
143
  selectorsFloatingMenus,
@@ -115,27 +147,30 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
115
147
  ...rest
116
148
  }, ref) {
117
149
  const prefix = usePrefix();
118
- const [isOpen, setIsOpen] = useState(!!open);
119
- const [wasOpen, setWasOpen] = useState(!!open);
120
150
  const innerModal = useRef(null);
121
151
  const button = useRef(null);
122
152
  const startSentinel = useRef(null);
123
153
  const endSentinel = useRef(null);
124
154
  const onMouseDownTarget = useRef(null);
155
+ const presenceContext = useContext(ComposedModalPresenceContext);
156
+ const mergedRefs = useMergeRefs([ref, presenceContext?.presenceRef]);
157
+ const enablePresence = useFeatureFlag('enable-presence') || presenceContext?.autoEnablePresence;
158
+
159
+ // always mark as open when mounted with presence
160
+ const open = externalOpen || enablePresence;
161
+ const modalState = useComposedModalState(open);
162
+ const [isOpen, setIsOpen] = presenceContext?.modalState ?? modalState;
125
163
  const enableDialogElement = useFeatureFlag('enable-dialog-element');
126
164
  const focusTrapWithoutSentinels = useFeatureFlag('enable-experimental-focus-wrap-without-sentinels');
127
165
  process.env.NODE_ENV !== "production" ? warning(!(focusTrapWithoutSentinels && enableDialogElement), '`<Modal>` detected both `focusTrapWithoutSentinels` and ' + '`enableDialogElement` feature flags are enabled. The native dialog ' + 'element handles focus, so `enableDialogElement` must be off for ' + '`focusTrapWithoutSentinels` to have any effect.') : void 0;
128
166
 
129
- // Keep track of modal open/close state
130
- // and propagate it to the document.body
167
+ // Propagate open/close state to the document.body
131
168
  useEffect(() => {
132
- if (!enableDialogElement && open !== wasOpen) {
133
- setIsOpen(!!open);
134
- setWasOpen(!!open);
169
+ if (!enableDialogElement) {
135
170
  toggleClass(document.body, `${prefix}--body--with-modal-open`, !!open);
136
171
  }
137
172
  // eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20071
138
- }, [open, wasOpen, prefix]);
173
+ }, [open, prefix]);
139
174
  // Remove the document.body className on unmount
140
175
  useEffect(() => {
141
176
  if (!enableDialogElement) {
@@ -226,7 +261,8 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
226
261
  }
227
262
  }
228
263
  const modalClass = cx(`${prefix}--modal`, {
229
- 'is-visible': isOpen,
264
+ 'is-visible': enablePresence || isOpen,
265
+ [`${prefix}--modal--enable-presence`]: presenceContext?.autoEnablePresence,
230
266
  [`${prefix}--modal--danger`]: danger,
231
267
  [`${prefix}--modal--slug`]: slug,
232
268
  [`${prefix}--modal--decorator`]: decorator
@@ -280,12 +316,23 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
280
316
  // eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20071
281
317
  }, [open]);
282
318
  useEffect(() => {
283
- if (!enableDialogElement && !open && launcherButtonRef) {
319
+ if (!enableDialogElement && !enablePresence && !open && launcherButtonRef) {
284
320
  setTimeout(() => {
285
321
  launcherButtonRef.current?.focus();
286
322
  });
287
323
  }
288
- }, [enableDialogElement, open, launcherButtonRef]);
324
+ }, [enableDialogElement, enablePresence, open, launcherButtonRef]);
325
+ // Focus launcherButtonRef on unmount
326
+ useEffect(() => {
327
+ const launcherButton = launcherButtonRef?.current;
328
+ return () => {
329
+ if (enablePresence && launcherButton) {
330
+ setTimeout(() => {
331
+ launcherButton.focus();
332
+ });
333
+ }
334
+ };
335
+ }, [enablePresence, launcherButtonRef]);
289
336
  useEffect(() => {
290
337
  if (!enableDialogElement) {
291
338
  const initialFocus = focusContainerElement => {
@@ -324,7 +371,8 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
324
371
  modal: true,
325
372
  className: containerClass,
326
373
  "aria-label": ariaLabel ? ariaLabel : generatedAriaLabel,
327
- "aria-labelledby": ariaLabelledBy
374
+ "aria-labelledby": ariaLabelledBy,
375
+ "data-exiting": presenceContext?.isExiting || undefined
328
376
  }, /*#__PURE__*/React.createElement("div", {
329
377
  ref: innerModal,
330
378
  className: `${prefix}--modal-container-body`
@@ -353,13 +401,14 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
353
401
  return /*#__PURE__*/React.createElement(Layer, _extends({}, rest, {
354
402
  level: 0,
355
403
  role: "presentation",
356
- ref: ref,
404
+ ref: mergedRefs,
357
405
  "aria-hidden": !open,
358
406
  onBlur: handleBlur,
359
407
  onClick: composeEventHandlers([rest?.onClick, handleOnClick]),
360
408
  onMouseDown: composeEventHandlers([rest?.onMouseDown, handleOnMouseDown]),
361
409
  onKeyDown: handleKeyDown,
362
- className: modalClass
410
+ className: modalClass,
411
+ "data-exiting": presenceContext?.isExiting || undefined
363
412
  }), modalBody);
364
413
  });
365
414
  ComposedModal.propTypes = {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright IBM Corp. 2016, 2025
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ import React, { type PropsWithChildren } from 'react';
8
+ import { type PresenceContext } from '../../internal/usePresenceContext';
9
+ import { useComposedModalState } from './useComposedModalState';
10
+ export interface ComposedModalPresenceProps {
11
+ /**
12
+ * Specify whether the Modal is currently open
13
+ */
14
+ open: boolean;
15
+ /**
16
+ * Internal property for backwards compatibility. Specify whether the Modal should opt in to presence mode.
17
+ */
18
+ _autoEnablePresence?: boolean;
19
+ /**
20
+ * Internal property to predefine the presence context's id for exclusivity.
21
+ */
22
+ _presenceId?: string;
23
+ }
24
+ export declare const ComposedModalPresence: ({ open, _presenceId: presenceId, _autoEnablePresence: autoEnablePresence, children, }: PropsWithChildren<ComposedModalPresenceProps>) => import("react/jsx-runtime").JSX.Element | null;
25
+ interface ComposedModalPresenceContextProps extends PresenceContext {
26
+ modalState: ReturnType<typeof useComposedModalState>;
27
+ autoEnablePresence: boolean;
28
+ }
29
+ export declare const ComposedModalPresenceContext: React.Context<ComposedModalPresenceContextProps | undefined>;
30
+ /**
31
+ * Handles occurrences where only a single composed modal must consume a context.
32
+ */
33
+ export declare const useExclusiveComposedModalPresenceContext: (id: string) => ComposedModalPresenceContextProps | undefined;
34
+ export {};
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright IBM Corp. 2016, 2023
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import React, { createContext, useContext, useMemo } from 'react';
9
+ import { usePresenceContext } from '../../internal/usePresenceContext.js';
10
+ import { useComposedModalState } from './useComposedModalState.js';
11
+
12
+ const ComposedModalPresence = ({
13
+ open,
14
+ _presenceId: presenceId,
15
+ _autoEnablePresence: autoEnablePresence = true,
16
+ children
17
+ }) => {
18
+ // Since the modal could be used without an onClose callback, we need to be aware of the internal isOpen state
19
+ const modalState = useComposedModalState(open);
20
+ const [isOpen] = modalState;
21
+ const [isPresent, context] = usePresenceContext(isOpen, presenceId);
22
+ const presenceContextValue = useMemo(() => ({
23
+ modalState,
24
+ autoEnablePresence,
25
+ ...context
26
+ }), [modalState, autoEnablePresence, context]);
27
+ if (!isPresent) return null;
28
+ return /*#__PURE__*/React.createElement(ComposedModalPresenceContext, {
29
+ value: presenceContextValue
30
+ }, children);
31
+ };
32
+ const ComposedModalPresenceContext = /*#__PURE__*/createContext(undefined);
33
+
34
+ /**
35
+ * Handles occurrences where only a single composed modal must consume a context.
36
+ */
37
+ const useExclusiveComposedModalPresenceContext = id => {
38
+ const ctx = useContext(ComposedModalPresenceContext);
39
+ return ctx?.isPresenceExclusive(id) ? ctx : undefined;
40
+ };
41
+
42
+ export { ComposedModalPresence, ComposedModalPresenceContext, useExclusiveComposedModalPresenceContext };
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import ComposedModal from './ComposedModal';
8
8
  export { default as ComposedModal, type ComposedModalProps, ModalBody, type ModalBodyProps, } from './ComposedModal';
9
+ export { ComposedModalPresence, type ComposedModalPresenceProps, } from './ComposedModalPresence';
9
10
  export { ModalHeader, type ModalHeaderProps } from './ModalHeader';
10
11
  export { ModalFooter, type ModalFooterProps } from './ModalFooter';
11
12
  export default ComposedModal;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright IBM Corp. 2016, 2025
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+ export declare const useComposedModalState: (open: boolean | undefined) => readonly [boolean, import("react").Dispatch<import("react").SetStateAction<boolean>>];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Copyright IBM Corp. 2016, 2023
3
+ *
4
+ * This source code is licensed under the Apache-2.0 license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ */
7
+
8
+ import { useState, useEffect, useMemo } from 'react';
9
+
10
+ const useComposedModalState = open => {
11
+ const [isOpen, setIsOpen] = useState(!!open);
12
+ const [wasOpen, setWasOpen] = useState(!!open);
13
+
14
+ // Keep track of modal open/close state
15
+ useEffect(() => {
16
+ if (open !== wasOpen) {
17
+ setIsOpen(!!open);
18
+ setWasOpen(!!open);
19
+ }
20
+ }, [open, wasOpen]);
21
+ return useMemo(() => [isOpen, setIsOpen], [isOpen]);
22
+ };
23
+
24
+ export { useComposedModalState };
@@ -12,9 +12,9 @@ import React from 'react';
12
12
  import Button from '../Button/Button.js';
13
13
  import '../Button/Button.Skeleton.js';
14
14
  import TableActionList from './TableActionList.js';
15
- import '../Text/index.js';
16
- import { usePrefix } from '../../internal/usePrefix.js';
17
15
  import { Text } from '../Text/Text.js';
16
+ import '../Text/TextDirection.js';
17
+ import { usePrefix } from '../../internal/usePrefix.js';
18
18
 
19
19
  const TableBatchActionsTranslationKeys = ['carbon.table.batch.cancel', 'carbon.table.batch.items.selected', 'carbon.table.batch.item.selected', 'carbon.table.batch.selectAll'];
20
20
  const translationKeys = {
@@ -14,11 +14,11 @@ import { usePrefix } from '../../internal/usePrefix.js';
14
14
  import '../FluidForm/FluidForm.js';
15
15
  import { FormContext } from '../FluidForm/FormContext.js';
16
16
  import { useId } from '../../internal/useId.js';
17
- import '../Text/index.js';
17
+ import { Text } from '../Text/Text.js';
18
+ import '../Text/TextDirection.js';
18
19
  import { deprecate } from '../../prop-types/deprecate.js';
19
20
  import { AILabel } from '../AILabel/index.js';
20
21
  import { isComponentElement } from '../../internal/utils.js';
21
- import { Text } from '../Text/Text.js';
22
22
 
23
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- https://github.com/carbon-design-system/carbon/issues/20071
24
24
 
@@ -14,7 +14,8 @@ import cx from 'classnames';
14
14
  import { Close } from '@carbon/icons-react';
15
15
  import { IconButton } from '../IconButton/index.js';
16
16
  import { noopFn } from '../../internal/noopFn.js';
17
- import '../Text/index.js';
17
+ import { Text } from '../Text/Text.js';
18
+ import '../Text/TextDirection.js';
18
19
  import { Layer } from '../Layer/index.js';
19
20
  import ButtonSet from '../ButtonSet/ButtonSet.js';
20
21
  import Button from '../Button/Button.js';
@@ -22,7 +23,6 @@ import '../Button/Button.Skeleton.js';
22
23
  import { useId } from '../../internal/useId.js';
23
24
  import InlineLoading from '../InlineLoading/InlineLoading.js';
24
25
  import { debounce } from '../../node_modules/es-toolkit/dist/compat/function/debounce.js';
25
- import { Text } from '../Text/Text.js';
26
26
 
27
27
  const DialogContext = /*#__PURE__*/createContext({});
28
28
 
@@ -6,13 +6,13 @@
6
6
  */
7
7
 
8
8
  import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
9
- import React, { useEffect, useContext, useCallback, useMemo, useState, isValidElement, cloneElement } from 'react';
9
+ import React, { useEffect, useContext, useCallback, useMemo, useState, useRef, isValidElement, cloneElement } from 'react';
10
10
  import { useSelect } from 'downshift';
11
11
  import cx from 'classnames';
12
12
  import PropTypes from 'prop-types';
13
13
  import { WarningFilled, WarningAltFilled, Checkmark } from '@carbon/icons-react';
14
14
  import ListBox from '../ListBox/index.js';
15
- import mergeRefs from '../../tools/mergeRefs.js';
15
+ import { mergeRefs } from '../../tools/mergeRefs.js';
16
16
  import { deprecate } from '../../prop-types/deprecate.js';
17
17
  import { usePrefix } from '../../internal/usePrefix.js';
18
18
  import '../FluidForm/FluidForm.js';
@@ -251,7 +251,8 @@ const Dropdown = /*#__PURE__*/React.forwardRef(({
251
251
  const handleFocus = evt => {
252
252
  setIsFocused(evt.type === 'focus' && !selectedItem ? true : false);
253
253
  };
254
- const mergedRef = mergeRefs(toggleButtonProps.ref, ref);
254
+ const buttonRef = useRef(null);
255
+ const mergedRef = mergeRefs(toggleButtonProps.ref, ref, buttonRef);
255
256
  const [currTimer, setCurrTimer] = useState();
256
257
  const [isTyping, setIsTyping] = useState(false);
257
258
  const onKeyDownHandler = useCallback(evt => {
@@ -287,7 +288,7 @@ const Dropdown = /*#__PURE__*/React.forwardRef(({
287
288
  // NOTE: does not prevent click
288
289
  evt.preventDefault();
289
290
  // focus on the element as per readonly input behavior
290
- mergedRef?.current?.focus();
291
+ buttonRef.current?.focus();
291
292
  },
292
293
  onKeyDown: evt => {
293
294
  const selectAccessKeys = ['ArrowDown', 'ArrowUp', ' ', 'Enter'];
@@ -302,7 +303,6 @@ const Dropdown = /*#__PURE__*/React.forwardRef(({
302
303
  onKeyDown: onKeyDownHandler
303
304
  };
304
305
  }
305
- // eslint-disable-next-line react-hooks/exhaustive-deps -- https://github.com/carbon-design-system/carbon/issues/20071
306
306
  }, [readOnly, onKeyDownHandler]);
307
307
  const menuProps = useMemo(() => getMenuProps({
308
308
  ref: enableFloatingStyles || autoAlign ? refs.setFloating : null
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright IBM Corp. 2021
2
+ * Copyright IBM Corp. 2021, 2025
3
3
  *
4
4
  * This source code is licensed under the Apache-2.0 license found in the
5
5
  * LICENSE file in the root directory of this source tree.
@@ -14,7 +14,7 @@ import { usePrefix } from '../../internal/usePrefix.js';
14
14
  import { composeEventHandlers } from '../../tools/events.js';
15
15
  import { Escape } from '../../internal/keyboard/keys.js';
16
16
  import { match } from '../../internal/keyboard/match.js';
17
- import mergeRefs from '../../tools/mergeRefs.js';
17
+ import { mergeRefs } from '../../tools/mergeRefs.js';
18
18
 
19
19
  const ExpandableSearch = /*#__PURE__*/React.forwardRef(function ExpandableSearch({
20
20
  onBlur,