@itwin/itwinui-react 3.9.0 → 3.10.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.
- package/CHANGELOG.md +30 -0
- package/cjs/core/Breadcrumbs/Breadcrumbs.js +2 -3
- package/cjs/core/Buttons/Button.js +1 -1
- package/cjs/core/Buttons/IconButton.js +1 -1
- package/cjs/core/Buttons/IdeasButton.js +6 -2
- package/cjs/core/ComboBox/ComboBox.js +1 -1
- package/cjs/core/DropdownMenu/DropdownMenu.js +36 -13
- package/cjs/core/Input/Input.js +1 -1
- package/cjs/core/LabeledSelect/LabeledSelect.d.ts +26 -4
- package/cjs/core/Menu/Menu.js +9 -0
- package/cjs/core/Menu/MenuItem.d.ts +12 -0
- package/cjs/core/Menu/MenuItem.js +105 -66
- package/cjs/core/NotificationMarker/NotificationMarker.d.ts +7 -6
- package/cjs/core/Popover/Popover.d.ts +32 -9
- package/cjs/core/Popover/Popover.js +65 -17
- package/cjs/core/Select/Select.js +2 -3
- package/cjs/core/SideNavigation/SideNavigation.js +1 -1
- package/cjs/core/Table/TablePaginator.js +1 -3
- package/cjs/core/Table/columns/selectionColumn.js +10 -1
- package/cjs/core/Table/hooks/useSubRowSelection.js +1 -1
- package/cjs/core/ThemeProvider/ThemeProvider.js +53 -17
- package/cjs/core/TimePicker/TimePicker.js +12 -12
- package/cjs/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
- package/cjs/core/ToggleSwitch/ToggleSwitch.js +2 -2
- package/cjs/core/Tooltip/Tooltip.js +19 -7
- package/cjs/utils/components/Portal.d.ts +6 -2
- package/cjs/utils/components/Portal.js +11 -14
- package/cjs/utils/providers/ScopeProvider.d.ts +26 -0
- package/cjs/utils/providers/ScopeProvider.js +77 -0
- package/cjs/utils/providers/index.d.ts +1 -0
- package/cjs/utils/providers/index.js +1 -0
- package/esm/core/Breadcrumbs/Breadcrumbs.js +2 -3
- package/esm/core/Buttons/Button.js +1 -1
- package/esm/core/Buttons/IconButton.js +1 -1
- package/esm/core/Buttons/IdeasButton.js +3 -2
- package/esm/core/ComboBox/ComboBox.js +1 -1
- package/esm/core/DropdownMenu/DropdownMenu.js +36 -13
- package/esm/core/Input/Input.js +1 -1
- package/esm/core/LabeledSelect/LabeledSelect.d.ts +26 -4
- package/esm/core/Menu/Menu.js +9 -0
- package/esm/core/Menu/MenuItem.d.ts +12 -0
- package/esm/core/Menu/MenuItem.js +105 -66
- package/esm/core/NotificationMarker/NotificationMarker.d.ts +7 -6
- package/esm/core/Popover/Popover.d.ts +32 -9
- package/esm/core/Popover/Popover.js +68 -20
- package/esm/core/Select/Select.js +2 -3
- package/esm/core/SideNavigation/SideNavigation.js +1 -1
- package/esm/core/Table/TablePaginator.js +2 -4
- package/esm/core/Table/columns/selectionColumn.js +10 -1
- package/esm/core/Table/hooks/useSubRowSelection.js +1 -1
- package/esm/core/ThemeProvider/ThemeProvider.js +54 -18
- package/esm/core/TimePicker/TimePicker.js +12 -12
- package/esm/core/ToggleSwitch/ToggleSwitch.d.ts +4 -0
- package/esm/core/ToggleSwitch/ToggleSwitch.js +2 -2
- package/esm/core/Tooltip/Tooltip.js +19 -7
- package/esm/utils/components/Portal.d.ts +6 -2
- package/esm/utils/components/Portal.js +9 -8
- package/esm/utils/providers/ScopeProvider.d.ts +26 -0
- package/esm/utils/providers/ScopeProvider.js +48 -0
- package/esm/utils/providers/index.d.ts +1 -0
- package/esm/utils/providers/index.js +1 -0
- package/package.json +2 -1
- package/styles.css +1 -1
|
@@ -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 {
|
|
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,
|
|
14
|
-
const
|
|
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: (
|
|
21
|
-
|
|
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,35 @@ 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, {
|
|
38
|
-
|
|
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: !!
|
|
62
|
+
enabled: !!mergedInteractions.hover,
|
|
41
63
|
delay: 100,
|
|
42
|
-
handleClose: safePolygon({ buffer: 1 }),
|
|
64
|
+
handleClose: safePolygon({ buffer: 1, requireIntent: false }),
|
|
65
|
+
move: false,
|
|
66
|
+
...mergedInteractions.hover,
|
|
67
|
+
}),
|
|
68
|
+
useFocus(floating.context, {
|
|
69
|
+
enabled: !!mergedInteractions.focus,
|
|
70
|
+
...mergedInteractions.focus,
|
|
43
71
|
}),
|
|
44
|
-
useFocus(floating.context, { enabled: !!trigger.focus }),
|
|
45
72
|
useRole(floating.context, { role: 'dialog', enabled: !!role }),
|
|
73
|
+
useListNavigation(floating.context, {
|
|
74
|
+
enabled: !!mergedInteractions.listNavigation,
|
|
75
|
+
...mergedInteractions.listNavigation,
|
|
76
|
+
}),
|
|
46
77
|
]);
|
|
47
78
|
const [referenceWidth, setReferenceWidth] = React.useState();
|
|
48
79
|
const getFloatingProps = React.useCallback((userProps) => interactions.getFloatingProps({
|
|
@@ -114,11 +145,28 @@ export const Popover = React.forwardRef((props, forwardedRef) => {
|
|
|
114
145
|
...popover.getReferenceProps(children.props),
|
|
115
146
|
ref: popover.refs.setReference,
|
|
116
147
|
})),
|
|
117
|
-
popover.open ? (React.createElement(
|
|
118
|
-
React.createElement(
|
|
119
|
-
React.createElement(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
148
|
+
popover.open ? (React.createElement(PopoverPortal, { portal: portal },
|
|
149
|
+
React.createElement(ThemeProvider, { portalContainer: popoverElement },
|
|
150
|
+
React.createElement(DisplayContents, null),
|
|
151
|
+
React.createElement(FloatingFocusManager, { context: popover.context, modal: false, initialFocus: popover.refs.floating },
|
|
152
|
+
React.createElement(Box, { className: cx({ 'iui-popover-surface': applyBackground }, className), "aria-labelledby": !hasAriaLabel
|
|
153
|
+
? popover.refs.domReference.current?.id
|
|
154
|
+
: undefined, ...popover.getFloatingProps(rest), ref: popoverRef }, content))))) : null));
|
|
155
|
+
});
|
|
156
|
+
// ----------------------------------------------------------------------------
|
|
157
|
+
const PopoverPortal = ({ children, portal = true, }) => {
|
|
158
|
+
const portalTo = usePortalTo(portal);
|
|
159
|
+
return (React.createElement(FloatingPortal, { root: portalTo },
|
|
160
|
+
React.createElement(DisplayContents, null),
|
|
161
|
+
children));
|
|
162
|
+
};
|
|
163
|
+
// ----------------------------------------------------------------------------
|
|
164
|
+
/** Applies `display: contents` to the parent div. */
|
|
165
|
+
const DisplayContents = React.memo(() => {
|
|
166
|
+
return (React.createElement(ShadowRoot, { css: `
|
|
167
|
+
:host {
|
|
168
|
+
display: contents;
|
|
169
|
+
}
|
|
170
|
+
` },
|
|
171
|
+
React.createElement("slot", null)));
|
|
124
172
|
});
|
|
@@ -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) => {
|
|
@@ -27,7 +27,7 @@ export const SideNavigation = React.forwardRef((props, forwardedRef) => {
|
|
|
27
27
|
React.useEffect(() => {
|
|
28
28
|
_setIsExpanded(isExpanded);
|
|
29
29
|
}, [isExpanded]);
|
|
30
|
-
const ExpandButton = (React.createElement(IconButton, { label: 'Toggle icon labels', "aria-expanded": _isExpanded, className: 'iui-sidenav-button iui-expand', onClick: React.useCallback(() => {
|
|
30
|
+
const ExpandButton = (React.createElement(IconButton, { label: 'Toggle icon labels', "aria-expanded": _isExpanded, className: 'iui-sidenav-button iui-expand', size: 'small', onClick: React.useCallback(() => {
|
|
31
31
|
_setIsExpanded((expanded) => !expanded);
|
|
32
32
|
onExpanderClick?.();
|
|
33
33
|
}, [onExpanderClick]) },
|
|
@@ -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,
|
|
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(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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)
|
|
59
|
-
return (React.createElement(
|
|
60
|
-
React.createElement(
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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)));
|
|
@@ -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
|
});
|
|
@@ -14,16 +14,20 @@ const useTooltip = (options = {}) => {
|
|
|
14
14
|
placement,
|
|
15
15
|
open,
|
|
16
16
|
onOpenChange,
|
|
17
|
-
whileElementsMounted: (
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
whileElementsMounted: React.useMemo(() =>
|
|
18
|
+
// autoUpdate is expensive and should only be called when tooltip is open
|
|
19
|
+
open ? (...args) => autoUpdate(...args, autoUpdateOptions) : undefined, [autoUpdateOptions, open]),
|
|
20
|
+
middleware: React.useMemo(() => [
|
|
21
|
+
middleware.offset !== undefined
|
|
22
|
+
? offset(middleware.offset)
|
|
23
|
+
: offset(4),
|
|
20
24
|
middleware.flip && flip(),
|
|
21
25
|
middleware.shift && shift(),
|
|
22
26
|
middleware.size && size(),
|
|
23
27
|
middleware.autoPlacement && autoPlacement(),
|
|
24
28
|
middleware.inline && inline(),
|
|
25
29
|
middleware.hide && hide(),
|
|
26
|
-
].filter(Boolean),
|
|
30
|
+
].filter(Boolean), [middleware]),
|
|
27
31
|
...(reference && { elements: { reference } }),
|
|
28
32
|
});
|
|
29
33
|
const ariaProps = React.useMemo(() => ariaStrategy === 'description'
|
|
@@ -94,7 +98,13 @@ const useTooltip = (options = {}) => {
|
|
|
94
98
|
...props,
|
|
95
99
|
id,
|
|
96
100
|
}), [interactions, props, id, open]);
|
|
97
|
-
return React.useMemo(() => ({
|
|
101
|
+
return React.useMemo(() => ({
|
|
102
|
+
getReferenceProps,
|
|
103
|
+
floatingProps,
|
|
104
|
+
...floating,
|
|
105
|
+
// styles are not relevant when tooltip is not open
|
|
106
|
+
floatingStyles: floating.context.open ? floating.floatingStyles : {},
|
|
107
|
+
}), [getReferenceProps, floatingProps, floating]);
|
|
98
108
|
};
|
|
99
109
|
/**
|
|
100
110
|
* Basic tooltip component to display informative content when an element is hovered or focused.
|
|
@@ -110,11 +120,13 @@ const useTooltip = (options = {}) => {
|
|
|
110
120
|
export const Tooltip = React.forwardRef((props, forwardedRef) => {
|
|
111
121
|
const { content, children, portal = true, className, style, ...rest } = props;
|
|
112
122
|
const tooltip = useTooltip(rest);
|
|
123
|
+
const refs = useMergedRefs(tooltip.refs.setFloating, forwardedRef);
|
|
113
124
|
return (React.createElement(React.Fragment, null,
|
|
114
125
|
cloneElementWithRef(children, (children) => ({
|
|
115
126
|
...tooltip.getReferenceProps(children.props),
|
|
116
127
|
ref: tooltip.refs.setReference,
|
|
117
128
|
})),
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
// Tooltip must always be present in the DOM (even when closed) for ARIA to work
|
|
130
|
+
props.ariaStrategy !== 'none' || tooltip.context.open ? (React.createElement(Portal, { portal: portal },
|
|
131
|
+
React.createElement(Box, { className: cx('iui-tooltip', className), ref: refs, style: { ...tooltip.floatingStyles, ...style }, ...tooltip.floatingProps }, content))) : null));
|
|
120
132
|
});
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
37
|
+
return portal ? portalContainer : null;
|
|
37
38
|
}
|
|
38
39
|
const portalTo = typeof portal.to === 'function' ? portal.to() : portal.to;
|
|
39
|
-
return portalTo ??
|
|
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;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*---------------------------------------------------------------------------------------------
|
|
2
|
+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
|
|
3
|
+
* See LICENSE.md in the project root for license terms and full copyright notice.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { createStore, useAtomValue, useSetAtom } from 'jotai';
|
|
7
|
+
const ScopeContext = React.createContext({
|
|
8
|
+
store: createStore(),
|
|
9
|
+
parentStore: null,
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Provider that creates a fresh, isolated jotai store for its children.
|
|
13
|
+
*
|
|
14
|
+
* Should be used with `useScopedAtom` and/or `useScopedSetAtom`.
|
|
15
|
+
*
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
export const ScopeProvider = ({ children }) => {
|
|
19
|
+
const store = React.useMemo(() => createStore(), []);
|
|
20
|
+
const parentStore = React.useContext(ScopeContext).store;
|
|
21
|
+
return (React.createElement(ScopeContext.Provider, { value: React.useMemo(() => ({ store, parentStore }), [store, parentStore]) }, children));
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Wrapper over `useAtom` that uses the store from the nearest `ScopeProvider`.
|
|
25
|
+
*
|
|
26
|
+
* If the atom is not set in the current store, it will recursively look in the parent store(s).
|
|
27
|
+
* This is only useful for initial values. Future updates to the parent will not be reflected.
|
|
28
|
+
*
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
export const useScopedAtom = (atom) => {
|
|
32
|
+
const { store, parentStore } = React.useContext(ScopeContext);
|
|
33
|
+
const setAtom = useScopedSetAtom(atom);
|
|
34
|
+
const value = useAtomValue(atom, { store });
|
|
35
|
+
const inheritedValue = useAtomValue(atom, { store: parentStore || store });
|
|
36
|
+
if (value == undefined && inheritedValue != undefined) {
|
|
37
|
+
setAtom(inheritedValue);
|
|
38
|
+
}
|
|
39
|
+
return [value, setAtom];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Wrapper over `useSetAtom` that uses the store from the nearest `ScopeProvider`.
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
export const useScopedSetAtom = (atom) => {
|
|
46
|
+
const { store } = React.useContext(ScopeContext);
|
|
47
|
+
return useSetAtom(atom, { store });
|
|
48
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itwin/itwinui-react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"author": "Bentley Systems",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@floating-ui/react": "^0.26.10",
|
|
60
60
|
"@itwin/itwinui-illustrations-react": "^2.1.0",
|
|
61
61
|
"classnames": "^2.3.2",
|
|
62
|
+
"jotai": "^2.8.0",
|
|
62
63
|
"react-table": "^7.8.0",
|
|
63
64
|
"react-transition-group": "^4.4.5",
|
|
64
65
|
"tslib": "^2.6.0"
|