@itwin/itwinui-react 3.9.1 → 3.10.1

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 (65) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/cjs/core/Breadcrumbs/Breadcrumbs.js +2 -3
  3. package/cjs/core/Buttons/Button.js +1 -1
  4. package/cjs/core/Buttons/IconButton.js +1 -1
  5. package/cjs/core/Buttons/IdeasButton.js +6 -2
  6. package/cjs/core/ComboBox/ComboBox.js +1 -1
  7. package/cjs/core/DropdownMenu/DropdownMenu.js +36 -13
  8. package/cjs/core/Input/Input.js +1 -1
  9. package/cjs/core/LabeledSelect/LabeledSelect.d.ts +26 -4
  10. package/cjs/core/Menu/Menu.js +9 -0
  11. package/cjs/core/Menu/MenuItem.d.ts +12 -0
  12. package/cjs/core/Menu/MenuItem.js +114 -66
  13. package/cjs/core/NotificationMarker/NotificationMarker.d.ts +7 -6
  14. package/cjs/core/Popover/Popover.d.ts +32 -9
  15. package/cjs/core/Popover/Popover.js +68 -17
  16. package/cjs/core/Select/Select.js +2 -3
  17. package/cjs/core/SideNavigation/SideNavigation.d.ts +1 -0
  18. package/cjs/core/SideNavigation/SideNavigation.js +20 -21
  19. package/cjs/core/SideNavigation/SidenavButton.js +5 -1
  20. package/cjs/core/Table/TablePaginator.js +1 -3
  21. package/cjs/core/Table/columns/selectionColumn.js +10 -1
  22. package/cjs/core/Table/hooks/useSubRowSelection.js +1 -1
  23. package/cjs/core/ThemeProvider/ThemeProvider.js +53 -17
  24. package/cjs/core/TimePicker/TimePicker.js +12 -12
  25. package/cjs/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
  26. package/cjs/core/ToggleSwitch/ToggleSwitch.js +2 -2
  27. package/cjs/utils/components/Portal.d.ts +6 -2
  28. package/cjs/utils/components/Portal.js +11 -14
  29. package/cjs/utils/providers/ScopeProvider.d.ts +26 -0
  30. package/cjs/utils/providers/ScopeProvider.js +77 -0
  31. package/cjs/utils/providers/index.d.ts +1 -0
  32. package/cjs/utils/providers/index.js +1 -0
  33. package/esm/core/Breadcrumbs/Breadcrumbs.js +2 -3
  34. package/esm/core/Buttons/Button.js +1 -1
  35. package/esm/core/Buttons/IconButton.js +1 -1
  36. package/esm/core/Buttons/IdeasButton.js +3 -2
  37. package/esm/core/ComboBox/ComboBox.js +1 -1
  38. package/esm/core/DropdownMenu/DropdownMenu.js +36 -13
  39. package/esm/core/Input/Input.js +1 -1
  40. package/esm/core/LabeledSelect/LabeledSelect.d.ts +26 -4
  41. package/esm/core/Menu/Menu.js +9 -0
  42. package/esm/core/Menu/MenuItem.d.ts +12 -0
  43. package/esm/core/Menu/MenuItem.js +114 -66
  44. package/esm/core/NotificationMarker/NotificationMarker.d.ts +7 -6
  45. package/esm/core/Popover/Popover.d.ts +32 -9
  46. package/esm/core/Popover/Popover.js +71 -20
  47. package/esm/core/Select/Select.js +2 -3
  48. package/esm/core/SideNavigation/SideNavigation.d.ts +1 -0
  49. package/esm/core/SideNavigation/SideNavigation.js +20 -21
  50. package/esm/core/SideNavigation/SidenavButton.js +5 -1
  51. package/esm/core/Table/TablePaginator.js +2 -4
  52. package/esm/core/Table/columns/selectionColumn.js +10 -1
  53. package/esm/core/Table/hooks/useSubRowSelection.js +1 -1
  54. package/esm/core/ThemeProvider/ThemeProvider.js +54 -18
  55. package/esm/core/TimePicker/TimePicker.js +12 -12
  56. package/esm/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
  57. package/esm/core/ToggleSwitch/ToggleSwitch.js +2 -2
  58. package/esm/utils/components/Portal.d.ts +6 -2
  59. package/esm/utils/components/Portal.js +9 -8
  60. package/esm/utils/providers/ScopeProvider.d.ts +26 -0
  61. package/esm/utils/providers/ScopeProvider.js +48 -0
  62. package/esm/utils/providers/index.d.ts +1 -0
  63. package/esm/utils/providers/index.js +1 -0
  64. package/package.json +2 -1
  65. package/styles.css +1 -1
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import type { Placement } from '@floating-ui/react';
2
+ import type { Placement, UseListNavigationProps, ReferenceType, UseFloatingOptions, UseHoverProps, UseClickProps, UseFocusProps, UseDismissProps } from '@floating-ui/react';
3
3
  import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
4
4
  import type { PortalProps } from '../../utils/components/Portal.js';
5
5
  type PopoverOptions = {
@@ -59,16 +59,39 @@ type PopoverInternalProps = {
59
59
  layoutShift?: boolean;
60
60
  };
61
61
  /**
62
- * By default, the popover will only open on click.
63
- * `hover` and `focus` can be manually specified as triggers.
62
+ * By default, only the click and dismiss interactions/triggers are enabled.
63
+ * Explicitly pass `false` to disable the defaults.
64
+ *
65
+ * Pass a boolean to enable/disable any of the supported interactions.
66
+ * Alternatively, pass an object to override the default props that the Popover sets for an interaction/trigger.
67
+ *
68
+ * When additional parameters are _required_ for an interaction/trigger, an object must be passed to enable it.
69
+ * Booleans will not be allowed in this case.
70
+ *
71
+ * @example
72
+ * <Popover
73
+ * interactions={{
74
+ * click: false,
75
+ * focus: true,
76
+ * hover: { move: false },
77
+ * listNavigation: { … },
78
+ * }}
79
+ * // …
80
+ * >…</Popover>
64
81
  */
65
- trigger?: Partial<Record<'hover' | 'click' | 'focus', boolean>>;
82
+ interactions?: {
83
+ click?: boolean | UseClickProps;
84
+ dismiss?: boolean | UseDismissProps;
85
+ hover?: boolean | UseHoverProps<ReferenceType>;
86
+ focus?: boolean | UseFocusProps;
87
+ listNavigation?: UseListNavigationProps;
88
+ };
66
89
  role?: 'dialog' | 'menu' | 'listbox';
67
90
  /**
68
91
  * Whether the popover should match the width of the trigger.
69
92
  */
70
93
  matchWidth?: boolean;
71
- };
94
+ } & Omit<UseFloatingOptions, 'middleware' | 'placement'>;
72
95
  export declare const usePopover: (options: PopoverOptions & PopoverInternalProps) => {
73
96
  placement: Placement;
74
97
  strategy: import("@floating-ui/utils").Strategy;
@@ -83,11 +106,11 @@ export declare const usePopover: (options: PopoverOptions & PopoverInternalProps
83
106
  floating: React.MutableRefObject<HTMLElement | null>;
84
107
  setReference: (node: import("@floating-ui/react-dom").ReferenceType | null) => void;
85
108
  setFloating: (node: HTMLElement | null) => void;
86
- } & import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
109
+ } & import("@floating-ui/react").ExtendedRefs<ReferenceType>;
87
110
  elements: {
88
111
  reference: import("@floating-ui/react-dom").ReferenceType | null;
89
112
  floating: HTMLElement | null;
90
- } & import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
113
+ } & import("@floating-ui/react").ExtendedElements<ReferenceType>;
91
114
  context: {
92
115
  x: number;
93
116
  y: number;
@@ -103,8 +126,8 @@ export declare const usePopover: (options: PopoverOptions & PopoverInternalProps
103
126
  dataRef: React.MutableRefObject<import("@floating-ui/react").ContextData>;
104
127
  nodeId: string | undefined;
105
128
  floatingId: string;
106
- refs: import("@floating-ui/react").ExtendedRefs<import("@floating-ui/react").ReferenceType>;
107
- elements: import("@floating-ui/react").ExtendedElements<import("@floating-ui/react").ReferenceType>;
129
+ refs: import("@floating-ui/react").ExtendedRefs<ReferenceType>;
130
+ elements: import("@floating-ui/react").ExtendedElements<ReferenceType>;
108
131
  };
109
132
  getFloatingProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
110
133
  getReferenceProps: (userProps?: React.HTMLProps<Element> | undefined) => Record<string, unknown>;
@@ -4,21 +4,35 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import cx from 'classnames';
7
- import { useFloating, useClick, useDismiss, useInteractions, size, autoUpdate, offset, flip, shift, autoPlacement, inline, hide, FloatingFocusManager, useHover, useFocus, safePolygon, useRole, FloatingPortal, } from '@floating-ui/react';
8
- import { Box, cloneElementWithRef, useControlledState, useId, useLayoutEffect, useMergedRefs, } from '../../utils/index.js';
9
- import { Portal } from '../../utils/components/Portal.js';
7
+ import { useFloating, useClick, useDismiss, useInteractions, size, autoUpdate, offset, flip, shift, autoPlacement, inline, hide, FloatingFocusManager, useHover, useFocus, safePolygon, useRole, FloatingPortal, useFloatingTree, useListNavigation, } from '@floating-ui/react';
8
+ import { Box, ShadowRoot, cloneElementWithRef, useControlledState, useId, useLayoutEffect, useMergedRefs, } from '../../utils/index.js';
9
+ import { usePortalTo } from '../../utils/components/Portal.js';
10
10
  import { ThemeProvider } from '../ThemeProvider/ThemeProvider.js';
11
11
  // ----------------------------------------------------------------------------
12
12
  export const usePopover = (options) => {
13
- const { placement = 'bottom-start', visible, onVisibleChange, closeOnOutsideClick, autoUpdateOptions, matchWidth, trigger = { click: true, hover: false, focus: false }, role, } = options;
14
- const middleware = { flip: true, shift: true, ...options.middleware };
13
+ const { placement = 'bottom-start', visible, onVisibleChange, closeOnOutsideClick, autoUpdateOptions, matchWidth, interactions: interactionsProp, role, ...rest } = options;
14
+ const mergedInteractions = {
15
+ ...{
16
+ click: true,
17
+ dismiss: true,
18
+ hover: false,
19
+ focus: false,
20
+ listNavigation: undefined,
21
+ },
22
+ ...interactionsProp,
23
+ };
24
+ const tree = useFloatingTree();
25
+ const middleware = React.useMemo(() => ({ flip: true, shift: true, ...options.middleware }), [options.middleware]);
15
26
  const [open, onOpenChange] = useControlledState(false, visible, onVisibleChange);
16
27
  const floating = useFloating({
17
28
  placement,
18
29
  open,
19
30
  onOpenChange,
20
- whileElementsMounted: (...args) => autoUpdate(...args, autoUpdateOptions),
21
- middleware: [
31
+ whileElementsMounted: React.useMemo(() =>
32
+ // autoUpdate is expensive and should only be called when the popover is open
33
+ open ? (...args) => autoUpdate(...args, autoUpdateOptions) : undefined, [autoUpdateOptions, open]),
34
+ ...rest,
35
+ middleware: React.useMemo(() => [
22
36
  middleware.offset !== undefined && offset(middleware.offset),
23
37
  middleware.flip && flip(),
24
38
  middleware.shift && shift(),
@@ -31,18 +45,38 @@ export const usePopover = (options) => {
31
45
  middleware.autoPlacement && autoPlacement(),
32
46
  middleware.inline && inline(),
33
47
  middleware.hide && hide(),
34
- ].filter(Boolean),
48
+ ].filter(Boolean), [matchWidth, middleware]),
35
49
  });
36
50
  const interactions = useInteractions([
37
- useClick(floating.context, { enabled: !!trigger.click }),
38
- useDismiss(floating.context, { outsidePress: closeOnOutsideClick }),
51
+ useClick(floating.context, {
52
+ enabled: !!mergedInteractions.click,
53
+ ...mergedInteractions.click,
54
+ }),
55
+ useDismiss(floating.context, {
56
+ enabled: !!mergedInteractions.dismiss,
57
+ outsidePress: closeOnOutsideClick,
58
+ bubbles: tree != null,
59
+ ...mergedInteractions.dismiss,
60
+ }),
39
61
  useHover(floating.context, {
40
- enabled: !!trigger.hover,
62
+ enabled: !!mergedInteractions.hover,
41
63
  delay: 100,
42
- handleClose: safePolygon({ buffer: 1 }),
64
+ handleClose: safePolygon({
65
+ buffer: 1,
66
+ blockPointerEvents: true,
67
+ }),
68
+ move: false,
69
+ ...mergedInteractions.hover,
70
+ }),
71
+ useFocus(floating.context, {
72
+ enabled: !!mergedInteractions.focus,
73
+ ...mergedInteractions.focus,
43
74
  }),
44
- useFocus(floating.context, { enabled: !!trigger.focus }),
45
75
  useRole(floating.context, { role: 'dialog', enabled: !!role }),
76
+ useListNavigation(floating.context, {
77
+ enabled: !!mergedInteractions.listNavigation,
78
+ ...mergedInteractions.listNavigation,
79
+ }),
46
80
  ]);
47
81
  const [referenceWidth, setReferenceWidth] = React.useState();
48
82
  const getFloatingProps = React.useCallback((userProps) => interactions.getFloatingProps({
@@ -114,11 +148,28 @@ export const Popover = React.forwardRef((props, forwardedRef) => {
114
148
  ...popover.getReferenceProps(children.props),
115
149
  ref: popover.refs.setReference,
116
150
  })),
117
- popover.open ? (React.createElement(Portal, { portal: portal },
118
- React.createElement(FloatingPortal, null,
119
- React.createElement(ThemeProvider, { portalContainer: popoverElement },
120
- React.createElement(FloatingFocusManager, { context: popover.context, modal: false, initialFocus: popover.refs.floating },
121
- React.createElement(Box, { className: cx({ 'iui-popover-surface': applyBackground }, className), "aria-labelledby": !hasAriaLabel
122
- ? popover.refs.domReference.current?.id
123
- : undefined, ...popover.getFloatingProps(rest), ref: popoverRef }, content)))))) : null));
151
+ popover.open ? (React.createElement(PopoverPortal, { portal: portal },
152
+ React.createElement(ThemeProvider, { portalContainer: popoverElement },
153
+ React.createElement(DisplayContents, null),
154
+ React.createElement(FloatingFocusManager, { context: popover.context, modal: false, initialFocus: popover.refs.floating },
155
+ React.createElement(Box, { className: cx({ 'iui-popover-surface': applyBackground }, className), "aria-labelledby": !hasAriaLabel
156
+ ? popover.refs.domReference.current?.id
157
+ : undefined, ...popover.getFloatingProps(rest), ref: popoverRef }, content))))) : null));
158
+ });
159
+ // ----------------------------------------------------------------------------
160
+ const PopoverPortal = ({ children, portal = true, }) => {
161
+ const portalTo = usePortalTo(portal);
162
+ return (React.createElement(FloatingPortal, { root: portalTo },
163
+ React.createElement(DisplayContents, null),
164
+ children));
165
+ };
166
+ // ----------------------------------------------------------------------------
167
+ /** Applies `display: contents` to the parent div. */
168
+ const DisplayContents = React.memo(() => {
169
+ return (React.createElement(ShadowRoot, { css: `
170
+ :host {
171
+ display: contents;
172
+ }
173
+ ` },
174
+ React.createElement("slot", null)));
124
175
  });
@@ -199,9 +199,8 @@ const CustomSelect = React.forwardRef((props, forwardedRef) => {
199
199
  });
200
200
  return (React.createElement(React.Fragment, null,
201
201
  React.createElement(InputWithIcon, { ...rest, ref: useMergedRefs(popover.refs.setPositionReference, forwardedRef) },
202
- React.createElement(SelectButton, { ...popover.getReferenceProps(), tabIndex: 0, role: 'combobox', size: size, status: status, "aria-disabled": disabled ? 'true' : undefined, "aria-autocomplete": 'none', "aria-expanded": isOpen, "aria-haspopup": 'listbox', "aria-controls": `${uid}-menu`, styleType: styleType, ...triggerProps, ref: useMergedRefs(selectRef, triggerProps?.ref, popover.refs.setReference), className: cx({
202
+ React.createElement(SelectButton, { ...popover.getReferenceProps(), tabIndex: 0, role: 'combobox', size: size, status: status, "aria-disabled": disabled ? 'true' : undefined, "data-iui-disabled": disabled ? 'true' : undefined, "aria-autocomplete": 'none', "aria-expanded": isOpen, "aria-haspopup": 'listbox', "aria-controls": `${uid}-menu`, styleType: styleType, ...triggerProps, ref: useMergedRefs(selectRef, triggerProps?.ref, popover.refs.setReference), className: cx({
203
203
  'iui-placeholder': (!selectedItems || selectedItems.length === 0) && !!placeholder,
204
- 'iui-disabled': disabled,
205
204
  }, triggerProps?.className) },
206
205
  (!selectedItems || selectedItems.length === 0) && (React.createElement(Box, { as: 'span', className: 'iui-content' }, placeholder)),
207
206
  isMultipleEnabled(selectedItems, multiple) ? (React.createElement(MultipleSelectButton, { selectedItems: selectedItems, selectedItemsRenderer: selectedItemRenderer, tagRenderer: tagRenderer })) : (React.createElement(SingleSelectButton, { selectedItem: selectedItems, selectedItemRenderer: selectedItemRenderer }))),
@@ -229,7 +228,7 @@ const isSingleOnChange = (onChange, multiple) => {
229
228
  // ----------------------------------------------------------------------------
230
229
  const SelectButton = React.forwardRef((props, forwardedRef) => {
231
230
  const { size, status, styleType = 'default', ...rest } = props;
232
- return (React.createElement(Box, { "data-iui-size": size, "data-iui-status": status, "data-iui-variant": styleType !== 'default' ? styleType : undefined, ...rest, ref: forwardedRef, className: cx('iui-select-button', props.className) }));
231
+ return (React.createElement(Box, { "data-iui-size": size, "data-iui-status": status, "data-iui-variant": styleType !== 'default' ? styleType : undefined, ...rest, ref: forwardedRef, className: cx('iui-select-button', 'iui-field', props.className) }));
233
232
  });
234
233
  // ----------------------------------------------------------------------------
235
234
  const SelectEndIcon = React.forwardRef((props, forwardedRef) => {
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import type { PolymorphicForwardRefComponent } from '../../utils/index.js';
3
+ export declare const SidenavExpandedContext: React.Context<boolean | undefined>;
3
4
  type SideNavigationProps = {
4
5
  /**
5
6
  * Buttons shown in the top portion of sidenav.
@@ -4,9 +4,10 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import cx from 'classnames';
7
- import { WithCSSTransition, SvgChevronRight, Box } from '../../utils/index.js';
7
+ import { WithCSSTransition, SvgChevronRight, Box, useControlledState, } from '../../utils/index.js';
8
8
  import { IconButton } from '../Buttons/IconButton.js';
9
- import { Tooltip } from '../Tooltip/Tooltip.js';
9
+ // ----------------------------------------------------------------------------
10
+ export const SidenavExpandedContext = React.createContext(undefined);
10
11
  /**
11
12
  * Left side navigation menu component.
12
13
  * @example
@@ -22,25 +23,23 @@ import { Tooltip } from '../Tooltip/Tooltip.js';
22
23
  * />
23
24
  */
24
25
  export const SideNavigation = React.forwardRef((props, forwardedRef) => {
25
- const { items, secondaryItems, expanderPlacement = 'top', className, isExpanded = false, onExpanderClick, submenu, isSubmenuOpen = false, wrapperProps, contentProps, topProps, bottomProps, ...rest } = props;
26
- const [_isExpanded, _setIsExpanded] = React.useState(isExpanded);
27
- React.useEffect(() => {
28
- _setIsExpanded(isExpanded);
29
- }, [isExpanded]);
30
- const ExpandButton = (React.createElement(IconButton, { label: 'Toggle icon labels', "aria-expanded": _isExpanded, className: 'iui-sidenav-button iui-expand', onClick: React.useCallback(() => {
31
- _setIsExpanded((expanded) => !expanded);
26
+ const { items, secondaryItems, expanderPlacement = 'top', className, isExpanded: isExpandedProp, onExpanderClick, submenu, isSubmenuOpen = false, wrapperProps, contentProps, topProps, bottomProps, ...rest } = props;
27
+ const [isExpanded, setIsExpanded] = useControlledState(false, isExpandedProp);
28
+ const ExpandButton = (React.createElement(IconButton, { label: 'Toggle icon labels', "aria-expanded": isExpanded, className: 'iui-sidenav-button iui-expand', size: 'small', onClick: React.useCallback(() => {
29
+ setIsExpanded((expanded) => !expanded);
32
30
  onExpanderClick?.();
33
- }, [onExpanderClick]) },
31
+ }, [onExpanderClick, setIsExpanded]) },
34
32
  React.createElement(SvgChevronRight, null)));
35
- return (React.createElement(Box, { ...wrapperProps, className: cx('iui-side-navigation-wrapper', wrapperProps?.className), ref: forwardedRef },
36
- React.createElement(Box, { as: 'div', className: cx('iui-side-navigation', {
37
- 'iui-expanded': _isExpanded,
38
- 'iui-collapsed': !_isExpanded,
39
- }, className), ...rest },
40
- expanderPlacement === 'top' && ExpandButton,
41
- React.createElement(Box, { as: 'div', ...contentProps, className: cx('iui-sidenav-content', contentProps?.className) },
42
- React.createElement(Box, { as: 'div', ...topProps, className: cx('iui-top', topProps?.className) }, items.map((sidenavButton, index) => !_isExpanded ? (React.createElement(Tooltip, { content: sidenavButton.props.children, placement: 'right', key: index }, sidenavButton)) : (sidenavButton))),
43
- React.createElement(Box, { as: 'div', ...bottomProps, className: cx('iui-bottom', bottomProps?.className) }, secondaryItems?.map((sidenavButton, index) => !_isExpanded ? (React.createElement(Tooltip, { content: sidenavButton.props.children, placement: 'right', key: index }, sidenavButton)) : (sidenavButton)))),
44
- expanderPlacement === 'bottom' && ExpandButton),
45
- submenu && (React.createElement(WithCSSTransition, { in: isSubmenuOpen, dimension: 'width', timeout: 200, classNames: 'iui' }, submenu))));
33
+ return (React.createElement(SidenavExpandedContext.Provider, { value: isExpanded },
34
+ React.createElement(Box, { ...wrapperProps, className: cx('iui-side-navigation-wrapper', wrapperProps?.className), ref: forwardedRef },
35
+ React.createElement(Box, { as: 'div', className: cx('iui-side-navigation', {
36
+ 'iui-expanded': isExpanded,
37
+ 'iui-collapsed': !isExpanded,
38
+ }, className), ...rest },
39
+ expanderPlacement === 'top' && ExpandButton,
40
+ React.createElement(Box, { as: 'div', ...contentProps, className: cx('iui-sidenav-content', contentProps?.className) },
41
+ React.createElement(Box, { as: 'div', ...topProps, className: cx('iui-top', topProps?.className) }, items),
42
+ React.createElement(Box, { as: 'div', ...bottomProps, className: cx('iui-bottom', bottomProps?.className) }, secondaryItems)),
43
+ expanderPlacement === 'bottom' && ExpandButton),
44
+ submenu && (React.createElement(WithCSSTransition, { in: isSubmenuOpen, dimension: 'width', timeout: 200, classNames: 'iui' }, submenu)))));
46
45
  });
@@ -5,11 +5,15 @@
5
5
  import cx from 'classnames';
6
6
  import * as React from 'react';
7
7
  import { Button } from '../Buttons/Button.js';
8
+ import { Tooltip } from '../Tooltip/Tooltip.js';
9
+ import { SidenavExpandedContext } from './SideNavigation.js';
8
10
  /**
9
11
  * Wrapper around Button to be used as SideNavigation items.
10
12
  * Label is hidden when sidenav is collapsed.
11
13
  */
12
14
  export const SidenavButton = React.forwardRef((props, ref) => {
13
15
  const { className, children, isActive = false, disabled = false, isSubmenuOpen = false, ...rest } = props;
14
- return (React.createElement(Button, { className: cx('iui-sidenav-button', { 'iui-submenu-open': isSubmenuOpen }, className), "data-iui-active": isActive, size: 'large', disabled: disabled, ref: ref, ...rest }, children));
16
+ const isExpanded = React.useContext(SidenavExpandedContext) === true;
17
+ const sidenavButton = (React.createElement(Button, { className: cx('iui-sidenav-button', { 'iui-submenu-open': isSubmenuOpen }, className), "data-iui-active": isActive, size: 'large', disabled: disabled, ref: ref, ...rest }, children));
18
+ return !isExpanded ? (React.createElement(Tooltip, { content: children, placement: 'right', ariaStrategy: 'none' }, sidenavButton)) : (sidenavButton);
15
19
  });
@@ -9,7 +9,7 @@ import { Button } from '../Buttons/Button.js';
9
9
  import { DropdownButton } from '../Buttons/DropdownButton.js';
10
10
  import { ProgressRadial } from '../ProgressIndicators/ProgressRadial.js';
11
11
  import { MenuItem } from '../Menu/MenuItem.js';
12
- import { getBoundedValue, useGlobals, useOverflow, useContainerWidth, SvgChevronLeft, SvgChevronRight, Box, ButtonBase, } from '../../utils/index.js';
12
+ import { getBoundedValue, useGlobals, useOverflow, useContainerWidth, SvgChevronLeft, SvgChevronRight, Box, } from '../../utils/index.js';
13
13
  const defaultLocalization = {
14
14
  pageSizeLabel: (size) => `${size} per page`,
15
15
  rangeLabel: (startIndex, endIndex, totalRows, isLoading) => isLoading
@@ -52,9 +52,7 @@ export const TablePaginator = (props) => {
52
52
  isMounted.current = true;
53
53
  }, [focusedIndex]);
54
54
  const buttonSize = size != 'default' ? 'small' : undefined;
55
- const pageButton = React.useCallback((index, tabIndex = index === focusedIndex ? 0 : -1) => (React.createElement(ButtonBase, { key: index, className: cx('iui-table-paginator-page-button', {
56
- 'iui-table-paginator-page-button-small': buttonSize === 'small',
57
- }), "data-iui-active": index === currentPage, onClick: () => onPageChange(index), "aria-current": index === currentPage, "aria-label": localization.goToPageLabel(index + 1), tabIndex: tabIndex }, index + 1)), [focusedIndex, currentPage, localization, buttonSize, onPageChange]);
55
+ const pageButton = React.useCallback((index, tabIndex = index === focusedIndex ? 0 : -1) => (React.createElement(Button, { key: index, className: 'iui-table-paginator-page-button', styleType: 'borderless', size: buttonSize, "data-iui-active": index === currentPage, onClick: () => onPageChange(index), "aria-current": index === currentPage, "aria-label": localization.goToPageLabel(index + 1), tabIndex: tabIndex }, index + 1)), [focusedIndex, currentPage, localization, buttonSize, onPageChange]);
58
56
  const totalPagesCount = Math.ceil(totalRowsCount / pageSize);
59
57
  const pageList = React.useMemo(() => new Array(totalPagesCount)
60
58
  .fill(null)
@@ -39,7 +39,16 @@ export const SelectionColumn = (props = {}) => {
39
39
  , checked: checked && !disabled, indeterminate: indeterminate, disabled: disabled, onChange: () => toggleAllRowsSelected(!rows.some((row) => row.isSelected)) }));
40
40
  },
41
41
  Cell: ({ row }) => (React.createElement(Checkbox, { ...row.getToggleRowSelectedProps(), style: {}, title: '' // Removes default title that comes from react-table
42
- , disabled: isDisabled?.(row.original), onClick: (e) => e.stopPropagation() })),
42
+ , disabled: isDisabled?.(row.original), onClick: (e) => e.stopPropagation(), onChange: () => {
43
+ if (row.subRows.length > 0) {
44
+ //This code ignores any sub-rows that are not currently available(i.e disabled or filtered out).
45
+ //If all available sub-rows are selected, then it deselects them all, otherwise it selects them all.
46
+ row.toggleRowSelected(!row.subRows.every((subRow) => subRow.isSelected || isDisabled?.(subRow.original)));
47
+ }
48
+ else {
49
+ row.toggleRowSelected();
50
+ }
51
+ } })),
43
52
  cellRenderer: (props) => (React.createElement(DefaultCell, { ...props, isDisabled: (rowData) => !!isDisabled?.(rowData) })),
44
53
  };
45
54
  };
@@ -11,7 +11,7 @@ const useInstance = (instance) => {
11
11
  const selectedFlatRows = [];
12
12
  const setSelectionState = (row, selectedRowIds) => {
13
13
  let isSomeSubRowsSelected = false;
14
- row.subRows.forEach((subRow) => {
14
+ row.initialSubRows.forEach((subRow) => {
15
15
  setSelectionState(subRow, selectedRowIds);
16
16
  if (subRow.isSelected || subRow.isSomeSelected) {
17
17
  isSomeSubRowsSelected = true;
@@ -5,9 +5,13 @@
5
5
  import * as React from 'react';
6
6
  import * as ReactDOM from 'react-dom';
7
7
  import cx from 'classnames';
8
- import { useMediaQuery, useMergedRefs, Box, useLayoutEffect, useControlledState, useLatestRef, importCss, isUnitTest, HydrationProvider, } from '../../utils/index.js';
8
+ import { useMediaQuery, useMergedRefs, Box, useLayoutEffect, useLatestRef, importCss, isUnitTest, HydrationProvider, useHydration, ScopeProvider, portalContainerAtom, useScopedAtom, useScopedSetAtom, } from '../../utils/index.js';
9
9
  import { ThemeContext } from './ThemeContext.js';
10
10
  import { ToastProvider, Toaster } from '../Toast/Toaster.js';
11
+ import { atom } from 'jotai';
12
+ // ----------------------------------------------------------------------------
13
+ const ownerDocumentAtom = atom(undefined);
14
+ // ----------------------------------------------------------------------------
11
15
  /**
12
16
  * This component provides global state and applies theme to the entire tree
13
17
  * that it is wrapping around.
@@ -45,26 +49,21 @@ export const ThemeProvider = React.forwardRef((props, forwardedRef) => {
45
49
  themeOptions.applyBackground ?? (themeOptions.applyBackground = !parent.theme);
46
50
  // default inherit highContrast option from parent if also inheriting base theme
47
51
  themeOptions.highContrast ?? (themeOptions.highContrast = themeProp === 'inherit' ? parent.highContrast : undefined);
48
- /**
49
- * We will portal our portal container into `portalContainer` prop (if specified),
50
- * or inherit `portalContainer` from context (if also inheriting theme).
51
- */
52
- const portaledPortalContainer = portalContainerProp ||
53
- (themeProp === 'inherit' ? parent.context?.portalContainer : undefined);
54
- const [portalContainer, setPortalContainer] = useControlledState(null, portaledPortalContainer);
55
- const contextValue = React.useMemo(() => ({ theme, themeOptions, portalContainer }),
52
+ const [portalContainerFromParent] = useScopedAtom(portalContainerAtom);
53
+ const contextValue = React.useMemo(() => ({ theme, themeOptions }),
56
54
  // we do include all dependencies below, but we want to stringify the objects as they could be different on each render
57
55
  // eslint-disable-next-line react-hooks/exhaustive-deps
58
- [theme, JSON.stringify(themeOptions), portalContainer]);
59
- return (React.createElement(HydrationProvider, null,
60
- React.createElement(ThemeContext.Provider, { value: contextValue },
61
- includeCss && rootElement ? (React.createElement(FallbackStyles, { root: rootElement })) : null,
62
- React.createElement(Root, { theme: theme, themeOptions: themeOptions, ref: useMergedRefs(forwardedRef, setRootElement), ...rest },
56
+ [theme, JSON.stringify(themeOptions)]);
57
+ return (React.createElement(ScopeProvider, null,
58
+ React.createElement(HydrationProvider, null,
59
+ React.createElement(ThemeContext.Provider, { value: contextValue },
63
60
  React.createElement(ToastProvider, null,
64
- children,
65
- portaledPortalContainer ? (ReactDOM.createPortal(React.createElement(Toaster, null), portaledPortalContainer)) : (React.createElement("div", { ref: setPortalContainer, style: { display: 'contents' } },
66
- React.createElement(Toaster, null))))))));
61
+ includeCss && rootElement ? (React.createElement(FallbackStyles, { root: rootElement })) : null,
62
+ React.createElement(Root, { theme: theme, themeOptions: themeOptions, ref: useMergedRefs(forwardedRef, setRootElement), ...rest },
63
+ children,
64
+ React.createElement(PortalContainer, { portalContainerProp: portalContainerProp, portalContainerFromParent: portalContainerFromParent, isInheritingTheme: themeProp === 'inherit' })))))));
67
65
  });
66
+ ThemeProvider.displayName = 'ThemeProvider';
68
67
  // ----------------------------------------------------------------------------
69
68
  const Root = React.forwardRef((props, forwardedRef) => {
70
69
  const { theme, children, themeOptions, className, ...rest } = props;
@@ -73,7 +72,10 @@ const Root = React.forwardRef((props, forwardedRef) => {
73
72
  const shouldApplyDark = theme === 'dark' || (theme === 'os' && prefersDark);
74
73
  const shouldApplyHC = themeOptions?.highContrast ?? prefersHighContrast;
75
74
  const shouldApplyBackground = themeOptions?.applyBackground;
76
- return (React.createElement(Box, { className: cx('iui-root', { 'iui-root-background': shouldApplyBackground }, className), "data-iui-theme": shouldApplyDark ? 'dark' : 'light', "data-iui-contrast": shouldApplyHC ? 'high' : 'default', ref: forwardedRef, ...rest }, children));
75
+ const setOwnerDocument = useScopedSetAtom(ownerDocumentAtom);
76
+ return (React.createElement(Box, { className: cx('iui-root', { 'iui-root-background': shouldApplyBackground }, className), "data-iui-theme": shouldApplyDark ? 'dark' : 'light', "data-iui-contrast": shouldApplyHC ? 'high' : 'default', ref: useMergedRefs(forwardedRef, (el) => {
77
+ setOwnerDocument(el?.ownerDocument);
78
+ }), ...rest }, children));
77
79
  });
78
80
  // ----------------------------------------------------------------------------
79
81
  /**
@@ -121,6 +123,40 @@ const useParentThemeAndContext = (rootElement) => {
121
123
  };
122
124
  };
123
125
  // ----------------------------------------------------------------------------
126
+ /**
127
+ * Creates a new portal container if necessary, or reuses the parent portal container.
128
+ *
129
+ * Updates `portalContainerAtom` with the correct portal container.
130
+ */
131
+ const PortalContainer = React.memo(({ portalContainerProp, portalContainerFromParent, isInheritingTheme, }) => {
132
+ const [ownerDocument] = useScopedAtom(ownerDocumentAtom);
133
+ const [portalContainer, setPortalContainer] = useScopedAtom(portalContainerAtom);
134
+ // bail if not hydrated, because portals don't work on server
135
+ const isHydrated = useHydration() === 'hydrated';
136
+ if (!isHydrated) {
137
+ return null;
138
+ }
139
+ // Create a new portal container only if necessary:
140
+ // - not inheriting theme
141
+ // - no parent portal container to portal into
142
+ // - parent portal container is in a different window (#2006)
143
+ if (!portalContainerProp && // bail if portalContainerProp is set, because it takes precedence
144
+ (!isInheritingTheme ||
145
+ !portalContainerFromParent ||
146
+ portalContainerFromParent.ownerDocument !== ownerDocument)) {
147
+ return (React.createElement("div", { style: { display: 'contents' }, ref: setPortalContainer },
148
+ React.createElement(Toaster, null)));
149
+ }
150
+ const portalTarget = portalContainerProp || portalContainerFromParent;
151
+ // Synchronize atom with the correct portal container if necessary.
152
+ if (portalTarget && portalTarget !== portalContainer) {
153
+ setPortalContainer(portalTarget);
154
+ }
155
+ return portalTarget
156
+ ? ReactDOM.createPortal(React.createElement(Toaster, null), portalTarget)
157
+ : null;
158
+ });
159
+ // ----------------------------------------------------------------------------
124
160
  /**
125
161
  * When `@itwin/itwinui-react/styles.css` is not imported, we will attempt to
126
162
  * dynamically import it (if possible) and fallback to loading it from a CDN.
@@ -207,16 +207,6 @@ export const TimePicker = React.forwardRef((props, forwardedRef) => {
207
207
  const TimePickerColumn = (props) => {
208
208
  const { data, onFocusChange, onSelectChange, isSameFocused, isSameSelected, setFocus = false, valueRenderer, precision = 'minutes', className = 'iui-time', } = props;
209
209
  const needFocus = React.useRef(setFocus);
210
- // Used to focus row only when changed (keyboard navigation)
211
- // e.g. without this on every rerender it would be focused
212
- React.useEffect(() => {
213
- if (needFocus.current) {
214
- needFocus.current = false;
215
- }
216
- });
217
- const scrollIntoView = (ref, isSame) => {
218
- isSame && ref?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
219
- };
220
210
  const handleTimeKeyDown = (event, maxValue, onFocus, onSelect, currentValue) => {
221
211
  if (event.altKey) {
222
212
  return;
@@ -254,8 +244,18 @@ const TimePickerColumn = (props) => {
254
244
  }, className: cx({
255
245
  'iui-selected': isSameSelected(value),
256
246
  }), key: index, tabIndex: isSameFocus ? 0 : undefined, ref: (ref) => {
257
- scrollIntoView(ref, isSameFocus);
258
- needFocus.current && isSameFocus && ref?.focus();
247
+ if (!ref || !isSameFocus) {
248
+ return;
249
+ }
250
+ // Move focus/scroll in the next task, after the DOM has stabilized.
251
+ // This gives it priority over other conflicting logic (e.g. from floating-ui/Popover).
252
+ setTimeout(() => {
253
+ ref.scrollIntoView({ block: 'nearest', inline: 'nearest' });
254
+ if (needFocus.current) {
255
+ ref.focus();
256
+ needFocus.current = false;
257
+ }
258
+ });
259
259
  }, onClick: () => {
260
260
  onSelectChange(value);
261
261
  } }, valueRenderer(value, precision)));
@@ -5,6 +5,10 @@ type ToggleSwitchProps = {
5
5
  * Label for the toggle switch.
6
6
  */
7
7
  label?: React.ReactNode;
8
+ /**
9
+ * Passes properties for ToggleSwitch label.
10
+ */
11
+ labelProps?: React.ComponentProps<'span'>;
8
12
  /**
9
13
  * Position of the label.
10
14
  * @default 'right'
@@ -24,7 +24,7 @@ import { Box, SvgCheckmark } from '../../utils/index.js';
24
24
  * <ToggleSwitch label='With icon toggle' icon={<svg viewBox='0 0 16 16'><path d='M1 1v14h14V1H1zm13 1.7v10.6L8.7 8 14 2.7zM8 7.3L2.7 2h10.6L8 7.3zm-.7.7L2 13.3V2.7L7.3 8zm.7.7l5.3 5.3H2.7L8 8.7z' /></svg>} />
25
25
  */
26
26
  export const ToggleSwitch = React.forwardRef((props, ref) => {
27
- const { disabled = false, labelPosition = 'right', label, className, style, size = 'default', icon: iconProp, ...rest } = props;
27
+ const { disabled = false, labelPosition = 'right', label, className, style, size = 'default', labelProps = {}, icon: iconProp, ...rest } = props;
28
28
  // Disallow custom icon for small size, but keep the default checkmark when prop is not passed.
29
29
  const shouldShowIcon = iconProp === undefined || (iconProp !== null && size !== 'small');
30
30
  return (React.createElement(Box, { as: label ? 'label' : 'div', className: cx('iui-toggle-switch-wrapper', {
@@ -34,5 +34,5 @@ export const ToggleSwitch = React.forwardRef((props, ref) => {
34
34
  }, className), "data-iui-size": size, style: style },
35
35
  React.createElement(Box, { as: 'input', className: 'iui-toggle-switch', type: 'checkbox', role: 'switch', disabled: disabled, ref: ref, ...rest }),
36
36
  shouldShowIcon && (React.createElement(Box, { as: 'span', className: 'iui-toggle-switch-icon', "aria-hidden": true }, iconProp || React.createElement(SvgCheckmark, null))),
37
- label && (React.createElement(Box, { as: 'span', className: 'iui-toggle-switch-label' }, label))));
37
+ label && (React.createElement(Box, { as: 'span', ...labelProps, className: cx('iui-toggle-switch-label', labelProps?.className) }, label))));
38
38
  });
@@ -1,9 +1,12 @@
1
1
  import * as React from 'react';
2
+ export declare const portalContainerAtom: import("jotai").PrimitiveAtom<HTMLElement | undefined> & {
3
+ init: HTMLElement | undefined;
4
+ };
2
5
  export type PortalProps = {
3
6
  /**
4
7
  * Where should the element be portaled to?
5
8
  *
6
- * If true, it will portal into nearest ThemeContext.portalContainer.
9
+ * If true, it will portal into nearest ThemeProvider's portalContainer.
7
10
  *
8
11
  * If false, it will not be portaled.
9
12
  *
@@ -20,7 +23,7 @@ export type PortalProps = {
20
23
  /**
21
24
  * Helper component that portals children according to the following conditions:
22
25
  * - renders null on server
23
- * - if `portal` is set to true, renders into nearest ThemeContext.portalContainer
26
+ * - if `portal` is set to true, renders into nearest ThemeProvider's portalContainer
24
27
  * - if `portal` is set to false, renders as-is without portal
25
28
  * - otherwise renders into `portal.to` (can be an element or a function)
26
29
  * - If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
@@ -29,3 +32,4 @@ export type PortalProps = {
29
32
  * @private
30
33
  */
31
34
  export declare const Portal: (props: React.PropsWithChildren<PortalProps>) => React.ReactNode;
35
+ export declare const usePortalTo: (portal: NonNullable<PortalProps['portal']>) => HTMLElement | null | undefined;
@@ -4,14 +4,16 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import * as ReactDOM from 'react-dom';
7
- import { ThemeContext } from '../../core/ThemeProvider/ThemeContext.js';
8
- import { getDocument } from '../functions/dom.js';
9
7
  import { useIsClient } from '../hooks/useIsClient.js';
8
+ import { atom } from 'jotai';
9
+ import { useScopedAtom } from '../providers/ScopeProvider.js';
10
+ // ----------------------------------------------------------------------------
11
+ export const portalContainerAtom = atom(undefined);
10
12
  // ----------------------------------------------------------------------------
11
13
  /**
12
14
  * Helper component that portals children according to the following conditions:
13
15
  * - renders null on server
14
- * - if `portal` is set to true, renders into nearest ThemeContext.portalContainer
16
+ * - if `portal` is set to true, renders into nearest ThemeProvider's portalContainer
15
17
  * - if `portal` is set to false, renders as-is without portal
16
18
  * - otherwise renders into `portal.to` (can be an element or a function)
17
19
  * - If `to`/`to()` === `null`/`undefined`, the default behavior will be used (i.e. as if `portal` is not passed).
@@ -29,12 +31,11 @@ export const Portal = (props) => {
29
31
  return portalTo ? ReactDOM.createPortal(children, portalTo) : children;
30
32
  };
31
33
  // ----------------------------------------------------------------------------
32
- const usePortalTo = (portal) => {
33
- const themeInfo = React.useContext(ThemeContext);
34
- const defaultPortalTo = themeInfo?.portalContainer ?? getDocument()?.body;
34
+ export const usePortalTo = (portal) => {
35
+ const [portalContainer] = useScopedAtom(portalContainerAtom);
35
36
  if (typeof portal === 'boolean') {
36
- return portal ? defaultPortalTo : null;
37
+ return portal ? portalContainer : null;
37
38
  }
38
39
  const portalTo = typeof portal.to === 'function' ? portal.to() : portal.to;
39
- return portalTo ?? defaultPortalTo;
40
+ return portalTo ?? portalContainer;
40
41
  };
@@ -0,0 +1,26 @@
1
+ import * as React from 'react';
2
+ import type { Atom, WritableAtom } from 'jotai';
3
+ /**
4
+ * Provider that creates a fresh, isolated jotai store for its children.
5
+ *
6
+ * Should be used with `useScopedAtom` and/or `useScopedSetAtom`.
7
+ *
8
+ * @private
9
+ */
10
+ export declare const ScopeProvider: ({ children }: {
11
+ children: React.ReactNode;
12
+ }) => React.JSX.Element;
13
+ /**
14
+ * Wrapper over `useAtom` that uses the store from the nearest `ScopeProvider`.
15
+ *
16
+ * If the atom is not set in the current store, it will recursively look in the parent store(s).
17
+ * This is only useful for initial values. Future updates to the parent will not be reflected.
18
+ *
19
+ * @private
20
+ */
21
+ export declare const useScopedAtom: <T>(atom: Atom<T>) => readonly [Awaited<T>, (...args: unknown[]) => unknown];
22
+ /**
23
+ * Wrapper over `useSetAtom` that uses the store from the nearest `ScopeProvider`.
24
+ * @private
25
+ */
26
+ export declare const useScopedSetAtom: <T>(atom: WritableAtom<T, unknown[], unknown>) => (...args: unknown[]) => unknown;