@carbon/react 1.89.0-rc.1 → 1.89.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 (35) hide show
  1. package/.playwright/INTERNAL_AVT_REPORT_DO_NOT_USE.json +1099 -837
  2. package/es/components/ComposedModal/ComposedModal.js +2 -2
  3. package/es/components/DataTable/DataTable.js +10 -3
  4. package/es/components/Dialog/Dialog.d.ts +245 -0
  5. package/es/components/Dialog/Dialog.js +593 -0
  6. package/es/components/Dialog/index.d.ts +3 -251
  7. package/es/components/Dialog/index.js +1 -609
  8. package/es/components/FeatureFlags/index.d.ts +3 -1
  9. package/es/components/FeatureFlags/index.js +5 -2
  10. package/es/components/FileUploader/FileUploader.d.ts +28 -6
  11. package/es/components/FileUploader/FileUploader.js +152 -38
  12. package/es/components/Menu/MenuItem.js +2 -1
  13. package/es/components/Modal/Modal.js +2 -2
  14. package/es/components/StructuredList/StructuredList.js +4 -2
  15. package/es/components/Toggletip/index.js +1 -0
  16. package/es/index.d.ts +2 -1
  17. package/es/index.js +2 -0
  18. package/lib/components/ComposedModal/ComposedModal.js +2 -2
  19. package/lib/components/DataTable/DataTable.js +10 -3
  20. package/lib/components/Dialog/Dialog.d.ts +245 -0
  21. package/lib/components/Dialog/Dialog.js +602 -0
  22. package/lib/components/Dialog/index.d.ts +3 -251
  23. package/lib/components/Dialog/index.js +9 -614
  24. package/lib/components/FeatureFlags/index.d.ts +3 -1
  25. package/lib/components/FeatureFlags/index.js +5 -2
  26. package/lib/components/FileUploader/FileUploader.d.ts +28 -6
  27. package/lib/components/FileUploader/FileUploader.js +151 -37
  28. package/lib/components/Menu/MenuItem.js +2 -1
  29. package/lib/components/Modal/Modal.js +9 -9
  30. package/lib/components/StructuredList/StructuredList.js +4 -2
  31. package/lib/components/Toggletip/index.js +1 -0
  32. package/lib/index.d.ts +2 -1
  33. package/lib/index.js +60 -58
  34. package/package.json +13 -13
  35. package/telemetry.yml +2 -0
@@ -5,6 +5,21 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
  import React, { type HTMLAttributes } from 'react';
8
+ interface FileItem {
9
+ name: string;
10
+ uuid: string;
11
+ file: File;
12
+ }
13
+ export interface FileChangeData {
14
+ addedFiles: FileItem[];
15
+ removedFiles: FileItem[];
16
+ currentFiles: FileItem[];
17
+ action: 'add' | 'remove' | 'clear';
18
+ }
19
+ export interface FileDeleteData {
20
+ deletedFile: FileItem;
21
+ remainingFiles: FileItem[];
22
+ }
8
23
  export interface FileUploaderProps extends HTMLAttributes<HTMLSpanElement> {
9
24
  /**
10
25
  * Specify the types of files that this input should be able to receive
@@ -52,20 +67,23 @@ export interface FileUploaderProps extends HTMLAttributes<HTMLSpanElement> {
52
67
  */
53
68
  name?: string;
54
69
  /**
55
- * Provide an optional `onChange` hook that is called each time the input is
56
- * changed
70
+ * Provide an optional `onChange` hook that is called each time the input is changed.
71
+ * When 'enable-enhanced-file-uploader' feature flag is enabled:
72
+ * - Also fires for file deletions and clearFiles operations
73
+ * - Event includes enhanced file information in event.target
57
74
  */
58
- onChange?: (event: any) => void;
75
+ onChange?: (event: any, data?: FileChangeData) => void;
59
76
  /**
60
77
  * Provide an optional `onClick` hook that is called each time the
61
78
  * FileUploader is clicked
62
79
  */
63
80
  onClick?: (event: any) => void;
64
81
  /**
65
- * Provide an optional `onDelete` hook that is called when an uploaded item
66
- * is removed
82
+ * Provide an optional `onDelete` hook that is called when an uploaded item is removed.
83
+ * When 'enable-enhanced-file-uploader' feature flag is enabled:
84
+ * - Event includes deleted file information in event.target
67
85
  */
68
- onDelete?: (event: any) => void;
86
+ onDelete?: (event: any, data?: FileDeleteData) => void;
69
87
  /**
70
88
  * Specify the size of the FileUploaderButton, from a list of available
71
89
  * sizes.
@@ -77,6 +95,10 @@ export interface FileUploaderHandle {
77
95
  * Clear internal state
78
96
  */
79
97
  clearFiles: () => void;
98
+ /**
99
+ * Get current files (only available when 'enable-enhanced-file-uploader' feature flag is enabled)
100
+ */
101
+ getCurrentFiles?: () => FileItem[];
80
102
  }
81
103
  declare const FileUploader: {
82
104
  <ItemType>(props: FileUploaderProps): React.ReactElement<any>;
@@ -8,7 +8,7 @@
8
8
  import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
9
9
  import cx from 'classnames';
10
10
  import PropTypes from 'prop-types';
11
- import React, { useState, useImperativeHandle } from 'react';
11
+ import React, { useState, useCallback, useImperativeHandle } from 'react';
12
12
  import Filename from './Filename.js';
13
13
  import FileUploaderButton from './FileUploaderButton.js';
14
14
  import { ButtonKinds } from '../Button/Button.js';
@@ -17,6 +17,7 @@ import { matches } from '../../internal/keyboard/match.js';
17
17
  import { usePrefix } from '../../internal/usePrefix.js';
18
18
  import '../Text/index.js';
19
19
  import { useId } from '../../internal/useId.js';
20
+ import { useFeatureFlag } from '../FeatureFlags/index.js';
20
21
  import { Text } from '../Text/Text.js';
21
22
 
22
23
  const FileUploader = /*#__PURE__*/React.forwardRef(({
@@ -38,45 +39,149 @@ const FileUploader = /*#__PURE__*/React.forwardRef(({
38
39
  ...other
39
40
  }, ref) => {
40
41
  const fileUploaderInstanceId = useId('file-uploader');
41
- const [state, updateState] = useState({
42
- fileNames: []
43
- });
44
- const nodes = [];
45
42
  const prefix = usePrefix();
46
- const handleChange = evt => {
43
+ const enhancedFileUploaderEnabled = useFeatureFlag('enable-enhanced-file-uploader');
44
+ const [fileItems, setFileItems] = useState([]);
45
+ const [legacyFileNames, setLegacyFileNames] = useState([]);
46
+ const [fileObjects, setFileObjects] = useState(new Map());
47
+ const nodes = [];
48
+ const createFileItem = file => ({
49
+ name: file.name,
50
+ uuid: `${fileUploaderInstanceId}-${Date.now()}-${Array.from(crypto.getRandomValues(new Uint8Array(8))).map(b => b.toString(36)).join('')}`,
51
+ file
52
+ });
53
+ const handleChange = useCallback(evt => {
47
54
  evt.stopPropagation();
48
- const filenames = Array.prototype.map.call(evt.target.files, file => file.name);
49
- updateState(prevState => ({
50
- fileNames: multiple ? [...new Set([...prevState.fileNames, ...filenames])] : filenames
51
- }));
52
- if (onChange) {
53
- onChange(evt);
55
+ const newFiles = Array.from(evt.target.files);
56
+ if (enhancedFileUploaderEnabled) {
57
+ const newFileItems = newFiles.map(createFileItem);
58
+ let updatedFileItems;
59
+ if (multiple) {
60
+ const existingNames = new Set(fileItems.map(item => item.name));
61
+ const uniqueNewItems = newFileItems.filter(item => !existingNames.has(item.name));
62
+ updatedFileItems = [...fileItems, ...uniqueNewItems];
63
+ } else {
64
+ updatedFileItems = newFileItems;
65
+ }
66
+ setFileItems(updatedFileItems);
67
+ if (onChange) {
68
+ const allFiles = updatedFileItems.map(item => item.file);
69
+ const enhancedEvent = {
70
+ ...evt,
71
+ target: {
72
+ ...evt.target,
73
+ files: Object.assign(allFiles, {
74
+ item: index => allFiles[index] || null
75
+ }),
76
+ addedFiles: newFileItems,
77
+ currentFiles: updatedFileItems,
78
+ action: 'add'
79
+ }
80
+ };
81
+ onChange(enhancedEvent);
82
+ }
83
+ } else {
84
+ const filenames = newFiles.map(file => file.name);
85
+ const updatedFileNames = multiple ? [...new Set([...legacyFileNames, ...filenames])] : filenames;
86
+ setLegacyFileNames(updatedFileNames);
87
+ setFileObjects(prevMap => {
88
+ const newMap = multiple ? new Map(prevMap) : new Map();
89
+ newFiles.forEach(file => {
90
+ newMap.set(file.name, file);
91
+ });
92
+ return newMap;
93
+ });
94
+ if (onChange) {
95
+ onChange(evt);
96
+ }
54
97
  }
55
- };
56
- const handleClick = (evt, {
98
+ }, [enhancedFileUploaderEnabled, fileItems, legacyFileNames, multiple, onChange]);
99
+ const handleClick = useCallback((evt, {
57
100
  index,
58
101
  filenameStatus
59
102
  }) => {
60
103
  if (filenameStatus === 'edit') {
61
104
  evt.stopPropagation();
62
- const filteredArray = state.fileNames.filter(filename => filename !== nodes[index]?.innerText?.trim());
63
- updateState({
64
- fileNames: filteredArray
65
- });
66
- if (onDelete) {
67
- onDelete(evt);
68
- uploaderButton.current?.focus?.();
105
+ if (enhancedFileUploaderEnabled) {
106
+ const deletedItem = fileItems[index];
107
+ if (!deletedItem) return;
108
+ const remainingItems = fileItems.filter((_, i) => i !== index);
109
+ setFileItems(remainingItems);
110
+ const remainingFiles = remainingItems.map(item => item.file);
111
+ const enhancedEvent = {
112
+ ...evt,
113
+ target: {
114
+ ...evt.target,
115
+ files: Object.assign(remainingFiles, {
116
+ item: index => remainingFiles[index] || null
117
+ }),
118
+ deletedFile: deletedItem,
119
+ deletedFileName: deletedItem.name,
120
+ remainingFiles: remainingItems,
121
+ currentFiles: remainingItems,
122
+ action: 'remove'
123
+ }
124
+ };
125
+ if (onDelete) {
126
+ onDelete(enhancedEvent);
127
+ }
128
+ if (onChange) {
129
+ onChange(enhancedEvent);
130
+ }
131
+ } else {
132
+ const deletedFileName = legacyFileNames[index];
133
+ const filteredArray = legacyFileNames.filter(filename => filename !== deletedFileName);
134
+ setLegacyFileNames(filteredArray);
135
+
136
+ // Update File objects
137
+ setFileObjects(prevMap => {
138
+ const newMap = new Map(prevMap);
139
+ if (deletedFileName) {
140
+ newMap.delete(deletedFileName);
141
+ }
142
+ return newMap;
143
+ });
144
+ if (onDelete) {
145
+ onDelete(evt);
146
+ }
147
+ }
148
+ if (onClick) {
149
+ onClick(evt);
69
150
  }
70
- onClick?.(evt);
151
+ uploaderButton.current?.focus?.();
71
152
  }
72
- };
153
+ }, [enhancedFileUploaderEnabled, fileItems, legacyFileNames, onDelete, onChange, onClick]);
73
154
  useImperativeHandle(ref, () => ({
74
155
  clearFiles() {
75
- updateState({
76
- fileNames: []
77
- });
78
- }
79
- }));
156
+ if (enhancedFileUploaderEnabled) {
157
+ const previousItems = [...fileItems];
158
+ setFileItems([]);
159
+ if (onChange && previousItems.length > 0) {
160
+ const enhancedEvent = {
161
+ target: {
162
+ files: Object.assign([], {
163
+ item: () => null
164
+ }),
165
+ clearedFiles: previousItems,
166
+ currentFiles: [],
167
+ action: 'clear'
168
+ },
169
+ preventDefault: () => {},
170
+ stopPropagation: () => {}
171
+ };
172
+ onChange(enhancedEvent);
173
+ }
174
+ } else {
175
+ setLegacyFileNames([]);
176
+ setFileObjects(new Map());
177
+ }
178
+ },
179
+ ...(enhancedFileUploaderEnabled && {
180
+ getCurrentFiles() {
181
+ return [...fileItems];
182
+ }
183
+ })
184
+ }), [enhancedFileUploaderEnabled, fileItems, onChange]);
80
185
  const uploaderButton = /*#__PURE__*/React.createRef();
81
186
  const classes = cx({
82
187
  [`${prefix}--form-item`]: true,
@@ -89,6 +194,15 @@ const FileUploader = /*#__PURE__*/React.forwardRef(({
89
194
  [`${prefix}--file__selected-file--md`]: size === 'field' || size === 'md',
90
195
  [`${prefix}--file__selected-file--sm`]: size === 'small' || size === 'sm'
91
196
  });
197
+ const displayFiles = enhancedFileUploaderEnabled ? fileItems.map((item, index) => ({
198
+ name: item.name,
199
+ key: item.uuid,
200
+ index
201
+ })) : legacyFileNames.map((name, index) => ({
202
+ name,
203
+ key: index,
204
+ index
205
+ }));
92
206
  return /*#__PURE__*/React.createElement("div", _extends({
93
207
  className: classes
94
208
  }, other), !labelTitle ? null : /*#__PURE__*/React.createElement(Text, {
@@ -112,32 +226,32 @@ const FileUploader = /*#__PURE__*/React.forwardRef(({
112
226
  "aria-describedby": fileUploaderInstanceId
113
227
  }), /*#__PURE__*/React.createElement("div", {
114
228
  className: `${prefix}--file-container`
115
- }, state.fileNames.length === 0 ? null : state.fileNames.map((name, index) => /*#__PURE__*/React.createElement("span", _extends({
116
- key: index,
229
+ }, displayFiles.length === 0 ? null : displayFiles.map(file => /*#__PURE__*/React.createElement("span", _extends({
230
+ key: file.key,
117
231
  className: selectedFileClasses,
118
232
  ref: node => {
119
- nodes[index] = node;
120
- } // eslint-disable-line
233
+ nodes[file.index] = node;
234
+ }
121
235
  }, other), /*#__PURE__*/React.createElement(Text, {
122
236
  as: "p",
123
237
  className: `${prefix}--file-filename`,
124
- id: name
125
- }, name), /*#__PURE__*/React.createElement("span", {
238
+ id: enhancedFileUploaderEnabled ? `${fileUploaderInstanceId}-file-${fileItems[file.index]?.uuid || file.index}` : `${fileUploaderInstanceId}-file-${file.index}`
239
+ }, file.name), /*#__PURE__*/React.createElement("span", {
126
240
  className: `${prefix}--file__state-container`
127
241
  }, /*#__PURE__*/React.createElement(Filename, {
128
- name: name,
242
+ name: file.name,
129
243
  iconDescription: iconDescription,
130
244
  status: filenameStatus,
131
245
  onKeyDown: evt => {
132
246
  if (matches(evt, [Enter, Space])) {
133
247
  handleClick(evt, {
134
- index,
248
+ index: file.index,
135
249
  filenameStatus
136
250
  });
137
251
  }
138
252
  },
139
253
  onClick: evt => handleClick(evt, {
140
- index,
254
+ index: file.index,
141
255
  filenameStatus
142
256
  })
143
257
  }))))));
@@ -207,7 +321,7 @@ FileUploader.propTypes = {
207
321
  * Specify the size of the FileUploaderButton, from a list of available
208
322
  * sizes.
209
323
  */
210
- size: PropTypes.oneOf(['sm', 'md', 'lg'])
324
+ size: PropTypes.oneOf(['sm', 'small', 'md', 'field', 'lg'])
211
325
  };
212
326
 
213
327
  export { FileUploader as default };
@@ -49,7 +49,8 @@ const MenuItem = /*#__PURE__*/forwardRef(function MenuItem({
49
49
  middleware: [offset({
50
50
  mainAxis: -6,
51
51
  crossAxis: -6
52
- })]
52
+ })],
53
+ strategy: 'fixed'
53
54
  });
54
55
  const {
55
56
  getReferenceProps,
@@ -30,7 +30,7 @@ import '../Text/index.js';
30
30
  import { useFeatureFlag } from '../FeatureFlags/index.js';
31
31
  import { composeEventHandlers } from '../../tools/events.js';
32
32
  import { deprecate } from '../../prop-types/deprecate.js';
33
- import { unstable__Dialog } from '../Dialog/index.js';
33
+ import { Dialog } from '../Dialog/Dialog.js';
34
34
  import { AILabel } from '../AILabel/index.js';
35
35
  import { isComponentElement } from '../../internal/utils.js';
36
36
  import { warning } from '../../internal/warning.js';
@@ -314,7 +314,7 @@ const Modal = /*#__PURE__*/React.forwardRef(function Modal({
314
314
  // alertdialog is the only permitted aria role for a native dialog element
315
315
  // https://www.w3.org/TR/html-aria/#docconformance:~:text=Role%3A-,alertdialog,-.%20(dialog%20is
316
316
  const isAlertDialog = alert && !passiveModal;
317
- const modalBody = enableDialogElement ? /*#__PURE__*/React.createElement(unstable__Dialog, {
317
+ const modalBody = enableDialogElement ? /*#__PURE__*/React.createElement(Dialog, {
318
318
  open: open,
319
319
  focusAfterCloseRef: launcherButtonRef,
320
320
  modal: true,
@@ -189,8 +189,10 @@ function StructuredListRow(props) {
189
189
  setHasFocusWithin(true);
190
190
  }
191
191
  },
192
- onFocus: () => {
193
- setHasFocusWithin(true);
192
+ onFocus: event => {
193
+ if (selection || event.currentTarget === event.target) {
194
+ setHasFocusWithin(true);
195
+ }
194
196
  },
195
197
  onBlur: () => {
196
198
  setHasFocusWithin(false);
@@ -100,6 +100,7 @@ function Toggletip({
100
100
  };
101
101
  const onKeyDown = event => {
102
102
  if (open && match(event, Escape)) {
103
+ event.stopPropagation();
103
104
  actions.close();
104
105
 
105
106
  // If the menu is closed while focus is still inside the menu, it should return to the trigger button (#12922)
package/es/index.d.ts CHANGED
@@ -58,6 +58,7 @@ export * from './components/OrderedList';
58
58
  export * from './components/OverflowMenu';
59
59
  export * from './components/OverflowMenuItem';
60
60
  export * as unstable__PageHeader from './components/PageHeader';
61
+ export * as preview__Dialog from './components/Dialog';
61
62
  export * from './components/Pagination';
62
63
  export * from './components/Pagination/Pagination.Skeleton';
63
64
  export * from './components/PaginationNav';
@@ -185,7 +186,6 @@ export type { TableToolbarSearchProps } from './components/DataTable/TableToolba
185
186
  export type { DataTableSkeletonProps } from './components/DataTableSkeleton/DataTableSkeleton';
186
187
  export type { DatePickerProps } from './components/DatePicker/DatePicker';
187
188
  export type { DatePickerInputProps } from './components/DatePickerInput/DatePickerInput';
188
- export type { DialogProps } from './components/Dialog/index';
189
189
  export type { DropdownProps } from './components/Dropdown/Dropdown';
190
190
  export type { ErrorBoundaryProps } from './components/ErrorBoundary/ErrorBoundary';
191
191
  export type { FeatureFlagsProps } from './components/FeatureFlags/index';
@@ -267,6 +267,7 @@ export type { OrderedListProps } from './components/OrderedList/OrderedList';
267
267
  export type { OverflowMenuProps } from './components/OverflowMenu/OverflowMenu';
268
268
  export type { OverflowMenuItemProps } from './components/OverflowMenuItem/OverflowMenuItem';
269
269
  export type { PageHeaderProps, PageHeaderBreadcrumbBarProps, PageHeaderContentProps, PageHeaderHeroImageProps, PageHeaderTabBarProps, } from './components/PageHeader';
270
+ export type { DialogProps, DialogHeaderProps, DialogControlsProps, DialogCloseButtonProps, DialogTitleProps, DialogSubtitleProps, DialogBodyProps, DialogFooterProps, } from './components/Dialog';
270
271
  export type { PaginationProps } from './components/Pagination/Pagination';
271
272
  export type { PaginationSkeletonProps } from './components/Pagination/Pagination.Skeleton';
272
273
  export type { DirectionButtonProps } from './components/PaginationNav/PaginationNav';
package/es/index.js CHANGED
@@ -105,6 +105,8 @@ export { default as OverflowMenu } from './components/OverflowMenu/index.js';
105
105
  export { default as OverflowMenuItem } from './components/OverflowMenuItem/OverflowMenuItem.js';
106
106
  import * as index from './components/PageHeader/index.js';
107
107
  export { index as unstable__PageHeader };
108
+ import * as index$1 from './components/Dialog/index.js';
109
+ export { index$1 as preview__Dialog };
108
110
  export { default as Pagination } from './components/Pagination/Pagination.js';
109
111
  export { default as PaginationSkeleton } from './components/Pagination/Pagination.Skeleton.js';
110
112
  export { default as PaginationNav } from './components/PaginationNav/PaginationNav.js';
@@ -28,7 +28,7 @@ var match = require('../../internal/keyboard/match.js');
28
28
  var index$1 = require('../FeatureFlags/index.js');
29
29
  var events = require('../../tools/events.js');
30
30
  var deprecate = require('../../prop-types/deprecate.js');
31
- var index$3 = require('../Dialog/index.js');
31
+ var Dialog = require('../Dialog/Dialog.js');
32
32
  var warning = require('../../internal/warning.js');
33
33
  var index$2 = require('../AILabel/index.js');
34
34
  var utils = require('../../internal/utils.js');
@@ -308,7 +308,7 @@ const ComposedModal = /*#__PURE__*/React.forwardRef(function ComposedModal({
308
308
  const normalizedDecorator = candidateIsAILabel ? /*#__PURE__*/React.cloneElement(candidate, {
309
309
  size: 'sm'
310
310
  }) : null;
311
- const modalBody = enableDialogElement ? /*#__PURE__*/React.createElement(index$3.unstable__Dialog, {
311
+ const modalBody = enableDialogElement ? /*#__PURE__*/React.createElement(Dialog.Dialog, {
312
312
  open: open,
313
313
  focusAfterCloseRef: launcherButtonRef,
314
314
  modal: true,
@@ -551,9 +551,16 @@ const DataTable = props => {
551
551
  * @param headerKey - The field for the header that we are sorting by.
552
552
  */
553
553
  const handleSortBy = headerKey => () => {
554
- setState(prev => sorting.getNextSortState(props, prev, {
555
- key: headerKey
556
- }));
554
+ setState(prev => {
555
+ const sortState = sorting.getNextSortState(props, prev, {
556
+ key: headerKey
557
+ });
558
+ return {
559
+ ...prev,
560
+ // Preserve ALL existing state
561
+ ...sortState // Then apply only the sorting changes
562
+ };
563
+ });
557
564
  };
558
565
 
559
566
  /**