@itwin/itwinui-react 1.37.2 → 1.38.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 (86) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/cjs/core/Breadcrumbs/Breadcrumbs.js +3 -5
  3. package/cjs/core/ColorPicker/ColorSwatch.d.ts +1 -1
  4. package/cjs/core/ComboBox/ComboBox.d.ts +11 -2
  5. package/cjs/core/ComboBox/ComboBox.js +135 -245
  6. package/cjs/core/ComboBox/ComboBoxDropdown.d.ts +8 -0
  7. package/cjs/core/ComboBox/ComboBoxDropdown.js +55 -0
  8. package/cjs/core/ComboBox/ComboBoxEndIcon.d.ts +5 -0
  9. package/cjs/core/ComboBox/ComboBoxEndIcon.js +54 -0
  10. package/cjs/core/ComboBox/ComboBoxInput.d.ts +5 -0
  11. package/cjs/core/ComboBox/ComboBoxInput.js +134 -0
  12. package/cjs/core/ComboBox/ComboBoxInputContainer.d.ts +8 -0
  13. package/cjs/core/ComboBox/ComboBoxInputContainer.js +45 -0
  14. package/cjs/core/ComboBox/ComboBoxMenu.d.ts +3 -0
  15. package/cjs/core/ComboBox/ComboBoxMenu.js +81 -0
  16. package/cjs/core/ComboBox/ComboBoxMenuItem.d.ts +21 -0
  17. package/cjs/core/ComboBox/ComboBoxMenuItem.js +64 -0
  18. package/cjs/core/ComboBox/helpers.d.ts +32 -0
  19. package/cjs/core/ComboBox/helpers.js +50 -0
  20. package/cjs/core/Modal/Modal.d.ts +1 -1
  21. package/cjs/core/Modal/Modal.js +6 -6
  22. package/cjs/core/Modal/ModalButtonBar.d.ts +1 -1
  23. package/cjs/core/Modal/ModalButtonBar.js +2 -2
  24. package/cjs/core/Modal/ModalContent.d.ts +1 -1
  25. package/cjs/core/Modal/ModalContent.js +2 -2
  26. package/cjs/core/RadioTiles/RadioTile.d.ts +1 -1
  27. package/cjs/core/RadioTiles/RadioTile.js +7 -9
  28. package/cjs/core/Select/Select.js +1 -1
  29. package/cjs/core/Slider/Thumb.js +15 -1
  30. package/cjs/core/Slider/Track.js +23 -12
  31. package/cjs/core/Table/Table.js +2 -2
  32. package/cjs/core/Table/filters/FilterToggle.js +3 -2
  33. package/cjs/core/Toast/ToastWrapper.d.ts +7 -5
  34. package/cjs/core/Toast/ToastWrapper.js +8 -4
  35. package/cjs/core/Toast/Toaster.d.ts +3 -0
  36. package/cjs/core/Toast/Toaster.js +30 -5
  37. package/cjs/core/utils/components/Popover.d.ts +1 -1
  38. package/cjs/core/utils/components/VirtualScroll.d.ts +35 -1
  39. package/cjs/core/utils/components/VirtualScroll.js +159 -26
  40. package/cjs/core/utils/hooks/index.d.ts +1 -0
  41. package/cjs/core/utils/hooks/index.js +1 -0
  42. package/cjs/core/utils/hooks/useSafeContext.d.ts +6 -0
  43. package/cjs/core/utils/hooks/useSafeContext.js +23 -0
  44. package/esm/core/Breadcrumbs/Breadcrumbs.js +3 -5
  45. package/esm/core/ColorPicker/ColorSwatch.d.ts +1 -1
  46. package/esm/core/ComboBox/ComboBox.d.ts +11 -2
  47. package/esm/core/ComboBox/ComboBox.js +137 -247
  48. package/esm/core/ComboBox/ComboBoxDropdown.d.ts +8 -0
  49. package/esm/core/ComboBox/ComboBoxDropdown.js +49 -0
  50. package/esm/core/ComboBox/ComboBoxEndIcon.d.ts +5 -0
  51. package/esm/core/ComboBox/ComboBoxEndIcon.js +48 -0
  52. package/esm/core/ComboBox/ComboBoxInput.d.ts +5 -0
  53. package/esm/core/ComboBox/ComboBoxInput.js +128 -0
  54. package/esm/core/ComboBox/ComboBoxInputContainer.d.ts +8 -0
  55. package/esm/core/ComboBox/ComboBoxInputContainer.js +38 -0
  56. package/esm/core/ComboBox/ComboBoxMenu.d.ts +3 -0
  57. package/esm/core/ComboBox/ComboBoxMenu.js +75 -0
  58. package/esm/core/ComboBox/ComboBoxMenuItem.d.ts +21 -0
  59. package/esm/core/ComboBox/ComboBoxMenuItem.js +58 -0
  60. package/esm/core/ComboBox/helpers.d.ts +32 -0
  61. package/esm/core/ComboBox/helpers.js +43 -0
  62. package/esm/core/Modal/Modal.d.ts +1 -1
  63. package/esm/core/Modal/Modal.js +6 -6
  64. package/esm/core/Modal/ModalButtonBar.d.ts +1 -1
  65. package/esm/core/Modal/ModalButtonBar.js +2 -2
  66. package/esm/core/Modal/ModalContent.d.ts +1 -1
  67. package/esm/core/Modal/ModalContent.js +2 -2
  68. package/esm/core/RadioTiles/RadioTile.d.ts +1 -1
  69. package/esm/core/RadioTiles/RadioTile.js +7 -9
  70. package/esm/core/Select/Select.js +1 -1
  71. package/esm/core/Slider/Thumb.js +15 -1
  72. package/esm/core/Slider/Track.js +23 -12
  73. package/esm/core/Table/Table.js +2 -2
  74. package/esm/core/Table/filters/FilterToggle.js +3 -2
  75. package/esm/core/Toast/ToastWrapper.d.ts +7 -5
  76. package/esm/core/Toast/ToastWrapper.js +8 -3
  77. package/esm/core/Toast/Toaster.d.ts +3 -0
  78. package/esm/core/Toast/Toaster.js +30 -5
  79. package/esm/core/utils/components/Popover.d.ts +1 -1
  80. package/esm/core/utils/components/VirtualScroll.d.ts +35 -1
  81. package/esm/core/utils/components/VirtualScroll.js +157 -25
  82. package/esm/core/utils/hooks/index.d.ts +1 -0
  83. package/esm/core/utils/hooks/index.js +1 -0
  84. package/esm/core/utils/hooks/useSafeContext.d.ts +6 -0
  85. package/esm/core/utils/hooks/useSafeContext.js +16 -0
  86. package/package.json +5 -33
@@ -64,7 +64,21 @@ export var Thumb = function (props) {
64
64
  }, [disabled, index, onThumbActivated]);
65
65
  var _a = React.useState(false), hasFocus = _a[0], setHasFocus = _a[1];
66
66
  var _b = React.useState(false), isHovered = _b[0], setIsHovered = _b[1];
67
- var leftPercent = (100.0 * (value - sliderMin)) / (sliderMax - sliderMin);
67
+ var adjustedValue = React.useMemo(function () {
68
+ if (value < sliderMin) {
69
+ return sliderMin;
70
+ }
71
+ if (value > sliderMax) {
72
+ return sliderMax;
73
+ }
74
+ return value;
75
+ }, [sliderMax, sliderMin, value]);
76
+ var leftPercent = React.useMemo(function () {
77
+ if (sliderMax === sliderMin) {
78
+ return 0;
79
+ }
80
+ return (100.0 * (adjustedValue - sliderMin)) / (sliderMax - sliderMin);
81
+ }, [adjustedValue, sliderMax, sliderMin]);
68
82
  var _c = thumbProps || {}, style = _c.style, className = _c.className, rest = __rest(_c, ["style", "className"]);
69
83
  return (React.createElement(Tooltip, __assign({ visible: isActive || hasFocus || isHovered, placement: 'top' }, tooltipProps),
70
84
  React.createElement("div", __assign({}, rest, { "data-index": index, ref: thumbRef, style: __assign(__assign({}, style), { left: "".concat(leftPercent, "%") }), className: cx('iui-slider-thumb', { 'iui-active': isActive }, className), role: 'slider', tabIndex: disabled ? undefined : 0, "aria-valuemin": minVal, "aria-valuenow": value, "aria-valuemax": maxVal, "aria-disabled": disabled, onPointerDown: handlePointerDownOnThumb, onKeyDown: handleOnKeyDown, onFocus: function () { return setHasFocus(true); }, onBlur: function () { return setHasFocus(false); }, onMouseEnter: function () { return setIsHovered(true); }, onMouseLeave: function () { return setIsHovered(false); } }))));
@@ -23,10 +23,18 @@ function shouldDisplaySegment(segmentIndex, mode) {
23
23
  }
24
24
  function generateSegments(values, min, max) {
25
25
  var segments = [];
26
+ var newValues = __spreadArray([], values, true);
27
+ newValues.sort(function (a, b) { return a - b; });
28
+ if (0 === newValues.length ||
29
+ newValues[0] < min ||
30
+ newValues[newValues.length - 1] > max ||
31
+ min === max) {
32
+ return [];
33
+ }
26
34
  var lastValue = min;
27
- for (var i = 0; i < values.length; i++) {
28
- segments.push({ left: lastValue, right: values[i] });
29
- lastValue = values[i];
35
+ for (var i = 0; i < newValues.length; i++) {
36
+ segments.push({ left: lastValue, right: newValues[i] });
37
+ lastValue = newValues[i];
30
38
  }
31
39
  segments.push({ left: lastValue, right: max });
32
40
  return segments;
@@ -37,18 +45,21 @@ function generateSegments(values, min, max) {
37
45
  */
38
46
  export var Track = function (props) {
39
47
  var trackDisplayMode = props.trackDisplayMode, sliderMin = props.sliderMin, sliderMax = props.sliderMax, values = props.values;
40
- var _a = React.useState(__spreadArray([], values, true).sort(function (a, b) { return a - b; })), currentValues = _a[0], setCurrentValues = _a[1];
48
+ var _a = React.useState(function () {
49
+ return generateSegments(values, sliderMin, sliderMax);
50
+ }), segments = _a[0], setSegments = _a[1];
41
51
  React.useEffect(function () {
42
- var newValues = __spreadArray([], values, true);
43
- newValues.sort(function (a, b) { return a - b; });
44
- setCurrentValues(newValues);
45
- }, [values]);
46
- var segments = React.useMemo(function () { return generateSegments(currentValues, sliderMin, sliderMax); }, [currentValues, sliderMin, sliderMax]);
52
+ setSegments(generateSegments(values, sliderMin, sliderMax));
53
+ }, [values, sliderMin, sliderMax]);
47
54
  return (React.createElement(React.Fragment, null, 'none' !== trackDisplayMode &&
48
55
  segments.map(function (segment, index) {
49
- var leftPercent = (100.0 * (segment.left - sliderMin)) / (sliderMax - sliderMin);
50
- var rightPercent = (100.0 * (segment.right - sliderMin)) / (sliderMax - sliderMin);
51
- rightPercent = 100.0 - rightPercent;
56
+ var leftPercent = segment.left >= sliderMin && sliderMax !== sliderMin
57
+ ? (100.0 * (segment.left - sliderMin)) / (sliderMax - sliderMin)
58
+ : 0;
59
+ var rightPercent = segment.right >= sliderMin && sliderMax !== sliderMin
60
+ ? 100.0 -
61
+ (100.0 * (segment.right - sliderMin)) / (sliderMax - sliderMin)
62
+ : 100;
52
63
  return (React.createElement(React.Fragment, { key: index }, shouldDisplaySegment(index, trackDisplayMode) ? (React.createElement("div", { className: 'iui-slider-track', style: { left: "".concat(leftPercent, "%"), right: "".concat(rightPercent, "%") } })) : null));
53
64
  })));
54
65
  };
@@ -180,7 +180,7 @@ export var Table = function (props) {
180
180
  }
181
181
  return result;
182
182
  }, {});
183
- var areFiltersSet = allColumns.some(function (column) { return !!column.filterValue; });
183
+ var areFiltersSet = allColumns.some(function (column) { return column.filterValue != null && column.filterValue !== ''; });
184
184
  var onRowClickHandler = React.useCallback(function (event, row) {
185
185
  var isDisabled = isRowDisabled === null || isRowDisabled === void 0 ? void 0 : isRowDisabled(row.original);
186
186
  if (!isDisabled) {
@@ -296,7 +296,7 @@ export var Table = function (props) {
296
296
  className: 'iui-row',
297
297
  });
298
298
  return (React.createElement("div", __assign({}, headerGroupProps, { key: headerGroupProps.key }), headerGroup.headers.map(function (column, index) {
299
- var columnProps = column.getHeaderProps(__assign(__assign({}, column.getSortByToggleProps()), { className: cx('iui-cell', { 'iui-actionable': column.canSort }, { 'iui-sorted': column.isSorted }, column.columnClassName), style: __assign({}, getCellStyle(column, !!state.isTableResizing)) }));
299
+ var columnProps = column.getHeaderProps(__assign(__assign({}, column.getSortByToggleProps()), { className: cx('iui-cell', { 'iui-actionable': column.canSort }, { 'iui-sorted': column.isSorted }, column.columnClassName), style: __assign(__assign({}, getCellStyle(column, !!state.isTableResizing)), { flexWrap: 'unset' }) }));
300
300
  return (React.createElement("div", __assign({}, columnProps, column.getDragAndDropProps(), { key: columnProps.key, title: undefined, ref: function (el) {
301
301
  if (el && isResizable) {
302
302
  columnRefs.current[column.id] = el;
@@ -47,10 +47,11 @@ export var FilterToggle = function (props) {
47
47
  column.setFilter(undefined);
48
48
  close();
49
49
  }, [close, column]);
50
+ var isColumnFiltered = column.filterValue != null && column.filterValue !== '';
50
51
  return (React.createElement(React.Fragment, null, column.canFilter && column.Filter && (React.createElement(Popover, { content: column.render('Filter', { close: close, setFilter: setFilter, clearFilter: clearFilter }), placement: 'bottom-start', visible: isVisible, onClickOutside: close, appendTo: ownerDocument === null || ownerDocument === void 0 ? void 0 : ownerDocument.body },
51
- React.createElement(IconButton, __assign({ styleType: 'borderless', isActive: isVisible || column.filterValue, className: cx('iui-filter-button', className), onClick: function (e) {
52
+ React.createElement(IconButton, __assign({ styleType: 'borderless', isActive: isVisible || isColumnFiltered, className: cx('iui-filter-button', className), onClick: function (e) {
52
53
  setIsVisible(function (v) { return !v; });
53
54
  // Prevents from triggering sort
54
55
  e.stopPropagation();
55
- } }, rest), column.filterValue ? React.createElement(SvgFilter, null) : React.createElement(SvgFilterHollow, null))))));
56
+ } }, rest), isColumnFiltered ? React.createElement(SvgFilter, null) : React.createElement(SvgFilterHollow, null))))));
56
57
  };
@@ -1,9 +1,11 @@
1
- /// <reference types="react" />
2
1
  import '@itwin/itwinui-css/css/toast-notification.css';
2
+ import React from 'react';
3
3
  import { ToastProps } from './Toast';
4
4
  import { ToasterSettings } from './Toaster';
5
- declare type ToastWrapperProps = {
6
- toasts: ToastProps[];
7
- } & Pick<ToasterSettings, 'placement'>;
8
- export declare const ToastWrapper: (props: ToastWrapperProps) => JSX.Element;
5
+ declare type ToastPlacement = NonNullable<ToasterSettings['placement']>;
6
+ export declare type ToastWrapperHandle = {
7
+ setToasts: (toasts: ToastProps[]) => void;
8
+ setPlacement: (placement: ToastPlacement) => void;
9
+ };
10
+ export declare const ToastWrapper: React.ForwardRefExoticComponent<React.RefAttributes<ToastWrapperHandle>>;
9
11
  export {};
@@ -17,10 +17,15 @@ import '@itwin/itwinui-css/css/toast-notification.css';
17
17
  import React from 'react';
18
18
  import cx from 'classnames';
19
19
  import Toast from './Toast';
20
- export var ToastWrapper = function (props) {
21
- var toasts = props.toasts, _a = props.placement, placement = _a === void 0 ? 'top' : _a;
20
+ export var ToastWrapper = React.forwardRef(function (_, ref) {
21
+ var _a = React.useState([]), toasts = _a[0], setToasts = _a[1];
22
+ var _b = React.useState('top'), placement = _b[0], setPlacement = _b[1];
22
23
  var placementPosition = placement.startsWith('top') ? 'top' : 'bottom';
24
+ React.useImperativeHandle(ref, function () { return ({
25
+ setToasts: setToasts,
26
+ setPlacement: setPlacement,
27
+ }); }, []);
23
28
  return (React.createElement("span", { className: cx("iui-toast-wrapper", "iui-placement-".concat(placement)) }, toasts.map(function (toastProps) {
24
29
  return (React.createElement(Toast, __assign({ key: toastProps.id, placementPosition: placementPosition }, toastProps)));
25
30
  })));
26
- };
31
+ });
@@ -20,6 +20,9 @@ export default class Toaster {
20
20
  private toasts;
21
21
  private lastId;
22
22
  private settings;
23
+ private toastsRef;
24
+ private isInitialized;
25
+ private asyncInit;
23
26
  /**
24
27
  * Set global Toaster settings for toasts order and placement.
25
28
  * Settings will be applied to new toasts on the page.
@@ -29,24 +29,49 @@ import { ToastWrapper } from './ToastWrapper';
29
29
  var TOASTS_CONTAINER_ID = 'iui-toasts-container';
30
30
  var Toaster = /** @class */ (function () {
31
31
  function Toaster() {
32
+ var _this = this;
32
33
  this.toasts = [];
33
34
  this.lastId = 0;
34
35
  this.settings = {
35
36
  order: 'descending',
36
37
  placement: 'top',
37
38
  };
39
+ this.toastsRef = React.createRef();
40
+ this.isInitialized = false;
41
+ // Create container on demand.
42
+ // Cannot do it in constructor, because SSG/SSR apps would fail.
43
+ this.asyncInit = new Promise(function (resolve) {
44
+ if (_this.isInitialized) {
45
+ resolve();
46
+ return;
47
+ }
48
+ var container = getContainer(TOASTS_CONTAINER_ID);
49
+ if (!container) {
50
+ // should never happen
51
+ resolve();
52
+ return;
53
+ }
54
+ _this.isInitialized = true;
55
+ ReactDOM.render(React.createElement(ToastWrapper, { ref: _this.toastsRef }), container);
56
+ resolve();
57
+ });
38
58
  }
39
59
  /**
40
60
  * Set global Toaster settings for toasts order and placement.
41
61
  * Settings will be applied to new toasts on the page.
42
62
  */
43
63
  Toaster.prototype.setSettings = function (newSettings) {
64
+ var _this = this;
44
65
  var _a, _b, _c;
45
66
  (_a = newSettings.placement) !== null && _a !== void 0 ? _a : (newSettings.placement = this.settings.placement);
46
67
  (_b = newSettings.order) !== null && _b !== void 0 ? _b : (newSettings.order = ((_c = newSettings.placement) === null || _c === void 0 ? void 0 : _c.startsWith('bottom'))
47
68
  ? 'ascending'
48
69
  : 'descending');
49
70
  this.settings = newSettings;
71
+ this.asyncInit.then(function () {
72
+ var _a, _b;
73
+ (_a = _this.toastsRef.current) === null || _a === void 0 ? void 0 : _a.setPlacement((_b = _this.settings.placement) !== null && _b !== void 0 ? _b : 'top');
74
+ });
50
75
  };
51
76
  Toaster.prototype.positive = function (content, options) {
52
77
  return this.createToast(content, 'positive', options);
@@ -79,11 +104,11 @@ var Toaster = /** @class */ (function () {
79
104
  this.updateView();
80
105
  };
81
106
  Toaster.prototype.updateView = function () {
82
- var container = getContainer(TOASTS_CONTAINER_ID);
83
- if (!container) {
84
- return;
85
- }
86
- ReactDOM.render(React.createElement(ToastWrapper, { toasts: this.toasts, placement: this.settings.placement }), container);
107
+ var _this = this;
108
+ this.asyncInit.then(function () {
109
+ var _a;
110
+ (_a = _this.toastsRef.current) === null || _a === void 0 ? void 0 : _a.setToasts(_this.toasts);
111
+ });
87
112
  };
88
113
  Toaster.prototype.closeToast = function (toastId) {
89
114
  this.toasts = this.toasts.map(function (toast) {
@@ -43,7 +43,7 @@ export declare const Popover: React.ForwardRefExoticComponent<Pick<{
43
43
  * @see [tippy.js placement prop](https://atomiks.github.io/tippyjs/v6/all-props/#placement).
44
44
  */
45
45
  placement?: import("@popperjs/core").Placement | undefined;
46
- } & Omit<TippyProps, "placement" | "trigger" | "visible">, "disabled" | "placement" | "trigger" | "visible" | "content" | "render" | "animateFill" | "appendTo" | "aria" | "delay" | "duration" | "followCursor" | "getReferenceClientRect" | "hideOnClick" | "ignoreAttributes" | "inlinePositioning" | "interactive" | "interactiveBorder" | "interactiveDebounce" | "moveTransition" | "offset" | "plugins" | "popperOptions" | "showOnCreate" | "sticky" | "touch" | "triggerTarget" | "onAfterUpdate" | "onBeforeUpdate" | "onCreate" | "onDestroy" | "onHidden" | "onHide" | "onMount" | "onShow" | "onShown" | "onTrigger" | "onUntrigger" | "onClickOutside" | "allowHTML" | "animation" | "arrow" | "inertia" | "maxWidth" | "role" | "theme" | "zIndex" | "children" | "className" | "singleton" | "reference"> & React.RefAttributes<unknown>>;
46
+ } & Omit<TippyProps, "placement" | "trigger" | "visible">, "disabled" | "children" | "placement" | "trigger" | "visible" | "content" | "render" | "animateFill" | "appendTo" | "aria" | "delay" | "duration" | "followCursor" | "getReferenceClientRect" | "hideOnClick" | "ignoreAttributes" | "inlinePositioning" | "interactive" | "interactiveBorder" | "interactiveDebounce" | "moveTransition" | "offset" | "plugins" | "popperOptions" | "showOnCreate" | "sticky" | "touch" | "triggerTarget" | "onAfterUpdate" | "onBeforeUpdate" | "onCreate" | "onDestroy" | "onHidden" | "onHide" | "onMount" | "onShow" | "onShown" | "onTrigger" | "onUntrigger" | "onClickOutside" | "allowHTML" | "animation" | "arrow" | "inertia" | "maxWidth" | "role" | "theme" | "zIndex" | "className" | "singleton" | "reference"> & React.RefAttributes<unknown>>;
47
47
  /**
48
48
  * Plugin to hide Popover when either Esc key is pressed,
49
49
  * or when the content inside is not tabbable and Tab key is pressed.
@@ -16,6 +16,10 @@ export declare type VirtualScrollProps = {
16
16
  * @default 10
17
17
  */
18
18
  bufferSize?: number;
19
+ /**
20
+ * Index of the first element on initial render.
21
+ */
22
+ scrollToIndex?: number;
19
23
  } & React.ComponentPropsWithRef<'div'>;
20
24
  /**
21
25
  * `VirtualScroll` component is used to render a huge amount of items in the DOM. It renders only the ones which are visible
@@ -38,5 +42,35 @@ export declare type VirtualScrollProps = {
38
42
  * />
39
43
  * @private
40
44
  */
41
- export declare const VirtualScroll: React.ForwardRefExoticComponent<Pick<VirtualScrollProps, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "itemsLength" | "itemRenderer" | "bufferSize"> & React.RefAttributes<HTMLDivElement>>;
45
+ export declare const VirtualScroll: React.ForwardRefExoticComponent<Pick<VirtualScrollProps, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "itemsLength" | "itemRenderer" | "bufferSize" | "scrollToIndex"> & React.RefAttributes<HTMLDivElement>>;
46
+ /**
47
+ * `useVirtualization` is used for efficiently rendering only the visible rows from a large list.
48
+ * It returns `outerProps` and `innerProps`, which need to be applied on 2 container elements and `visibleChildren` which is a list of virtualized items.
49
+ * @example
50
+ * const itemRenderer = React.useCallback((index: number) => (
51
+ * <li key={index}>
52
+ * This is my item #{index}
53
+ * </li>
54
+ * ), [])
55
+ *
56
+ * const { outerProps, innerProps, visibleChildren } = useVirtualization({itemsLength: 1000, itemRenderer: itemRenderer});
57
+ * return (
58
+ * <div {...outerProps}>
59
+ * <ul {...innerProps}>
60
+ * {visibleChildren}
61
+ * </ul>
62
+ * </div>
63
+ * );
64
+ * @private
65
+ */
66
+ export declare const useVirtualization: (props: VirtualScrollProps) => {
67
+ outerProps: React.HTMLAttributes<HTMLElement>;
68
+ innerProps: {
69
+ readonly style: {
70
+ readonly willChange: "transform";
71
+ };
72
+ readonly ref: (instance: HTMLElement | null) => void;
73
+ };
74
+ visibleChildren: JSX.Element[];
75
+ };
42
76
  export default VirtualScroll;
@@ -25,6 +25,7 @@ var __rest = (this && this.__rest) || function (s, e) {
25
25
  * See LICENSE.md in the project root for license terms and full copyright notice.
26
26
  *--------------------------------------------------------------------------------------------*/
27
27
  import React from 'react';
28
+ import { mergeRefs } from '../hooks';
28
29
  import { useResizeObserver } from '../hooks/useResizeObserver';
29
30
  var getScrollableParent = function (element, ownerDocument) {
30
31
  if (ownerDocument === void 0) { ownerDocument = document; }
@@ -46,6 +47,14 @@ var getElementHeight = function (element) {
46
47
  var _a;
47
48
  return (_a = element === null || element === void 0 ? void 0 : element.getBoundingClientRect().height) !== null && _a !== void 0 ? _a : 0;
48
49
  };
50
+ var getElementHeightWithMargins = function (element) {
51
+ if (!element) {
52
+ return undefined;
53
+ }
54
+ var margin = parseFloat(getElementStyle(element, 'margin-top')) +
55
+ parseFloat(getElementStyle(element, 'margin-bottom'));
56
+ return getElementHeight(element) + (isNaN(margin) ? 0 : margin);
57
+ };
49
58
  var getNumberOfNodesInHeight = function (childHeight, totalHeight) {
50
59
  if (!childHeight) {
51
60
  return 0;
@@ -53,7 +62,10 @@ var getNumberOfNodesInHeight = function (childHeight, totalHeight) {
53
62
  return Math.floor(totalHeight / childHeight);
54
63
  };
55
64
  var getTranslateValue = function (childHeight, startIndex) {
56
- return childHeight * startIndex;
65
+ if (startIndex > 0) {
66
+ return childHeight * startIndex;
67
+ }
68
+ return 0;
57
69
  };
58
70
  var getVisibleNodeCount = function (childHeight, startIndex, childrenLength, scrollContainer) {
59
71
  return Math.min(childrenLength - startIndex, getNumberOfNodesInHeight(childHeight, getElementHeight(scrollContainer)));
@@ -79,21 +91,55 @@ var getVisibleNodeCount = function (childHeight, startIndex, childrenLength, scr
79
91
  * />
80
92
  * @private
81
93
  */
82
- export var VirtualScroll = React.forwardRef(function (_a, ref) {
83
- var itemsLength = _a.itemsLength, itemRenderer = _a.itemRenderer, _b = _a.bufferSize, bufferSize = _b === void 0 ? 10 : _b, style = _a.style, rest = __rest(_a, ["itemsLength", "itemRenderer", "bufferSize", "style"]);
84
- var _c = React.useState(0), startNode = _c[0], setStartNode = _c[1];
85
- var _d = React.useState(0), visibleNodeCount = _d[0], setVisibleNodeCount = _d[1];
94
+ export var VirtualScroll = React.forwardRef(function (props, ref) {
95
+ var _a = useVirtualization(props), innerProps = _a.innerProps, outerProps = _a.outerProps, visibleChildren = _a.visibleChildren;
96
+ return (React.createElement("div", __assign({}, outerProps, { ref: ref }),
97
+ React.createElement("div", __assign({}, innerProps), visibleChildren)));
98
+ });
99
+ /**
100
+ * `useVirtualization` is used for efficiently rendering only the visible rows from a large list.
101
+ * It returns `outerProps` and `innerProps`, which need to be applied on 2 container elements and `visibleChildren` which is a list of virtualized items.
102
+ * @example
103
+ * const itemRenderer = React.useCallback((index: number) => (
104
+ * <li key={index}>
105
+ * This is my item #{index}
106
+ * </li>
107
+ * ), [])
108
+ *
109
+ * const { outerProps, innerProps, visibleChildren } = useVirtualization({itemsLength: 1000, itemRenderer: itemRenderer});
110
+ * return (
111
+ * <div {...outerProps}>
112
+ * <ul {...innerProps}>
113
+ * {visibleChildren}
114
+ * </ul>
115
+ * </div>
116
+ * );
117
+ * @private
118
+ */
119
+ export var useVirtualization = function (props) {
120
+ var itemsLength = props.itemsLength, itemRenderer = props.itemRenderer, _a = props.bufferSize, bufferSize = _a === void 0 ? 10 : _a, scrollToIndex = props.scrollToIndex, style = props.style, rest = __rest(props, ["itemsLength", "itemRenderer", "bufferSize", "scrollToIndex", "style"]);
121
+ var _b = React.useState(0), startNode = _b[0], setStartNode = _b[1];
122
+ var _c = React.useState(0), visibleNodeCount = _c[0], setVisibleNodeCount = _c[1];
86
123
  var scrollContainer = React.useRef();
87
124
  var parentRef = React.useRef(null);
88
- var childHeight = React.useRef(0);
125
+ var childHeight = React.useRef({ first: 0, middle: 0, last: 0 });
89
126
  var onScrollRef = React.useRef();
90
127
  // Used only to recalculate on resize
91
- var _e = React.useState(0), scrollContainerHeight = _e[0], setScrollContainerHeight = _e[1];
128
+ var _d = React.useState(0), scrollContainerHeight = _d[0], setScrollContainerHeight = _d[1];
129
+ var visibleIndex = React.useRef({ start: 0, end: 0 });
130
+ // Used to mark when scroll container has height (updated by resize observer)
131
+ // because before that calculations are not right
132
+ var _e = React.useState(false), isMounted = _e[0], setIsMounted = _e[1];
92
133
  var onResize = React.useCallback(function (_a) {
93
134
  var height = _a.height;
135
+ // Initial value returned by resize observer is 0
136
+ // So wait for the next one
137
+ if (height > 0) {
138
+ setIsMounted(true);
139
+ }
94
140
  setScrollContainerHeight(height);
95
141
  }, []);
96
- var resizeRef = useResizeObserver(onResize)[0];
142
+ var _f = useResizeObserver(onResize), resizeRef = _f[0], resizeObserver = _f[1];
97
143
  // Find scrollable parent
98
144
  // Needed only on init
99
145
  React.useLayoutEffect(function () {
@@ -102,6 +148,14 @@ export var VirtualScroll = React.forwardRef(function (_a, ref) {
102
148
  scrollContainer.current = scrollableParent;
103
149
  resizeRef(scrollableParent);
104
150
  }, [resizeRef]);
151
+ // Stop watching resize, when virtual scroll is unmounted
152
+ React.useLayoutEffect(function () {
153
+ return function () { return resizeObserver === null || resizeObserver === void 0 ? void 0 : resizeObserver.disconnect(); };
154
+ }, [resizeObserver]);
155
+ var getScrollableContainer = function () {
156
+ var _a, _b;
157
+ return (_a = scrollContainer.current) !== null && _a !== void 0 ? _a : (_b = parentRef.current) === null || _b === void 0 ? void 0 : _b.ownerDocument.scrollingElement;
158
+ };
105
159
  var visibleChildren = React.useMemo(function () {
106
160
  var arr = [];
107
161
  var endIndex = Math.min(itemsLength, startNode + visibleNodeCount + bufferSize * 2);
@@ -112,27 +166,42 @@ export var VirtualScroll = React.forwardRef(function (_a, ref) {
112
166
  }, [itemsLength, itemRenderer, bufferSize, startNode, visibleNodeCount]);
113
167
  // Get child height when children available
114
168
  React.useLayoutEffect(function () {
169
+ var _a, _b, _c, _d, _e, _f;
115
170
  if (!parentRef.current || !visibleChildren.length) {
116
171
  return;
117
172
  }
118
173
  var firstChild = parentRef.current.children.item(0);
119
- childHeight.current = Number(getElementHeight(firstChild).toFixed(2));
174
+ var secondChild = parentRef.current.children.item(1);
175
+ var lastChild = parentRef.current.children.item(parentRef.current.children.length - 1);
176
+ var firstChildHeight = Number((_b = (_a = getElementHeightWithMargins(firstChild)) === null || _a === void 0 ? void 0 : _a.toFixed(2)) !== null && _b !== void 0 ? _b : 0);
177
+ childHeight.current = {
178
+ first: firstChildHeight,
179
+ middle: Number((_d = (_c = getElementHeightWithMargins(secondChild)) === null || _c === void 0 ? void 0 : _c.toFixed(2)) !== null && _d !== void 0 ? _d : firstChildHeight),
180
+ last: Number((_f = (_e = getElementHeightWithMargins(lastChild)) === null || _e === void 0 ? void 0 : _e.toFixed(2)) !== null && _f !== void 0 ? _f : firstChildHeight),
181
+ };
120
182
  }, [visibleChildren.length]);
121
183
  var updateVirtualScroll = React.useCallback(function () {
122
- var _a, _b;
123
- var scrollableContainer = (_a = scrollContainer.current) !== null && _a !== void 0 ? _a : (_b = parentRef.current) === null || _b === void 0 ? void 0 : _b.ownerDocument.scrollingElement;
184
+ var scrollableContainer = getScrollableContainer();
124
185
  if (!scrollableContainer) {
125
186
  return;
126
187
  }
127
- var start = getNumberOfNodesInHeight(childHeight.current, scrollableContainer.scrollTop);
128
- var startIndex = Math.max(0, start - bufferSize);
188
+ var start = getNumberOfNodesInHeight(childHeight.current.middle, Math.round(scrollableContainer.scrollTop));
189
+ var visibleNodes = getVisibleNodeCount(childHeight.current.middle, start, itemsLength, scrollableContainer);
190
+ // If there are less items at the end than buffer size
191
+ // show more items at the start.
192
+ // Have boundaries for edge cases, e.g. 1 item length
193
+ var startIndex = Math.min(Math.max(0, start - bufferSize), Math.max(0, itemsLength - bufferSize * 2 - visibleNodes));
194
+ visibleIndex.current = { start: start, end: start + visibleNodes };
129
195
  setStartNode(startIndex);
130
- setVisibleNodeCount(getVisibleNodeCount(childHeight.current, start, itemsLength, scrollableContainer));
196
+ setVisibleNodeCount(visibleNodes);
131
197
  if (!parentRef.current) {
132
198
  return;
133
199
  }
134
- parentRef.current.style.transform = "translateY(".concat(getTranslateValue(childHeight.current, startIndex), "px)");
200
+ parentRef.current.style.transform = "translateY(".concat(getTranslateValue(childHeight.current.middle, startIndex), "px)");
135
201
  }, [bufferSize, itemsLength]);
202
+ var onScroll = React.useCallback(function () {
203
+ updateVirtualScroll();
204
+ }, [updateVirtualScroll]);
136
205
  var removeScrollListener = React.useCallback(function () {
137
206
  var _a, _b;
138
207
  if (!onScrollRef.current) {
@@ -147,22 +216,85 @@ export var VirtualScroll = React.forwardRef(function (_a, ref) {
147
216
  React.useLayoutEffect(function () {
148
217
  var _a, _b;
149
218
  removeScrollListener();
150
- onScrollRef.current = updateVirtualScroll;
219
+ onScrollRef.current = onScroll;
151
220
  if (!scrollContainer.current ||
152
221
  scrollContainer.current === ((_a = parentRef.current) === null || _a === void 0 ? void 0 : _a.ownerDocument.body)) {
153
- (_b = parentRef.current) === null || _b === void 0 ? void 0 : _b.ownerDocument.addEventListener('scroll', updateVirtualScroll);
222
+ (_b = parentRef.current) === null || _b === void 0 ? void 0 : _b.ownerDocument.addEventListener('scroll', onScroll);
154
223
  }
155
224
  else {
156
- scrollContainer.current.addEventListener('scroll', updateVirtualScroll);
225
+ scrollContainer.current.addEventListener('scroll', onScroll);
157
226
  }
158
227
  return removeScrollListener;
159
- }, [updateVirtualScroll, removeScrollListener]);
228
+ }, [onScroll, removeScrollListener]);
229
+ React.useLayoutEffect(function () {
230
+ if (!isMounted) {
231
+ return;
232
+ }
233
+ var scrollableContainer = getScrollableContainer();
234
+ if (!scrollableContainer || scrollToIndex == null) {
235
+ return;
236
+ }
237
+ // if `scrollToIndex` is not visible, scroll to it
238
+ if (scrollToIndex > visibleIndex.current.end ||
239
+ scrollToIndex < visibleIndex.current.start) {
240
+ var indexDiff = scrollToIndex > visibleIndex.current.end
241
+ ? scrollToIndex - visibleIndex.current.end
242
+ : scrollToIndex - visibleIndex.current.start;
243
+ if (scrollToIndex === 0) {
244
+ scrollableContainer.scrollTo({ top: 0 });
245
+ return;
246
+ }
247
+ // If go down: add to the existing scrollTop needed height
248
+ // If go up: calculate the exact scroll top
249
+ scrollableContainer.scrollTo({
250
+ top: indexDiff > 0
251
+ ? Math.ceil(scrollableContainer.scrollTop) +
252
+ indexDiff * childHeight.current.middle
253
+ : scrollToIndex * childHeight.current.middle,
254
+ });
255
+ }
256
+ // if `scrollToIndex` is the first visible node
257
+ // ensure it is fully visible
258
+ if (scrollToIndex === visibleIndex.current.start) {
259
+ var roundedScrollTop = Math.round(scrollableContainer.scrollTop);
260
+ var diff = roundedScrollTop % childHeight.current.middle;
261
+ diff > 0 &&
262
+ scrollableContainer.scrollTo({
263
+ top: roundedScrollTop - diff,
264
+ });
265
+ return;
266
+ }
267
+ // if `scrollToIndex` is the last visible node
268
+ // ensure it is fully visible
269
+ if (scrollToIndex === visibleIndex.current.end) {
270
+ var diff = (scrollableContainer.offsetHeight - childHeight.current.first) %
271
+ childHeight.current.middle;
272
+ var roundedScrollTop = Math.ceil(scrollableContainer.scrollTop);
273
+ var scrollTopMod = roundedScrollTop % childHeight.current.middle;
274
+ if (diff > 0 && scrollTopMod === 0) {
275
+ scrollableContainer.scrollTo({
276
+ top: roundedScrollTop + childHeight.current.middle - diff,
277
+ });
278
+ }
279
+ }
280
+ }, [scrollToIndex, isMounted]);
160
281
  React.useLayoutEffect(function () {
282
+ if (!scrollContainerHeight) {
283
+ return;
284
+ }
161
285
  updateVirtualScroll();
162
- }, [scrollContainerHeight, itemsLength, updateVirtualScroll]);
163
- return (React.createElement("div", __assign({ style: __assign({ overflow: 'hidden', minHeight: itemsLength * childHeight.current, width: '100%' }, style), ref: ref }, rest),
164
- React.createElement("div", { style: {
165
- willChange: 'transform',
166
- }, ref: parentRef }, visibleChildren)));
167
- });
286
+ }, [scrollContainerHeight, updateVirtualScroll]);
287
+ return {
288
+ outerProps: __assign({ style: __assign({ overflow: 'hidden', minHeight: itemsLength > 1
289
+ ? Math.max(itemsLength - 2, 0) * childHeight.current.middle +
290
+ childHeight.current.first +
291
+ childHeight.current.last
292
+ : childHeight.current.middle, width: '100%' }, style) }, rest),
293
+ innerProps: {
294
+ style: { willChange: 'transform' },
295
+ ref: mergeRefs(parentRef), // convert object ref to callback ref for better types
296
+ },
297
+ visibleChildren: visibleChildren,
298
+ };
299
+ };
168
300
  export default VirtualScroll;
@@ -6,3 +6,4 @@ export * from './useContainerWidth';
6
6
  export * from './useTheme';
7
7
  export * from './useIntersection';
8
8
  export * from './useMediaQuery';
9
+ export * from './useSafeContext';
@@ -10,3 +10,4 @@ export * from './useContainerWidth';
10
10
  export * from './useTheme';
11
11
  export * from './useIntersection';
12
12
  export * from './useMediaQuery';
13
+ export * from './useSafeContext';
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ /**
3
+ * Wrapper hook around `useContext` that throws an error if the context is not provided.
4
+ * @param context Context to use. Must have a `displayName` for useful errors.
5
+ */
6
+ export declare const useSafeContext: <T>(context: React.Context<T>) => NonNullable<T>;
@@ -0,0 +1,16 @@
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 React from 'react';
6
+ /**
7
+ * Wrapper hook around `useContext` that throws an error if the context is not provided.
8
+ * @param context Context to use. Must have a `displayName` for useful errors.
9
+ */
10
+ export var useSafeContext = function (context) {
11
+ var value = React.useContext(context);
12
+ if (!value) {
13
+ throw new Error("".concat(context.displayName, " is undefined"));
14
+ }
15
+ return value; // eslint-disable-line @typescript-eslint/no-non-null-assertion -- we already checked for undefined
16
+ };