@itwin/itwinui-react 3.2.3 → 3.3.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 (63) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/cjs/core/ComboBox/ComboBox.d.ts +3 -1
  3. package/cjs/core/ComboBox/ComboBox.js +3 -4
  4. package/cjs/core/ComboBox/ComboBoxInputContainer.js +1 -1
  5. package/cjs/core/Dialog/DialogContext.d.ts +1 -0
  6. package/cjs/core/ExpandableBlock/ExpandableBlock.d.ts +7 -4
  7. package/cjs/core/ExpandableBlock/ExpandableBlock.js +25 -12
  8. package/cjs/core/InputGrid/InputGrid.d.ts +7 -5
  9. package/cjs/core/InputGrid/InputGrid.js +175 -6
  10. package/cjs/core/InputGroup/InputGroup.d.ts +1 -1
  11. package/cjs/core/InputGroup/InputGroup.js +4 -13
  12. package/cjs/core/LabeledInput/LabeledInput.d.ts +1 -2
  13. package/cjs/core/LabeledInput/LabeledInput.js +11 -8
  14. package/cjs/core/LabeledSelect/LabeledSelect.d.ts +21 -1
  15. package/cjs/core/LabeledSelect/LabeledSelect.js +5 -20
  16. package/cjs/core/List/ListItem.d.ts +10 -0
  17. package/cjs/core/List/ListItem.js +14 -0
  18. package/cjs/core/Menu/MenuItem.d.ts +1 -1
  19. package/cjs/core/Modal/Modal.d.ts +3 -4
  20. package/cjs/core/Select/Select.d.ts +3 -1
  21. package/cjs/core/Select/Select.js +2 -2
  22. package/cjs/core/StatusMessage/StatusMessage.d.ts +4 -2
  23. package/cjs/core/StatusMessage/StatusMessage.js +3 -1
  24. package/cjs/core/Table/columns/selectionColumn.d.ts +1 -1
  25. package/cjs/core/Table/columns/selectionColumn.js +3 -3
  26. package/cjs/core/utils/components/InputWithIcon.d.ts +2 -0
  27. package/cjs/core/utils/components/InputWithIcon.js +11 -0
  28. package/cjs/core/utils/components/Portal.d.ts +5 -1
  29. package/cjs/core/utils/components/Portal.js +6 -2
  30. package/cjs/core/utils/components/index.d.ts +1 -0
  31. package/cjs/core/utils/components/index.js +1 -0
  32. package/esm/core/ComboBox/ComboBox.d.ts +3 -1
  33. package/esm/core/ComboBox/ComboBox.js +3 -3
  34. package/esm/core/ComboBox/ComboBoxInputContainer.js +2 -2
  35. package/esm/core/Dialog/DialogContext.d.ts +1 -0
  36. package/esm/core/ExpandableBlock/ExpandableBlock.d.ts +7 -4
  37. package/esm/core/ExpandableBlock/ExpandableBlock.js +26 -13
  38. package/esm/core/InputGrid/InputGrid.d.ts +7 -5
  39. package/esm/core/InputGrid/InputGrid.js +176 -7
  40. package/esm/core/InputGroup/InputGroup.d.ts +1 -1
  41. package/esm/core/InputGroup/InputGroup.js +5 -14
  42. package/esm/core/LabeledInput/LabeledInput.d.ts +1 -2
  43. package/esm/core/LabeledInput/LabeledInput.js +8 -9
  44. package/esm/core/LabeledSelect/LabeledSelect.d.ts +21 -1
  45. package/esm/core/LabeledSelect/LabeledSelect.js +5 -19
  46. package/esm/core/List/ListItem.d.ts +10 -0
  47. package/esm/core/List/ListItem.js +14 -0
  48. package/esm/core/Menu/MenuItem.d.ts +1 -1
  49. package/esm/core/Modal/Modal.d.ts +3 -4
  50. package/esm/core/Select/Select.d.ts +3 -1
  51. package/esm/core/Select/Select.js +3 -3
  52. package/esm/core/StatusMessage/StatusMessage.d.ts +4 -2
  53. package/esm/core/StatusMessage/StatusMessage.js +3 -1
  54. package/esm/core/Table/columns/selectionColumn.d.ts +1 -1
  55. package/esm/core/Table/columns/selectionColumn.js +3 -3
  56. package/esm/core/utils/components/InputWithIcon.d.ts +2 -0
  57. package/esm/core/utils/components/InputWithIcon.js +8 -0
  58. package/esm/core/utils/components/Portal.d.ts +5 -1
  59. package/esm/core/utils/components/Portal.js +6 -2
  60. package/esm/core/utils/components/index.d.ts +1 -0
  61. package/esm/core/utils/components/index.js +1 -0
  62. package/package.json +3 -3
  63. package/styles.css +7 -7
@@ -34,6 +34,7 @@ exports.ListItem = void 0;
34
34
  const React = __importStar(require("react"));
35
35
  const classnames_1 = __importDefault(require("classnames"));
36
36
  const index_js_1 = require("../utils/index.js");
37
+ const LinkAction_js_1 = require("../LinkAction/LinkAction.js");
37
38
  const ListItemComponent = React.forwardRef((props, ref) => {
38
39
  const { size = 'default', disabled = false, active = false, actionable = false, focused = false, className, ...rest } = props;
39
40
  return (React.createElement(index_js_1.Box, { as: 'li', className: (0, classnames_1.default)('iui-list-item', className), "data-iui-active": active ? 'true' : undefined, "data-iui-disabled": disabled ? 'true' : undefined, "data-iui-size": size === 'large' ? 'large' : undefined, "data-iui-actionable": actionable ? 'true' : undefined, "data-iui-focused": focused ? 'true' : undefined, ref: ref, ...rest }));
@@ -49,6 +50,9 @@ ListItemContent.displayName = 'ListItem.Content';
49
50
  const ListItemDescription = (0, index_js_1.polymorphic)('iui-list-item-description');
50
51
  ListItemDescription.displayName = 'ListItem.Description';
51
52
  // ----------------------------------------------------------------------------
53
+ const ListItemAction = LinkAction_js_1.LinkAction;
54
+ ListItemAction.displayName = 'ListItem.Action';
55
+ // ----------------------------------------------------------------------------
52
56
  // Exported compound component
53
57
  /**
54
58
  * A generic ListItem component that can be used simply for displaying data, or as a base
@@ -90,4 +94,14 @@ exports.ListItem = Object.assign(ListItemComponent, {
90
94
  * </ListItem>
91
95
  */
92
96
  Description: ListItemDescription,
97
+ /**
98
+ * Wrapper over [LinkAction](https://itwinui.bentley.com/docs/linkaction) which allows rendering a link inside a ListItem.
99
+ * This ensures that clicking anywhere on the ListItem will trigger the link.
100
+ *
101
+ * @example
102
+ * <ListItem>
103
+ * <ListItem.Action href='https://example.com'>Example link</ListItem.Action>
104
+ * </ListItem>
105
+ */
106
+ Action: ListItemAction,
93
107
  });
@@ -43,7 +43,7 @@ export type MenuItemProps = {
43
43
  */
44
44
  endIcon?: JSX.Element;
45
45
  /**
46
- * @deprecated Use endIcon
46
+ * @deprecated Use endIcon.
47
47
  * SVG icon component shown on the right.
48
48
  */
49
49
  badge?: JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import type { PolymorphicForwardRefComponent } from '../utils/index.js';
2
+ import type { PolymorphicForwardRefComponent, PortalProps } from '../utils/index.js';
3
3
  import type { DialogMainProps } from '../Dialog/DialogMain.js';
4
4
  type ModalProps = {
5
5
  /**
@@ -33,12 +33,11 @@ type ModalProps = {
33
33
  * If true, the dialog will be portaled into a <div> inside the nearest `ThemeProvider`.
34
34
  *
35
35
  * Can be set to an object with a `to` property to portal into a specific element.
36
+ * If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
36
37
  *
37
38
  * @default true
38
39
  */
39
- portal?: boolean | {
40
- to: HTMLElement;
41
- };
40
+ portal?: PortalProps['portal'];
42
41
  /**
43
42
  * Content of the modal.
44
43
  */
@@ -168,5 +168,7 @@ export type SelectProps<T> = {
168
168
  * )}
169
169
  * />
170
170
  */
171
- export declare const Select: React.ForwardRefExoticComponent<SelectProps<unknown> & React.RefAttributes<HTMLElement>>;
171
+ export declare const Select: <T>(props: SelectProps<T> & {
172
+ ref?: React.ForwardedRef<HTMLElement> | undefined;
173
+ }) => JSX.Element;
172
174
  export default Select;
@@ -98,7 +98,7 @@ const isSingleOnChange = (onChange, multiple) => {
98
98
  */
99
99
  exports.Select = React.forwardRef((props, forwardedRef) => {
100
100
  const uid = (0, index_js_1.useId)();
101
- const { options, value: valueProp, onChange: onChangeProp, placeholder, disabled = false, size, itemRenderer, selectedItemRenderer, className, style, menuClassName, menuStyle, multiple = false, triggerProps, status, popoverProps, ...rest } = props;
101
+ const { options, value: valueProp, onChange: onChangeProp, placeholder, disabled = false, size, itemRenderer, selectedItemRenderer, menuClassName, menuStyle, multiple = false, triggerProps, status, popoverProps, ...rest } = props;
102
102
  const [isOpen, setIsOpen] = React.useState(false);
103
103
  const [liveRegionSelection, setLiveRegionSelection] = React.useState('');
104
104
  const [uncontrolledValue, setUncontrolledValue] = React.useState();
@@ -189,7 +189,7 @@ exports.Select = React.forwardRef((props, forwardedRef) => {
189
189
  onVisibleChange: (open) => (open ? show() : hide()),
190
190
  });
191
191
  return (React.createElement(React.Fragment, null,
192
- React.createElement(index_js_1.Box, { className: (0, classnames_1.default)('iui-input-with-icon', className), style: style, ...rest, ref: (0, index_js_1.useMergedRefs)(popover.refs.setPositionReference, forwardedRef) },
192
+ React.createElement(index_js_1.InputWithIcon, { ...rest, ref: (0, index_js_1.useMergedRefs)(popover.refs.setPositionReference, forwardedRef) },
193
193
  React.createElement(index_js_1.Box, { ...popover.getReferenceProps(), tabIndex: 0, role: 'combobox', "data-iui-size": size, "data-iui-status": status, "aria-disabled": disabled, "aria-autocomplete": 'none', "aria-expanded": isOpen, "aria-haspopup": 'listbox', "aria-controls": `${uid}-menu`, ...triggerProps, ref: (0, index_js_1.useMergedRefs)(selectRef, triggerProps?.ref, popover.refs.setReference), className: (0, classnames_1.default)('iui-select-button', {
194
194
  'iui-placeholder': (!selectedItems || selectedItems.length === 0) &&
195
195
  !!placeholder,
@@ -4,9 +4,11 @@ import { Icon } from '../Icon/Icon.js';
4
4
  type StatusMessageProps = {
5
5
  /**
6
6
  * Custom icon to be displayed at the beginning.
7
- * It will default to the `status` icon, if it's set.
7
+ *
8
+ * - It will default to the `status` icon, if `status` is set.
9
+ * - If `startIcon` is set to `null`, no icon will be displayed, even if `status` is set.
8
10
  */
9
- startIcon?: JSX.Element;
11
+ startIcon?: JSX.Element | null;
10
12
  /**
11
13
  * Message content.
12
14
  */
@@ -44,8 +44,10 @@ const Icon_js_1 = require("../Icon/Icon.js");
44
44
  exports.StatusMessage = React.forwardRef((props, ref) => {
45
45
  const { children, startIcon: userStartIcon, status, className, iconProps, contentProps, ...rest } = props;
46
46
  const icon = userStartIcon ?? (status && index_js_1.StatusIconMap[status]());
47
+ // If user passes null, we don't want to show the icon
48
+ const shouldShowIcon = userStartIcon !== null && !!icon;
47
49
  return (React.createElement(index_js_1.Box, { className: (0, classnames_1.default)('iui-status-message', className), "data-iui-status": status, ref: ref, ...rest },
48
- !!icon ? (React.createElement(Icon_js_1.Icon, { "aria-hidden": true, ...iconProps }, icon)) : null,
50
+ shouldShowIcon ? (React.createElement(Icon_js_1.Icon, { "aria-hidden": true, ...iconProps }, icon)) : null,
49
51
  React.createElement(index_js_1.Box, { ...contentProps }, children)));
50
52
  });
51
53
  exports.default = exports.StatusMessage;
@@ -27,7 +27,7 @@ export declare const SelectionColumn: <T extends Record<string, unknown>>(props?
27
27
  maxWidth: number;
28
28
  columnClassName: string;
29
29
  cellClassName: string;
30
- Header: ({ getToggleAllRowsSelectedProps, toggleAllRowsSelected, rows, initialRows, state, }: HeaderProps<T>) => React.JSX.Element;
30
+ Header: ({ getToggleAllRowsSelectedProps, toggleAllRowsSelected, rows, preFilteredFlatRows, state, }: HeaderProps<T>) => React.JSX.Element;
31
31
  Cell: ({ row }: CellProps<T>) => React.JSX.Element;
32
32
  cellRenderer: (props: CellRendererProps<T>) => React.JSX.Element;
33
33
  };
@@ -57,9 +57,9 @@ const SelectionColumn = (props = {}) => {
57
57
  maxWidth: densityWidth,
58
58
  columnClassName: 'iui-slot',
59
59
  cellClassName: 'iui-slot',
60
- Header: ({ getToggleAllRowsSelectedProps, toggleAllRowsSelected, rows, initialRows, state, }) => {
61
- const disabled = rows.every((row) => isDisabled?.(row.original));
62
- const checked = initialRows.every((row) => state.selectedRowIds[row.id] || isDisabled?.(row.original));
60
+ Header: ({ getToggleAllRowsSelectedProps, toggleAllRowsSelected, rows, preFilteredFlatRows, state, }) => {
61
+ const disabled = preFilteredFlatRows.every((row) => isDisabled?.(row.original));
62
+ const checked = preFilteredFlatRows.every((row) => state.selectedRowIds[row.id] || isDisabled?.(row.original));
63
63
  const indeterminate = !checked && Object.keys(state.selectedRowIds).length > 0;
64
64
  return (React.createElement(Checkbox_js_1.Checkbox, { ...getToggleAllRowsSelectedProps(), style: {}, title: '' // Removes default title that comes from react-table
65
65
  , checked: checked && !disabled, indeterminate: indeterminate, disabled: disabled, onChange: () => toggleAllRowsSelected(!rows.some((row) => row.isSelected)) }));
@@ -0,0 +1,2 @@
1
+ /** @private */
2
+ export declare const InputWithIcon: import("../props.js").PolymorphicForwardRefComponent<"div", {}>;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InputWithIcon = void 0;
4
+ /*---------------------------------------------------------------------------------------------
5
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
6
+ * See LICENSE.md in the project root for license terms and full copyright notice.
7
+ *--------------------------------------------------------------------------------------------*/
8
+ const polymorphic_js_1 = require("../functions/polymorphic.js");
9
+ /** @private */
10
+ exports.InputWithIcon = polymorphic_js_1.polymorphic.div('iui-input-with-icon');
11
+ exports.InputWithIcon.displayName = 'InputWithIcon';
@@ -9,10 +9,12 @@ export type PortalProps = {
9
9
  *
10
10
  * Otherwise, it will portal to the element passed to `to`.
11
11
  *
12
+ * If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
13
+ *
12
14
  * @default true
13
15
  */
14
16
  portal?: boolean | {
15
- to: HTMLElement | (() => HTMLElement);
17
+ to: HTMLElement | null | undefined | (() => HTMLElement | null | undefined);
16
18
  };
17
19
  };
18
20
  /**
@@ -21,6 +23,8 @@ export type PortalProps = {
21
23
  * - if `portal` is set to true, renders into nearest ThemeContext.portalContainer
22
24
  * - if `portal` is set to false, renders as-is without portal
23
25
  * - otherwise renders into `portal.to` (can be an element or a function)
26
+ * - If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
27
+ * - E.g. `portal={{ to: () => document.querySelector('.may-not-exist') }}`.
24
28
  *
25
29
  * @private
26
30
  */
@@ -40,6 +40,8 @@ const useIsClient_js_1 = require("../hooks/useIsClient.js");
40
40
  * - if `portal` is set to true, renders into nearest ThemeContext.portalContainer
41
41
  * - if `portal` is set to false, renders as-is without portal
42
42
  * - otherwise renders into `portal.to` (can be an element or a function)
43
+ * - If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
44
+ * - E.g. `portal={{ to: () => document.querySelector('.may-not-exist') }}`.
43
45
  *
44
46
  * @private
45
47
  */
@@ -56,8 +58,10 @@ exports.Portal = Portal;
56
58
  // ----------------------------------------------------------------------------
57
59
  const usePortalTo = (portal) => {
58
60
  const themeInfo = React.useContext(ThemeContext_js_1.ThemeContext);
61
+ const defaultPortalTo = themeInfo?.portalContainer ?? (0, dom_js_1.getDocument)()?.body;
59
62
  if (typeof portal === 'boolean') {
60
- return portal ? themeInfo?.portalContainer ?? (0, dom_js_1.getDocument)()?.body : null;
63
+ return portal ? defaultPortalTo : null;
61
64
  }
62
- return typeof portal.to === 'function' ? portal.to() : portal.to;
65
+ const portalTo = typeof portal.to === 'function' ? portal.to() : portal.to;
66
+ return portalTo ?? defaultPortalTo;
63
67
  };
@@ -2,6 +2,7 @@ export * from './Resizer.js';
2
2
  export * from './FocusTrap.js';
3
3
  export * from './InputContainer.js';
4
4
  export * from './InputFlexContainer.js';
5
+ export * from './InputWithIcon.js';
5
6
  export * from './WithCSSTransition.js';
6
7
  export * from './MiddleTextTruncation.js';
7
8
  export * from './VirtualScroll.js';
@@ -22,6 +22,7 @@ __exportStar(require("./Resizer.js"), exports);
22
22
  __exportStar(require("./FocusTrap.js"), exports);
23
23
  __exportStar(require("./InputContainer.js"), exports);
24
24
  __exportStar(require("./InputFlexContainer.js"), exports);
25
+ __exportStar(require("./InputWithIcon.js"), exports);
25
26
  __exportStar(require("./WithCSSTransition.js"), exports);
26
27
  __exportStar(require("./MiddleTextTruncation.js"), exports);
27
28
  __exportStar(require("./VirtualScroll.js"), exports);
@@ -102,5 +102,7 @@ export type ComboBoxProps<T> = {
102
102
  * onChange={() => {}}
103
103
  * />
104
104
  */
105
- export declare const ComboBox: <T>(props: ComboBoxProps<T>) => React.JSX.Element;
105
+ export declare const ComboBox: <T>(props: ComboBoxProps<T> & {
106
+ ref?: React.ForwardedRef<HTMLElement> | undefined;
107
+ }) => JSX.Element;
106
108
  export default ComboBox;
@@ -39,7 +39,7 @@ const getOptionId = (option, idPrefix) => {
39
39
  * onChange={() => {}}
40
40
  * />
41
41
  */
42
- export const ComboBox = (props) => {
42
+ export const ComboBox = React.forwardRef((props, forwardedRef) => {
43
43
  const idPrefix = useId();
44
44
  const { options, value: valueProp, onChange, filterFunction, inputProps, endIconProps, dropdownMenuProps, emptyStateMessage = 'No options found', itemRenderer, enableVirtualization = false, multiple = false, onShow: onShowProp, onHide: onHideProp, id = inputProps?.id ? `iui-${inputProps.id}-cb` : idPrefix, ...rest } = props;
45
45
  // Refs get set in subcomponents
@@ -299,7 +299,7 @@ export const ComboBox = (props) => {
299
299
  show,
300
300
  hide,
301
301
  } },
302
- React.createElement(ComboBoxInputContainer, { disabled: inputProps?.disabled, ...rest },
302
+ React.createElement(ComboBoxInputContainer, { ref: forwardedRef, disabled: inputProps?.disabled, ...rest },
303
303
  React.createElement(React.Fragment, null,
304
304
  React.createElement(ComboBoxInput, { value: inputValue, disabled: inputProps?.disabled, ...inputProps, onChange: handleOnInput, selectTags: isMultipleEnabled(selected, multiple)
305
305
  ? selected.map((index) => {
@@ -312,5 +312,5 @@ export const ComboBox = (props) => {
312
312
  React.createElement(ComboBoxMenu, { as: 'div', ...dropdownMenuProps }, filteredOptions.length > 0 && !enableVirtualization
313
313
  ? filteredOptions.map(getMenuItem)
314
314
  : emptyContent)))));
315
- };
315
+ });
316
316
  export default ComboBox;
@@ -4,13 +4,13 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import { StatusMessage } from '../StatusMessage/StatusMessage.js';
7
- import { InputContainer, useSafeContext, Box } from '../utils/index.js';
7
+ import { InputContainer, useSafeContext, InputWithIcon, } from '../utils/index.js';
8
8
  import { ComboBoxStateContext } from './helpers.js';
9
9
  export const ComboBoxInputContainer = React.forwardRef((props, forwardedRef) => {
10
10
  const { className, status, message, children, ...rest } = props;
11
11
  const { id } = useSafeContext(ComboBoxStateContext);
12
12
  return (React.createElement(InputContainer, { className: className, status: status, statusMessage: typeof message === 'string' ? (React.createElement(StatusMessage, { status: status }, message)) : (React.isValidElement(message) &&
13
13
  React.cloneElement(message, { status })), ref: forwardedRef, ...rest, id: id },
14
- React.createElement(Box, { className: 'iui-input-with-icon' }, children)));
14
+ React.createElement(InputWithIcon, null, children)));
15
15
  });
16
16
  ComboBoxInputContainer.displayName = 'ComboBoxInputContainer';
@@ -67,6 +67,7 @@ export type DialogContextProps = {
67
67
  * Recommended to set to true when for modal dialogs that use `relativeTo='viewport'`.
68
68
  *
69
69
  * Can be set to an object with a `to` property to portal into a specific element.
70
+ * If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
70
71
  *
71
72
  * Defaults to false if using `Dialog` and true if using `Modal`.
72
73
  */
@@ -73,7 +73,7 @@ export declare const ExpandableBlock: PolymorphicForwardRefComponent<"div", Expa
73
73
  * <ExpandableBlock.EndIcon/>
74
74
  * </ExpandableBlock.Trigger>
75
75
  */
76
- Trigger: PolymorphicForwardRefComponent<"button", ExpandableBlockTriggerOwnProps>;
76
+ Trigger: PolymorphicForwardRefComponent<"div", ExpandableBlockTriggerOwnProps>;
77
77
  /**
78
78
  * The expanding icon on the left of header
79
79
  */
@@ -90,11 +90,11 @@ export declare const ExpandableBlock: PolymorphicForwardRefComponent<"div", Expa
90
90
  /**
91
91
  * The main text displayed on the block header, regardless of state.
92
92
  */
93
- Title: PolymorphicForwardRefComponent<"div", {}>;
93
+ Title: PolymorphicForwardRefComponent<"button", {}>;
94
94
  /**
95
95
  * Small note displayed below title, regardless of state.
96
96
  */
97
- Caption: PolymorphicForwardRefComponent<NonNullable<keyof JSX.IntrinsicElements>, {}>;
97
+ Caption: PolymorphicForwardRefComponent<"div", {}>;
98
98
  /**
99
99
  * Svg icon displayed at the end of the main text.
100
100
  * Will override status icon if specified. Used inside `Header` subcomponent.
@@ -110,7 +110,10 @@ export declare const ExpandableBlock: PolymorphicForwardRefComponent<"div", Expa
110
110
  fill?: "default" | "informational" | "negative" | "positive" | "warning" | import("../utils/types.js").AnyString | undefined;
111
111
  padded?: boolean | undefined;
112
112
  } & Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & {
113
- as?: "span" | undefined;
113
+ as?: "span" | undefined; /**
114
+ * Status of the block.
115
+ * When set, a colored status icon is shown at the end of the main text.
116
+ */
114
117
  }, "ref">>;
115
118
  /**
116
119
  * Content shown in the block's body when fully expanded.
@@ -4,8 +4,9 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import cx from 'classnames';
6
6
  import * as React from 'react';
7
- import { StatusIconMap, SvgChevronRight, Box, useSafeContext, polymorphic, mergeEventHandlers, ButtonBase, } from '../utils/index.js';
7
+ import { StatusIconMap, SvgChevronRight, Box, useSafeContext, polymorphic, mergeEventHandlers, ButtonBase, useId, } from '../utils/index.js';
8
8
  import { Icon } from '../Icon/Icon.js';
9
+ import { LinkBox } from '../LinkAction/LinkAction.js';
9
10
  const ExpandableBlockContext = React.createContext(undefined);
10
11
  ExpandableBlockContext.displayName = 'ExpandableBlockContext';
11
12
  const ExpandableBlockComponent = React.forwardRef((props, forwardedRef) => {
@@ -20,6 +21,7 @@ const ExpandableBlockWrapper = React.forwardRef((props, forwardedRef) => {
20
21
  const { children, className, onToggle, style, isExpanded, status, size = 'default', styleType = 'default', disabled = false, ...rest } = props;
21
22
  const [expandedState, setExpanded] = React.useState(isExpanded ?? false);
22
23
  const expanded = isExpanded ?? expandedState;
24
+ const [descriptionId, setDescriptionId] = React.useState(undefined);
23
25
  return (React.createElement(ExpandableBlockContext.Provider, { value: {
24
26
  status,
25
27
  isExpanded: expanded,
@@ -29,20 +31,16 @@ const ExpandableBlockWrapper = React.forwardRef((props, forwardedRef) => {
29
31
  disabled,
30
32
  setExpanded,
31
33
  children,
34
+ descriptionId,
35
+ setDescriptionId,
32
36
  } },
33
37
  React.createElement(Box, { className: cx('iui-expandable-block', className), "data-iui-expanded": expanded, "data-iui-size": size, "data-iui-variant": styleType !== 'default' ? styleType : undefined, style: style, ref: forwardedRef, ...rest }, children)));
34
38
  });
35
39
  ExpandableBlockWrapper.displayName = 'ExpandableBlock.Wrapper';
36
40
  const ExpandableBlockTrigger = React.forwardRef((props, forwardedRef) => {
37
- const { className, children, label, caption, onClick: onClickProp, expandIcon, endIcon, ...rest } = props;
38
- const { isExpanded, setExpanded, disabled, onToggle, status } = useSafeContext(ExpandableBlockContext);
39
- return (React.createElement(ButtonBase, { className: cx('iui-expandable-header', className), "aria-expanded": isExpanded, "aria-disabled": disabled, onClick: mergeEventHandlers(onClickProp, () => {
40
- if (disabled) {
41
- return;
42
- }
43
- setExpanded(!isExpanded);
44
- onToggle?.(!isExpanded);
45
- }), ref: forwardedRef, ...rest }, children ?? (React.createElement(React.Fragment, null,
41
+ const { className, children, label, caption, expandIcon, endIcon, ...rest } = props;
42
+ const { disabled, status } = useSafeContext(ExpandableBlockContext);
43
+ return (React.createElement(LinkBox, { className: cx('iui-expandable-header', className), "data-iui-disabled": disabled ? 'true' : undefined, ref: forwardedRef, ...rest }, children ?? (React.createElement(React.Fragment, null,
46
44
  expandIcon ?? React.createElement(ExpandableBlock.ExpandIcon, null),
47
45
  React.createElement(ExpandableBlock.LabelArea, null,
48
46
  React.createElement(ExpandableBlock.Title, null, label),
@@ -64,13 +62,28 @@ ExpandableBlockLabelArea.displayName = 'ExpandableBlock.LabelArea';
64
62
  // ----------------------------------------------------------------------------
65
63
  // ExpandableBlock.Title component
66
64
  const ExpandableBlockTitle = React.forwardRef((props, forwardedRef) => {
67
- const { className, children, ...rest } = props;
68
- return (React.createElement(Box, { className: cx('iui-expandable-block-title', className), ref: forwardedRef, ...rest }, children));
65
+ const { className, children, onClick: onClickProp, ...rest } = props;
66
+ const { isExpanded, setExpanded, disabled, onToggle, descriptionId } = useSafeContext(ExpandableBlockContext);
67
+ return (React.createElement(ButtonBase, { className: cx('iui-expandable-block-title', 'iui-link-action', className), "aria-expanded": isExpanded, "aria-disabled": disabled, onClick: mergeEventHandlers(onClickProp, () => {
68
+ if (disabled) {
69
+ return;
70
+ }
71
+ setExpanded(!isExpanded);
72
+ onToggle?.(!isExpanded);
73
+ }), ref: forwardedRef, "aria-describedby": descriptionId, ...rest }, children));
69
74
  });
70
75
  ExpandableBlockTitle.displayName = 'ExpandableBlock.Title';
71
76
  // ----------------------------------------------------------------------------
72
77
  // ExpandableBlock.Caption component
73
- const ExpandableBlockCaption = polymorphic('iui-expandable-block-caption');
78
+ const ExpandableBlockCaption = React.forwardRef((props, forwardedRef) => {
79
+ const fallbackId = useId();
80
+ const { setDescriptionId } = useSafeContext(ExpandableBlockContext);
81
+ React.useEffect(() => {
82
+ setDescriptionId(props.id || fallbackId);
83
+ return () => setDescriptionId(undefined);
84
+ }, [props.id, fallbackId, setDescriptionId]);
85
+ return (React.createElement(Box, { ref: forwardedRef, id: fallbackId, ...props, className: cx('iui-expandable-block-caption', props?.className) }));
86
+ });
74
87
  ExpandableBlockCaption.displayName = 'ExpandableBlock.Caption';
75
88
  // ----------------------------------------------------------------------------
76
89
  // ExpandableBlock.EndIcon component
@@ -10,15 +10,17 @@ type InputGridOwnProps = {
10
10
  labelPlacement?: 'default' | 'inline';
11
11
  };
12
12
  /**
13
- * InputGrid component is used to display inputs (input, textarea, select)
13
+ * InputGrid component is used to display form fields (input, textarea, select)
14
14
  * with label and/or status message
15
15
  *
16
- * @usage
16
+ * Form fields are automatically associated with the label and status message for
17
+ * better accessibility.
17
18
  *
19
+ * @example
18
20
  * <InputGrid>
19
- * <Label htmlFor='input-1'>This is label</Label>
20
- * <Input id='input-1'/>
21
- * <StatusMessage>This is message</StatusMessage>
21
+ * <Label>This is a label</Label>
22
+ * <Input />
23
+ * <StatusMessage>This is a message</StatusMessage>
22
24
  * </InputGrid>
23
25
  */
24
26
  export declare const InputGrid: PolymorphicForwardRefComponent<"div", InputGridOwnProps>;
@@ -3,24 +3,193 @@
3
3
  * See LICENSE.md in the project root for license terms and full copyright notice.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
- import { Box } from '../utils/index.js';
6
+ import { Box, InputWithIcon, cloneElementWithRef, useId, } from '../utils/index.js';
7
7
  import cx from 'classnames';
8
+ import { Label } from '../Label/Label.js';
9
+ import { Input } from '../Input/Input.js';
10
+ import { Textarea } from '../Textarea/Textarea.js';
11
+ import { StatusMessage } from '../StatusMessage/StatusMessage.js';
12
+ import { InputWithDecorations } from '../InputWithDecorations/InputWithDecorations.js';
13
+ import { ComboBox } from '../ComboBox/ComboBox.js';
14
+ import { Select } from '../Select/Select.js';
8
15
  //-------------------------------------------------------------------------------
9
16
  /**
10
- * InputGrid component is used to display inputs (input, textarea, select)
17
+ * InputGrid component is used to display form fields (input, textarea, select)
11
18
  * with label and/or status message
12
19
  *
13
- * @usage
20
+ * Form fields are automatically associated with the label and status message for
21
+ * better accessibility.
14
22
  *
23
+ * @example
15
24
  * <InputGrid>
16
- * <Label htmlFor='input-1'>This is label</Label>
17
- * <Input id='input-1'/>
18
- * <StatusMessage>This is message</StatusMessage>
25
+ * <Label>This is a label</Label>
26
+ * <Input />
27
+ * <StatusMessage>This is a message</StatusMessage>
19
28
  * </InputGrid>
20
29
  */
21
30
  export const InputGrid = React.forwardRef((props, ref) => {
22
- const { children, className, labelPlacement = undefined, ...rest } = props;
31
+ const { children: childrenProp, className, labelPlacement = undefined, ...rest } = props;
32
+ const children = useChildrenWithIds(childrenProp);
23
33
  return (React.createElement(Box, { className: cx('iui-input-grid', className), "data-iui-label-placement": labelPlacement, ref: ref, ...rest }, children));
24
34
  });
25
35
  //-------------------------------------------------------------------------------
36
+ /**
37
+ * Ensures that label, input and message are properly associated
38
+ * with each other, for accessibility purposes.
39
+ *
40
+ * - `Select` will be associated with label using `aria-labelledby`
41
+ * - Other inputs will be associated with label using `htmlFor`
42
+ * - Message will be associated with input/select using `aria-describedby`
43
+ */
44
+ const useChildrenWithIds = (children) => {
45
+ const { labelId, inputId, messageId } = useSetup(children);
46
+ return React.useMemo(() => React.Children.map(children, (child) => {
47
+ if (!React.isValidElement(child)) {
48
+ return child;
49
+ }
50
+ if (child.type === Label || child.type === 'label') {
51
+ return cloneElementWithRef(child, (child) => ({
52
+ ...child.props,
53
+ htmlFor: child.props.htmlFor || inputId,
54
+ id: child.props.id || labelId,
55
+ }));
56
+ }
57
+ if (child.type === StatusMessage) {
58
+ return cloneElementWithRef(child, (child) => ({
59
+ ...child.props,
60
+ id: child.props.id || messageId,
61
+ }));
62
+ }
63
+ if (isInput(child) ||
64
+ child.type === InputWithDecorations ||
65
+ child.type === InputWithIcon) {
66
+ return handleCloningInputs(child, {
67
+ labelId,
68
+ inputId,
69
+ messageId,
70
+ });
71
+ }
72
+ return child;
73
+ }), [children, inputId, labelId, messageId]);
74
+ };
75
+ //-------------------------------------------------------------------------------
76
+ /**
77
+ * Setup/prerequisite for `useChildrenWithIds` to gather information from children.
78
+ *
79
+ * @returns the following ids (prefers id from props, otherwise generates one)
80
+ * - `labelId`
81
+ * - `inputId`
82
+ * - `messageId`
83
+ */
84
+ const useSetup = (children) => {
85
+ const idPrefix = useId();
86
+ let labelId;
87
+ let inputId;
88
+ let messageId;
89
+ let hasLabel = false;
90
+ let hasSelect = false;
91
+ const findInputId = (child) => {
92
+ if (!React.isValidElement(child)) {
93
+ return;
94
+ }
95
+ // ComboBox input id is passed through `inputProps`
96
+ if (child.type === ComboBox) {
97
+ return child.props.inputProps?.id || `${idPrefix}--input`;
98
+ }
99
+ // Select input id would be passed through `triggerProps`, but we don't even
100
+ // need it because, unlike other inputs, it gets labelled using `aria-labelledby`
101
+ else if (child.type !== Select) {
102
+ return child.props.id || `${idPrefix}--input`;
103
+ }
104
+ };
105
+ React.Children.forEach(children, (child) => {
106
+ if (!React.isValidElement(child)) {
107
+ return;
108
+ }
109
+ if (child.type === Label || child.type === 'label') {
110
+ hasLabel = true;
111
+ labelId || (labelId = child.props.id || `${idPrefix}--label`);
112
+ }
113
+ if (child.type === StatusMessage) {
114
+ messageId || (messageId = child.props.id || `${idPrefix}--message`);
115
+ }
116
+ if (child.type === InputWithDecorations || child.type === InputWithIcon) {
117
+ React.Children.forEach(child.props.children, (child) => {
118
+ if (isInput(child)) {
119
+ inputId || (inputId = findInputId(child));
120
+ }
121
+ });
122
+ }
123
+ else if (isInput(child)) {
124
+ inputId || (inputId = findInputId(child));
125
+ }
126
+ if (child.type === Select) {
127
+ hasSelect = true;
128
+ }
129
+ });
130
+ return {
131
+ labelId: hasSelect ? labelId : undefined,
132
+ inputId: hasLabel && !hasSelect ? inputId : undefined,
133
+ messageId,
134
+ };
135
+ };
136
+ //-------------------------------------------------------------------------------
137
+ /**
138
+ * Handles regular inputs, plus `InputWithDecorations`, `InputWithIcon`, `ComboBox` and `Select`.
139
+ */
140
+ const handleCloningInputs = (child, { labelId, inputId, messageId, }) => {
141
+ const inputProps = (props = {}) => {
142
+ // Concatenate aria-describedby from props and from StatusMessage
143
+ const ariaDescribedBy = [props['aria-describedby'], messageId]
144
+ .filter(Boolean)
145
+ .join(' ');
146
+ return {
147
+ ...props,
148
+ ...(child.type !== Select && { id: props.id || inputId }),
149
+ 'aria-describedby': ariaDescribedBy?.trim() || undefined,
150
+ };
151
+ };
152
+ const cloneInput = (child) => {
153
+ if (child.type === ComboBox) {
154
+ return cloneElementWithRef(child, (child) => ({
155
+ inputProps: inputProps(child.props.inputProps),
156
+ }));
157
+ }
158
+ if (child.type === Select) {
159
+ return cloneElementWithRef(child, (child) => ({
160
+ triggerProps: {
161
+ ...{ 'aria-labelledby': labelId },
162
+ ...inputProps(child.props.triggerProps),
163
+ },
164
+ }));
165
+ }
166
+ return cloneElementWithRef(child, (child) => inputProps(child.props));
167
+ };
168
+ if (child.type === InputWithDecorations || child.type === InputWithIcon) {
169
+ return cloneElementWithRef(child, (child) => ({
170
+ children: React.Children.map(child.props.children, (child) => {
171
+ if (React.isValidElement(child) && isInput(child)) {
172
+ return cloneInput(child);
173
+ }
174
+ return child;
175
+ }),
176
+ }));
177
+ }
178
+ return cloneInput(child);
179
+ };
180
+ //-------------------------------------------------------------------------------
181
+ /** @returns true if `child` is a form element that can be associated with a label using id */
182
+ const isInput = (child) => {
183
+ return (React.isValidElement(child) &&
184
+ (child.type === 'input' ||
185
+ child.type === 'textarea' ||
186
+ child.type === 'select' ||
187
+ child.type === Input ||
188
+ child.type === Textarea ||
189
+ child.type === InputWithDecorations.Input ||
190
+ child.type === Select || // contains Select.triggerProps
191
+ child.type === ComboBox) // contains ComboBox.inputProps
192
+ );
193
+ };
194
+ //-------------------------------------------------------------------------------
26
195
  export default InputGrid;
@@ -32,7 +32,7 @@ type InputGroupProps = {
32
32
  /**
33
33
  * Custom icon. If group has status, default status icon is used instead.
34
34
  */
35
- svgIcon?: JSX.Element;
35
+ svgIcon?: React.ComponentPropsWithoutRef<typeof StatusMessage>['startIcon'];
36
36
  /**
37
37
  * Child inputs inside group.
38
38
  */