@itwin/itwinui-react 3.3.3 → 3.4.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 (55) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/cjs/core/Carousel/CarouselSlider.js +1 -1
  3. package/cjs/core/ComboBox/ComboBox.js +1 -1
  4. package/cjs/core/DatePicker/DatePicker.d.ts +8 -0
  5. package/cjs/core/DatePicker/DatePicker.js +9 -4
  6. package/cjs/core/Dialog/DialogMain.js +5 -4
  7. package/cjs/core/FileUpload/FileUploadTemplate.d.ts +2 -1
  8. package/cjs/core/FileUpload/FileUploadTemplate.js +2 -1
  9. package/cjs/core/LinkAction/LinkAction.d.ts +3 -2
  10. package/cjs/core/LinkAction/LinkAction.js +33 -1
  11. package/cjs/core/Modal/Modal.d.ts +12 -0
  12. package/cjs/core/Modal/Modal.js +4 -4
  13. package/cjs/core/Popover/Popover.js +1 -1
  14. package/cjs/core/ProgressIndicators/ProgressLinear.d.ts +15 -7
  15. package/cjs/core/ProgressIndicators/ProgressLinear.js +14 -6
  16. package/cjs/core/ProgressIndicators/ProgressRadial.d.ts +3 -2
  17. package/cjs/core/ProgressIndicators/ProgressRadial.js +3 -1
  18. package/cjs/core/Table/Table.js +23 -10
  19. package/cjs/core/Tabs/Tabs.js +17 -16
  20. package/cjs/core/Tag/Tag.d.ts +29 -7
  21. package/cjs/core/Tag/Tag.js +12 -5
  22. package/cjs/core/ThemeProvider/ThemeProvider.js +2 -2
  23. package/cjs/core/utils/components/VirtualScroll.js +7 -7
  24. package/cjs/core/utils/hooks/useIsomorphicLayoutEffect.d.ts +4 -1
  25. package/cjs/core/utils/hooks/useIsomorphicLayoutEffect.js +5 -2
  26. package/cjs/core/utils/hooks/useMediaQuery.js +1 -1
  27. package/cjs/core/utils/hooks/useOverflow.js +3 -3
  28. package/esm/core/Carousel/CarouselSlider.js +2 -2
  29. package/esm/core/ComboBox/ComboBox.js +2 -2
  30. package/esm/core/DatePicker/DatePicker.d.ts +8 -0
  31. package/esm/core/DatePicker/DatePicker.js +9 -4
  32. package/esm/core/Dialog/DialogMain.js +6 -5
  33. package/esm/core/FileUpload/FileUploadTemplate.d.ts +2 -1
  34. package/esm/core/FileUpload/FileUploadTemplate.js +2 -1
  35. package/esm/core/LinkAction/LinkAction.d.ts +3 -2
  36. package/esm/core/LinkAction/LinkAction.js +7 -1
  37. package/esm/core/Modal/Modal.d.ts +12 -0
  38. package/esm/core/Modal/Modal.js +4 -4
  39. package/esm/core/Popover/Popover.js +2 -2
  40. package/esm/core/ProgressIndicators/ProgressLinear.d.ts +15 -7
  41. package/esm/core/ProgressIndicators/ProgressLinear.js +14 -6
  42. package/esm/core/ProgressIndicators/ProgressRadial.d.ts +3 -2
  43. package/esm/core/ProgressIndicators/ProgressRadial.js +3 -1
  44. package/esm/core/Table/Table.js +24 -11
  45. package/esm/core/Tabs/Tabs.js +18 -17
  46. package/esm/core/Tag/Tag.d.ts +29 -7
  47. package/esm/core/Tag/Tag.js +13 -6
  48. package/esm/core/ThemeProvider/ThemeProvider.js +3 -3
  49. package/esm/core/utils/components/VirtualScroll.js +8 -8
  50. package/esm/core/utils/hooks/useIsomorphicLayoutEffect.d.ts +4 -1
  51. package/esm/core/utils/hooks/useIsomorphicLayoutEffect.js +4 -1
  52. package/esm/core/utils/hooks/useMediaQuery.js +2 -2
  53. package/esm/core/utils/hooks/useOverflow.js +4 -4
  54. package/package.json +2 -2
  55. package/styles.css +9 -9
@@ -4,7 +4,7 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import cx from 'classnames';
6
6
  import * as React from 'react';
7
- import { useSafeContext, Box, polymorphic, useIsClient, useIsomorphicLayoutEffect, useMergedRefs, useContainerWidth, ButtonBase, mergeEventHandlers, useControlledState, useId, useLatestRef, } from '../utils/index.js';
7
+ import { useSafeContext, Box, polymorphic, useIsClient, useLayoutEffect, useMergedRefs, useContainerWidth, ButtonBase, mergeEventHandlers, useControlledState, useId, useLatestRef, } from '../utils/index.js';
8
8
  import { Icon } from '../Icon/Icon.js';
9
9
  const TabsWrapper = React.forwardRef((props, ref) => {
10
10
  const { className, children, orientation = 'horizontal', type = 'default', focusActivationMode = 'auto', color = 'blue', defaultValue, value: activeValueProp, onValueChange, ...rest } = props;
@@ -53,27 +53,27 @@ const Tab = React.forwardRef((props, forwardedRef) => {
53
53
  const isActive = activeValue === value;
54
54
  const isActiveRef = useLatestRef(isActive);
55
55
  // Scroll to active tab only on initial render
56
- useIsomorphicLayoutEffect(() => {
56
+ useLayoutEffect(() => {
57
57
  if (isActiveRef.current) {
58
58
  tabRef.current?.parentElement?.scrollTo({
59
59
  [orientation === 'horizontal' ? 'left' : 'top']: tabRef.current?.[orientation === 'horizontal' ? 'offsetLeft' : 'offsetTop'] - 4,
60
60
  behavior: 'instant', // not using 'smooth' to reduce layout shift on page load
61
61
  });
62
62
  }
63
- }, []);
64
- const updateStripe = () => {
65
- const currentTabRect = tabRef.current?.getBoundingClientRect();
66
- setStripeProperties({
67
- '--iui-tabs-stripe-size': orientation === 'horizontal'
68
- ? `${currentTabRect?.width}px`
69
- : `${currentTabRect?.height}px`,
70
- '--iui-tabs-stripe-position': orientation === 'horizontal'
71
- ? `${tabRef.current?.offsetLeft}px`
72
- : `${tabRef.current?.offsetTop}px`,
73
- });
74
- };
63
+ }, [isActiveRef, orientation]);
75
64
  // CSS custom properties to place the active stripe
76
- useIsomorphicLayoutEffect(() => {
65
+ useLayoutEffect(() => {
66
+ const updateStripe = () => {
67
+ const currentTabRect = tabRef.current?.getBoundingClientRect();
68
+ setStripeProperties({
69
+ '--iui-tabs-stripe-size': orientation === 'horizontal'
70
+ ? `${currentTabRect?.width}px`
71
+ : `${currentTabRect?.height}px`,
72
+ '--iui-tabs-stripe-position': orientation === 'horizontal'
73
+ ? `${tabRef.current?.offsetLeft}px`
74
+ : `${tabRef.current?.offsetTop}px`,
75
+ });
76
+ };
77
77
  if (type !== 'default' && isActive) {
78
78
  updateStripe();
79
79
  }
@@ -81,7 +81,8 @@ const Tab = React.forwardRef((props, forwardedRef) => {
81
81
  type,
82
82
  orientation,
83
83
  isActive,
84
- tabsWidth, // to fix visual artifact on initial render
84
+ tabsWidth,
85
+ setStripeProperties,
85
86
  ]);
86
87
  const onKeyDown = (event) => {
87
88
  if (event.altKey) {
@@ -157,7 +158,7 @@ TabLabel.displayName = 'Tabs.TabLabel';
157
158
  const TabDescription = React.forwardRef((props, ref) => {
158
159
  const { className, children, ...rest } = props;
159
160
  const { hasSublabel, setHasSublabel } = useSafeContext(TabsContext);
160
- useIsomorphicLayoutEffect(() => {
161
+ useLayoutEffect(() => {
161
162
  if (!hasSublabel) {
162
163
  setHasSublabel(true);
163
164
  }
@@ -2,22 +2,44 @@ import * as React from 'react';
2
2
  import type { PolymorphicForwardRefComponent } from '../utils/index.js';
3
3
  type TagProps = {
4
4
  /**
5
- * Callback function that handles click on close icon.
6
- * Close icon is shown only when this function is passed.
7
- * Use only with 'default' Tag.
5
+ * Text inside the tag.
6
+ */
7
+ children: React.ReactNode;
8
+ /**
9
+ * Callback invoked when the tag is clicked.
10
+ *
11
+ * When this prop is passed, the tag will be rendered as a button.
12
+ */
13
+ onClick?: React.MouseEventHandler;
14
+ /**
15
+ * Callback function that handles click on the remove ("❌") button.
16
+ * If not passed, the remove button will not be shown.
17
+ *
18
+ * If both `onClick` and `onRemove` are passed, then the tag label (rather than the tag itself)
19
+ * will be rendered as a button, to avoid invalid markup (nested buttons).
8
20
  */
9
21
  onRemove?: React.MouseEventHandler;
10
22
  /**
11
- * Text inside the tag.
23
+ * Props for customizing the remove ("❌") button.
12
24
  */
13
- children: React.ReactNode;
25
+ removeButtonProps?: React.ComponentPropsWithRef<'button'>;
26
+ } & ({
14
27
  /**
15
28
  * Variant of tag.
16
29
  * Basic tags don't have an outline.
17
30
  * @default 'default'
18
31
  */
19
- variant?: 'default' | 'basic';
20
- };
32
+ variant?: 'default';
33
+ /**
34
+ * Props for customizing the label.
35
+ *
36
+ * Only relevant for the 'default' Tag.
37
+ */
38
+ labelProps?: React.ComponentPropsWithRef<'span'>;
39
+ } | {
40
+ variant?: 'basic';
41
+ labelProps?: never;
42
+ });
21
43
  /**
22
44
  * Tag for showing categories, filters etc.
23
45
  * @example
@@ -4,8 +4,9 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import cx from 'classnames';
6
6
  import * as React from 'react';
7
- import { SvgCloseSmall, Box } from '../utils/index.js';
7
+ import { SvgCloseSmall, Box, ButtonBase } from '../utils/index.js';
8
8
  import { IconButton } from '../Buttons/IconButton.js';
9
+ import { LinkAction, LinkBox } from '../LinkAction/LinkAction.js';
9
10
  /**
10
11
  * Tag for showing categories, filters etc.
11
12
  * @example
@@ -13,13 +14,19 @@ import { IconButton } from '../Buttons/IconButton.js';
13
14
  * <Tag variant='basic'>Basic tag</Tag>
14
15
  */
15
16
  export const Tag = React.forwardRef((props, forwardedRef) => {
16
- const { className, variant = 'default', children, onRemove, ...rest } = props;
17
- return (React.createElement(Box, { as: 'span', className: cx({
17
+ const { className, variant = 'default', children, onRemove, onClick, labelProps, removeButtonProps, ...rest } = props;
18
+ // If both onClick and onRemove are passed, we want to render the label as a button
19
+ // to avoid invalid markup (nested buttons). LinkAction ensures that clicking anywhere outside
20
+ // the remove button (including padding) will still trigger the main onClick callback.
21
+ const shouldUseLinkAction = !!onClick && !!onRemove;
22
+ return (React.createElement(Box, { as: shouldUseLinkAction ? LinkBox : !!onClick ? ButtonBase : 'span', className: cx({
18
23
  'iui-tag-basic': variant === 'basic',
19
24
  'iui-tag': variant === 'default',
20
- }, className), ref: forwardedRef, ...rest },
21
- variant === 'default' ? (React.createElement(Box, { as: 'span', className: 'iui-tag-label' }, children)) : (children),
22
- onRemove && (React.createElement(IconButton, { styleType: 'borderless', size: 'small', onClick: onRemove, "aria-label": 'Delete tag', className: 'iui-tag-button' },
25
+ }, className),
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ref's type doesn't matter internally
27
+ ref: forwardedRef, onClick: !shouldUseLinkAction ? onClick : undefined, ...rest },
28
+ variant === 'default' ? (React.createElement(Box, { as: (shouldUseLinkAction ? LinkAction : 'span'), onClick: shouldUseLinkAction ? onClick : undefined, ...labelProps, className: cx('iui-tag-label', labelProps?.className) }, children)) : (children),
29
+ onRemove && (React.createElement(IconButton, { styleType: 'borderless', size: 'small', onClick: onRemove, "aria-label": 'Delete tag', ...removeButtonProps, className: cx('iui-tag-button', removeButtonProps?.className) },
23
30
  React.createElement(SvgCloseSmall, { "aria-hidden": true })))));
24
31
  });
25
32
  export default Tag;
@@ -5,7 +5,7 @@
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, useIsomorphicLayoutEffect, useControlledState, useLatestRef, importCss, isUnitTest, } from '../utils/index.js';
8
+ import { useMediaQuery, useMergedRefs, Box, useLayoutEffect, useControlledState, useLatestRef, importCss, isUnitTest, } from '../utils/index.js';
9
9
  import { ThemeContext } from './ThemeContext.js';
10
10
  import { ToastProvider, Toaster } from '../Toast/Toaster.js';
11
11
  /**
@@ -87,7 +87,7 @@ const useParentThemeAndContext = (rootElement) => {
87
87
  const [parentThemeState, setParentTheme] = React.useState(parentContext?.theme);
88
88
  const [parentHighContrastState, setParentHighContrastState] = React.useState(parentContext?.themeOptions?.highContrast);
89
89
  const parentThemeRef = useLatestRef(parentContext?.theme);
90
- useIsomorphicLayoutEffect(() => {
90
+ useLayoutEffect(() => {
91
91
  // bail if we already have theme from context
92
92
  if (parentThemeRef.current) {
93
93
  return;
@@ -126,7 +126,7 @@ const useParentThemeAndContext = (rootElement) => {
126
126
  * dynamically import it (if possible) and fallback to loading it from a CDN.
127
127
  */
128
128
  const FallbackStyles = ({ root }) => {
129
- useIsomorphicLayoutEffect(() => {
129
+ useLayoutEffect(() => {
130
130
  // bail if styles are already loaded
131
131
  if (getComputedStyle(root).getPropertyValue('--_iui-v3-loaded') === 'yes') {
132
132
  return;
@@ -4,7 +4,7 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import * as ReactDOM from 'react-dom';
7
- import { mergeRefs, useResizeObserver, useIsomorphicLayoutEffect, } from '../hooks/index.js';
7
+ import { mergeRefs, useResizeObserver, useLayoutEffect, } from '../hooks/index.js';
8
8
  const unstable_batchedUpdates = ReactDOM.unstable_batchedUpdates ?? ((cb) => void cb());
9
9
  const getScrollableParent = (element, ownerDocument = document) => {
10
10
  if (!element || element === ownerDocument.body) {
@@ -146,17 +146,17 @@ export const useVirtualization = (props) => {
146
146
  const [resizeRef, resizeObserver] = useResizeObserver(onResize);
147
147
  // Find scrollable parent
148
148
  // Needed only on init
149
- useIsomorphicLayoutEffect(() => {
149
+ useLayoutEffect(() => {
150
150
  const scrollableParent = getScrollableParent(parentRef.current, parentRef.current?.ownerDocument);
151
151
  scrollContainer.current = scrollableParent;
152
152
  resizeRef(scrollableParent);
153
153
  }, [resizeRef]);
154
154
  // Stop watching resize, when virtual scroll is unmounted
155
- useIsomorphicLayoutEffect(() => {
155
+ useLayoutEffect(() => {
156
156
  return () => resizeObserver?.disconnect();
157
157
  }, [resizeObserver]);
158
158
  // Get child height when children available
159
- useIsomorphicLayoutEffect(() => updateChildHeight(), [updateChildHeight]);
159
+ useLayoutEffect(() => updateChildHeight(), [updateChildHeight]);
160
160
  const updateVirtualScroll = React.useCallback(() => {
161
161
  const scrollableContainer = getScrollableContainer();
162
162
  if (!scrollableContainer) {
@@ -189,7 +189,7 @@ export const useVirtualization = (props) => {
189
189
  : scrollContainer.current.removeEventListener('scroll', onScrollRef.current);
190
190
  }, []);
191
191
  // Add event listener to the scrollable container.
192
- useIsomorphicLayoutEffect(() => {
192
+ useLayoutEffect(() => {
193
193
  removeScrollListener();
194
194
  onScrollRef.current = onScroll;
195
195
  if (!scrollContainer.current ||
@@ -201,7 +201,7 @@ export const useVirtualization = (props) => {
201
201
  }
202
202
  return removeScrollListener;
203
203
  }, [onScroll, removeScrollListener]);
204
- useIsomorphicLayoutEffect(() => {
204
+ useLayoutEffect(() => {
205
205
  if (!isMounted) {
206
206
  return;
207
207
  }
@@ -256,8 +256,8 @@ export const useVirtualization = (props) => {
256
256
  });
257
257
  }
258
258
  }
259
- }, [scrollToIndex, isMounted]);
260
- useIsomorphicLayoutEffect(() => {
259
+ }, [scrollToIndex, isMounted, itemsLength]);
260
+ useLayoutEffect(() => {
261
261
  if (!scrollContainerHeight) {
262
262
  return;
263
263
  }
@@ -2,6 +2,9 @@ import * as React from 'react';
2
2
  /**
3
3
  * SSR-safe version of `useLayoutEffect` that replaces it with `useEffect` on the server.
4
4
  *
5
+ * Exported as `useLayoutEffect` so that the react hooks linter correctly identifies the necessary dependencies.
6
+ *
5
7
  * @see https://fb.me/react-uselayouteffect-ssr
6
8
  */
7
- export declare const useIsomorphicLayoutEffect: typeof React.useEffect;
9
+ declare const useIsomorphicLayoutEffect: typeof React.useEffect;
10
+ export { useIsomorphicLayoutEffect as useLayoutEffect };
@@ -6,6 +6,9 @@ import * as React from 'react';
6
6
  /**
7
7
  * SSR-safe version of `useLayoutEffect` that replaces it with `useEffect` on the server.
8
8
  *
9
+ * Exported as `useLayoutEffect` so that the react hooks linter correctly identifies the necessary dependencies.
10
+ *
9
11
  * @see https://fb.me/react-uselayouteffect-ssr
10
12
  */
11
- export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
13
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
14
+ export { useIsomorphicLayoutEffect as useLayoutEffect };
@@ -4,10 +4,10 @@
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  import * as React from 'react';
6
6
  import { getWindow } from '../functions/index.js';
7
- import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect.js';
7
+ import { useLayoutEffect } from './useIsomorphicLayoutEffect.js';
8
8
  export const useMediaQuery = (queryString) => {
9
9
  const [matches, setMatches] = React.useState();
10
- useIsomorphicLayoutEffect(() => {
10
+ useLayoutEffect(() => {
11
11
  const mediaQueryList = getWindow()?.matchMedia?.(queryString);
12
12
  const handleChange = ({ matches }) => setMatches(matches);
13
13
  if (mediaQueryList != undefined) {
@@ -5,7 +5,7 @@
5
5
  import * as React from 'react';
6
6
  import { useMergedRefs } from './useMergedRefs.js';
7
7
  import { useResizeObserver } from './useResizeObserver.js';
8
- import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect.js';
8
+ import { useLayoutEffect } from './useIsomorphicLayoutEffect.js';
9
9
  const STARTING_MAX_ITEMS_COUNT = 20;
10
10
  /**
11
11
  * Hook that observes the size of an element and returns the number of items
@@ -38,7 +38,7 @@ export const useOverflow = (items, disabled = false, orientation = 'horizontal')
38
38
  const updateContainerSize = React.useCallback(({ width, height }) => setContainerSize(orientation === 'horizontal' ? width : height), [orientation]);
39
39
  const [resizeRef, observer] = useResizeObserver(updateContainerSize);
40
40
  const resizeObserverRef = React.useRef(observer);
41
- useIsomorphicLayoutEffect(() => {
41
+ useLayoutEffect(() => {
42
42
  if (disabled) {
43
43
  setVisibleCount(items.length);
44
44
  }
@@ -48,7 +48,7 @@ export const useOverflow = (items, disabled = false, orientation = 'horizontal')
48
48
  }
49
49
  }, [containerSize, disabled, items]);
50
50
  const mergedRefs = useMergedRefs(containerRef, resizeRef);
51
- useIsomorphicLayoutEffect(() => {
51
+ useLayoutEffect(() => {
52
52
  if (!containerRef.current || disabled) {
53
53
  resizeObserverRef.current?.disconnect();
54
54
  return;
@@ -77,7 +77,7 @@ export const useOverflow = (items, disabled = false, orientation = 'horizontal')
77
77
  }
78
78
  needsFullRerender.current = false;
79
79
  }, [containerSize, visibleCount, disabled, items.length, orientation]);
80
- useIsomorphicLayoutEffect(() => {
80
+ useLayoutEffect(() => {
81
81
  previousContainerSize.current = containerSize;
82
82
  }, [containerSize]);
83
83
  return [mergedRefs, visibleCount];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itwin/itwinui-react",
3
- "version": "3.3.3",
3
+ "version": "3.4.0",
4
4
  "author": "Bentley Systems",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -86,7 +86,7 @@
86
86
  "tslib": "^2.6.0"
87
87
  },
88
88
  "devDependencies": {
89
- "@itwin/itwinui-css": "^2.2.1",
89
+ "@itwin/itwinui-css": "^2.3.0",
90
90
  "@itwin/itwinui-variables": "3.0.0",
91
91
  "@swc/cli": "^0.1.62",
92
92
  "@swc/core": "^1.3.68",