carbon-react 124.3.0 → 124.3.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.
@@ -1,6 +1,8 @@
1
1
  import { enableFetchMocks } from "jest-fetch-mock";
2
2
  import { setupMatchMediaMock } from "./mock-match-media";
3
3
  import setupResizeObserverMock from "./mock-resize-observer";
4
+ import setupScrollToMock from "./mock-element-scrollto";
4
5
  enableFetchMocks();
5
6
  setupResizeObserverMock();
6
- setupMatchMediaMock();
7
+ setupMatchMediaMock();
8
+ setupScrollToMock();
@@ -0,0 +1,2 @@
1
+ declare const setupScrollToMock: () => void;
2
+ export default setupScrollToMock;
@@ -0,0 +1,6 @@
1
+ const setupScrollToMock = () => {
2
+ // need to mock the `scrollTo` method, which is undefined in JSDOM. As we're not actually testing this behaviour, just make it
3
+ // do nothing.
4
+ HTMLElement.prototype.scrollTo = () => {};
5
+ };
6
+ export default setupScrollToMock;
@@ -0,0 +1,3 @@
1
+ import type { ReactWrapper } from "enzyme";
2
+ export declare function simulateSelectTextboxEvent(container: ReactWrapper, eventType: string, ...eventArgs: any[]): void;
3
+ export declare function simulateDropdownEvent(container: ReactWrapper, eventType: string): void;
@@ -0,0 +1,39 @@
1
+ import { act } from "react-dom/test-utils";
2
+ import { mockResizeObserver } from "jsdom-testing-mocks";
3
+ import StyledSelectListContainer from "../components/select/select-list/select-list-container.style";
4
+ const resizeObserver = mockResizeObserver();
5
+ export function simulateSelectTextboxEvent(container, eventType,
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ ...eventArgs) {
8
+ const selectText = container.find('input[type="text"]').first();
9
+ selectText.simulate(eventType, ...eventArgs);
10
+ const selectList = container.find(StyledSelectListContainer).getDOMNode();
11
+ // need to manually trigger a resize to make react-virtual realise that child options should be rendered
12
+ act(() => {
13
+ resizeObserver.mockElementSize(selectList, {
14
+ contentBoxSize: {
15
+ inlineSize: 500,
16
+ blockSize: 180
17
+ }
18
+ });
19
+ resizeObserver.resize();
20
+ });
21
+ if (eventType === "focus") jest.runOnlyPendingTimers();
22
+ container.update();
23
+ }
24
+ export function simulateDropdownEvent(container, eventType) {
25
+ const dropdown = container.find('[type="dropdown"]').first();
26
+ dropdown.simulate(eventType);
27
+ const selectList = container.find(StyledSelectListContainer).getDOMNode();
28
+ // need to manually trigger a resize to make react-virtual realise that child options should be rendered
29
+ act(() => {
30
+ resizeObserver.mockElementSize(selectList, {
31
+ contentBoxSize: {
32
+ inlineSize: 500,
33
+ blockSize: 180
34
+ }
35
+ });
36
+ resizeObserver.resize();
37
+ });
38
+ container.update();
39
+ }
@@ -2,8 +2,9 @@ function _extends() { _extends = Object.assign ? Object.assign.bind() : function
2
2
  import React, { useEffect, useState, useCallback, useLayoutEffect, useRef, useMemo } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { flip, offset, size } from "@floating-ui/dom";
5
- import { useVirtualizer } from "@tanstack/react-virtual";
5
+ import { useVirtualizer, defaultRangeExtractor } from "@tanstack/react-virtual";
6
6
  import findLastIndex from "lodash/findLastIndex";
7
+ import usePrevious from "../../../hooks/__internal__/usePrevious";
7
8
  import useScrollBlock from "../../../hooks/__internal__/useScrollBlock";
8
9
  import useModalManager from "../../../hooks/__internal__/useModalManager";
9
10
  import { StyledSelectList, StyledSelectLoaderContainer, StyledSelectListTable, StyledSelectListTableHeader, StyledSelectListTableBody } from "./select-list.style";
@@ -60,7 +61,26 @@ const SelectList = /*#__PURE__*/React.forwardRef(({
60
61
  allowScroll
61
62
  } = useScrollBlock();
62
63
  const actionButtonHeight = useRef(0);
64
+ const wasOpen = usePrevious(isOpen);
65
+
66
+ // ensure scroll-position goes back to the top whenever the list is (re)-opened. (On Safari, without this it remains at the bottom if it had been scrolled
67
+ // to the bottom before closing.)
68
+ useEffect(() => {
69
+ if (isOpen && !wasOpen) {
70
+ listContainerRef.current?.scrollTo(0, 0);
71
+ }
72
+ });
63
73
  const overscan = enableVirtualScroll ? virtualScrollOverscan : React.Children.count(children);
74
+
75
+ // need to use a custom rangeExtractor that ensure the currently-selected option, if any, always appears as an option returned from the virtualiser.
76
+ // This ensures that the aria-activedescendant value is always valid even when the currently-selected item is out of the visible range.
77
+ const rangeExtractor = range => {
78
+ const toRender = defaultRangeExtractor(range);
79
+ if (currentOptionsListIndex !== -1 && !toRender.includes(currentOptionsListIndex)) {
80
+ toRender.push(currentOptionsListIndex);
81
+ }
82
+ return toRender;
83
+ };
64
84
  const virtualizer = useVirtualizer({
65
85
  count: React.Children.count(children),
66
86
  getScrollElement: () => listContainerRef.current,
@@ -68,9 +88,21 @@ const SelectList = /*#__PURE__*/React.forwardRef(({
68
88
  // value doesn't really seem to matter since we're dynamically measuring, but 40px is the height of a single-line option
69
89
  overscan,
70
90
  paddingStart: multiColumn ? TABLE_HEADER_HEIGHT : 0,
71
- scrollPaddingEnd: actionButtonHeight.current
91
+ scrollPaddingEnd: actionButtonHeight.current,
92
+ rangeExtractor
72
93
  });
73
94
  const items = virtualizer.getVirtualItems();
95
+
96
+ // getVirtualItems returns an empty array of items if the select list is currently closed - which is correct visually but
97
+ // we need to ensure that any currently-selected option is still in the DOM to avoid any accessibility issues.
98
+ // The isOpen prop will ensure that no options are visible regardless of what is in the items array.
99
+ if (items.length === 0 && currentOptionsListIndex !== -1) {
100
+ // only index property is required with the item not visible so the following type assertion, even though incorrect,
101
+ // should be OK
102
+ items.push({
103
+ index: currentOptionsListIndex
104
+ });
105
+ }
74
106
  const listHeight = virtualizer.getTotalSize();
75
107
  useEffect(() => {
76
108
  if (isOpen) {
@@ -119,11 +151,13 @@ const SelectList = /*#__PURE__*/React.forwardRef(({
119
151
  measureElement(element);
120
152
  }
121
153
  };
122
- const renderedChildren = items.map(item => {
123
- const {
124
- index,
125
- start
126
- } = item;
154
+
155
+ // the rangeExtractor above can cause an undefined value to be appended to the return items.
156
+ // Easiest way to stop that crashing is just to filter it out.
157
+ const renderedChildren = items.filter(item => item !== undefined).map(({
158
+ index,
159
+ start
160
+ }) => {
127
161
  const child = childrenList[index];
128
162
  if (! /*#__PURE__*/React.isValidElement(child)) {
129
163
  return child;
@@ -3,7 +3,9 @@
3
3
  var _jestFetchMock = require("jest-fetch-mock");
4
4
  var _mockMatchMedia = require("./mock-match-media");
5
5
  var _mockResizeObserver = _interopRequireDefault(require("./mock-resize-observer"));
6
+ var _mockElementScrollto = _interopRequireDefault(require("./mock-element-scrollto"));
6
7
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
7
8
  (0, _jestFetchMock.enableFetchMocks)();
8
9
  (0, _mockResizeObserver.default)();
9
- (0, _mockMatchMedia.setupMatchMediaMock)();
10
+ (0, _mockMatchMedia.setupMatchMediaMock)();
11
+ (0, _mockElementScrollto.default)();
@@ -0,0 +1,2 @@
1
+ declare const setupScrollToMock: () => void;
2
+ export default setupScrollToMock;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = void 0;
7
+ const setupScrollToMock = () => {
8
+ // need to mock the `scrollTo` method, which is undefined in JSDOM. As we're not actually testing this behaviour, just make it
9
+ // do nothing.
10
+ HTMLElement.prototype.scrollTo = () => {};
11
+ };
12
+ var _default = exports.default = setupScrollToMock;
@@ -0,0 +1,3 @@
1
+ import type { ReactWrapper } from "enzyme";
2
+ export declare function simulateSelectTextboxEvent(container: ReactWrapper, eventType: string, ...eventArgs: any[]): void;
3
+ export declare function simulateDropdownEvent(container: ReactWrapper, eventType: string): void;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.simulateDropdownEvent = simulateDropdownEvent;
7
+ exports.simulateSelectTextboxEvent = simulateSelectTextboxEvent;
8
+ var _testUtils = require("react-dom/test-utils");
9
+ var _jsdomTestingMocks = require("jsdom-testing-mocks");
10
+ var _selectListContainer = _interopRequireDefault(require("../components/select/select-list/select-list-container.style"));
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+ const resizeObserver = (0, _jsdomTestingMocks.mockResizeObserver)();
13
+ function simulateSelectTextboxEvent(container, eventType,
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ ...eventArgs) {
16
+ const selectText = container.find('input[type="text"]').first();
17
+ selectText.simulate(eventType, ...eventArgs);
18
+ const selectList = container.find(_selectListContainer.default).getDOMNode();
19
+ // need to manually trigger a resize to make react-virtual realise that child options should be rendered
20
+ (0, _testUtils.act)(() => {
21
+ resizeObserver.mockElementSize(selectList, {
22
+ contentBoxSize: {
23
+ inlineSize: 500,
24
+ blockSize: 180
25
+ }
26
+ });
27
+ resizeObserver.resize();
28
+ });
29
+ if (eventType === "focus") jest.runOnlyPendingTimers();
30
+ container.update();
31
+ }
32
+ function simulateDropdownEvent(container, eventType) {
33
+ const dropdown = container.find('[type="dropdown"]').first();
34
+ dropdown.simulate(eventType);
35
+ const selectList = container.find(_selectListContainer.default).getDOMNode();
36
+ // need to manually trigger a resize to make react-virtual realise that child options should be rendered
37
+ (0, _testUtils.act)(() => {
38
+ resizeObserver.mockElementSize(selectList, {
39
+ contentBoxSize: {
40
+ inlineSize: 500,
41
+ blockSize: 180
42
+ }
43
+ });
44
+ resizeObserver.resize();
45
+ });
46
+ container.update();
47
+ }
@@ -9,6 +9,7 @@ var _propTypes = _interopRequireDefault(require("prop-types"));
9
9
  var _dom = require("@floating-ui/dom");
10
10
  var _reactVirtual = require("@tanstack/react-virtual");
11
11
  var _findLastIndex = _interopRequireDefault(require("lodash/findLastIndex"));
12
+ var _usePrevious = _interopRequireDefault(require("../../../hooks/__internal__/usePrevious"));
12
13
  var _useScrollBlock = _interopRequireDefault(require("../../../hooks/__internal__/useScrollBlock"));
13
14
  var _useModalManager = _interopRequireDefault(require("../../../hooks/__internal__/useModalManager"));
14
15
  var _selectList = require("./select-list.style");
@@ -69,7 +70,26 @@ const SelectList = /*#__PURE__*/_react.default.forwardRef(({
69
70
  allowScroll
70
71
  } = (0, _useScrollBlock.default)();
71
72
  const actionButtonHeight = (0, _react.useRef)(0);
73
+ const wasOpen = (0, _usePrevious.default)(isOpen);
74
+
75
+ // ensure scroll-position goes back to the top whenever the list is (re)-opened. (On Safari, without this it remains at the bottom if it had been scrolled
76
+ // to the bottom before closing.)
77
+ (0, _react.useEffect)(() => {
78
+ if (isOpen && !wasOpen) {
79
+ listContainerRef.current?.scrollTo(0, 0);
80
+ }
81
+ });
72
82
  const overscan = enableVirtualScroll ? virtualScrollOverscan : _react.default.Children.count(children);
83
+
84
+ // need to use a custom rangeExtractor that ensure the currently-selected option, if any, always appears as an option returned from the virtualiser.
85
+ // This ensures that the aria-activedescendant value is always valid even when the currently-selected item is out of the visible range.
86
+ const rangeExtractor = range => {
87
+ const toRender = (0, _reactVirtual.defaultRangeExtractor)(range);
88
+ if (currentOptionsListIndex !== -1 && !toRender.includes(currentOptionsListIndex)) {
89
+ toRender.push(currentOptionsListIndex);
90
+ }
91
+ return toRender;
92
+ };
73
93
  const virtualizer = (0, _reactVirtual.useVirtualizer)({
74
94
  count: _react.default.Children.count(children),
75
95
  getScrollElement: () => listContainerRef.current,
@@ -77,9 +97,21 @@ const SelectList = /*#__PURE__*/_react.default.forwardRef(({
77
97
  // value doesn't really seem to matter since we're dynamically measuring, but 40px is the height of a single-line option
78
98
  overscan,
79
99
  paddingStart: multiColumn ? TABLE_HEADER_HEIGHT : 0,
80
- scrollPaddingEnd: actionButtonHeight.current
100
+ scrollPaddingEnd: actionButtonHeight.current,
101
+ rangeExtractor
81
102
  });
82
103
  const items = virtualizer.getVirtualItems();
104
+
105
+ // getVirtualItems returns an empty array of items if the select list is currently closed - which is correct visually but
106
+ // we need to ensure that any currently-selected option is still in the DOM to avoid any accessibility issues.
107
+ // The isOpen prop will ensure that no options are visible regardless of what is in the items array.
108
+ if (items.length === 0 && currentOptionsListIndex !== -1) {
109
+ // only index property is required with the item not visible so the following type assertion, even though incorrect,
110
+ // should be OK
111
+ items.push({
112
+ index: currentOptionsListIndex
113
+ });
114
+ }
83
115
  const listHeight = virtualizer.getTotalSize();
84
116
  (0, _react.useEffect)(() => {
85
117
  if (isOpen) {
@@ -128,11 +160,13 @@ const SelectList = /*#__PURE__*/_react.default.forwardRef(({
128
160
  measureElement(element);
129
161
  }
130
162
  };
131
- const renderedChildren = items.map(item => {
132
- const {
133
- index,
134
- start
135
- } = item;
163
+
164
+ // the rangeExtractor above can cause an undefined value to be appended to the return items.
165
+ // Easiest way to stop that crashing is just to filter it out.
166
+ const renderedChildren = items.filter(item => item !== undefined).map(({
167
+ index,
168
+ start
169
+ }) => {
136
170
  const child = childrenList[index];
137
171
  if (! /*#__PURE__*/_react.default.isValidElement(child)) {
138
172
  return child;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "carbon-react",
3
- "version": "124.3.0",
3
+ "version": "124.3.1",
4
4
  "description": "A library of reusable React components for easily building user interfaces.",
5
5
  "files": [
6
6
  "lib",
@@ -156,6 +156,7 @@
156
156
  "jest-environment-jsdom": "^27.5.1",
157
157
  "jest-fetch-mock": "^3.0.3",
158
158
  "jest-styled-components": "^6.3.4",
159
+ "jsdom-testing-mocks": "^1.11.0",
159
160
  "lint-staged": "^11.2.6",
160
161
  "mockdate": "^2.0.5",
161
162
  "nock": "^13.3.8",
@@ -182,7 +183,7 @@
182
183
  "@floating-ui/react-dom": "~1.3.0",
183
184
  "@octokit/rest": "^18.12.0",
184
185
  "@styled-system/prop-types": "^5.1.5",
185
- "@tanstack/react-virtual": "3.0.0-beta.54",
186
+ "@tanstack/react-virtual": "^3.0.0-beta.68",
186
187
  "@types/styled-system": "^5.1.22",
187
188
  "chalk": "^4.1.2",
188
189
  "ci-info": "^3.8.0",