Skip to content

Commit cdd03b9

Browse files
author
Haider Alshamma
committed
feat: Depart away from react-windowed-select
In this release, we roll our own windowing inside of react-select. This is mainly done because react-windowed-select types are outdated and causing many downstream issues for the Select and AsyncSelect users.
1 parent a2c9e9a commit cdd03b9

15 files changed

+682
-383
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"jscodeshift": "^0.11.0",
124124
"mockdate": "^3.0.2",
125125
"plop": "^2.4.0",
126+
"prettier": "^3.4.2",
126127
"react": "17.0.2",
127128
"react-color": "^2.18.1",
128129
"react-dom": "17.0.2",
@@ -151,6 +152,7 @@
151152
"@styled-system/prop-types": "^5.1.4",
152153
"@styled-system/theme-get": "^5.1.2",
153154
"@types/react-router-dom": "5.3.0",
155+
"@types/react-window": "^1.8.8",
154156
"@types/styled-system": "5.1.22",
155157
"body-scroll-lock": "^3.1.5",
156158
"core-js": "3",
@@ -169,6 +171,7 @@
169171
"react-popper-2": "npm:[email protected]",
170172
"react-resize-detector": "^9.1.0",
171173
"react-select": "^5.8.0",
174+
"react-window": "^1.8.11",
172175
"react-windowed-select": "^5.2.0",
173176
"smoothscroll-polyfill": "^0.4.4",
174177
"styled-system": "^5.1.4",

src/AsyncSelect/AsyncSelectComponents.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
MultiValueProps,
1111
} from "react-select";
1212
import { components, GroupBase } from "react-select";
13-
import type { OptionProps } from "react-windowed-select";
13+
import { OptionProps } from "react-select";
14+
import { IconName } from "@nulogy/icons";
1415
import { useComponentVariant } from "../NDSProvider/ComponentVariantContext";
1516
import type { ComponentVariant } from "../NDSProvider/ComponentVariantContext";
1617
import { StyledOption } from "../Select/SelectOption";
17-
import { IconName } from "@nulogy/icons";
1818
import { InputIcon } from "../Icon/Icon";
1919

2020
export const SelectControl = <Option, IsMulti extends boolean, Group extends GroupBase<Option>>({

src/Select/MenuList.tsx

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
Copied as is from: https://github.com/jacobworrel/react-windowed-select/blob/master/src/MenuList.tsx
3+
MIT License
4+
Copyright (c) 2019 Jacob Worrel
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
The above copyright notice and this permission notice shall be included in all
12+
copies or substantial portions of the Software.
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.
20+
*/
21+
22+
import * as React from "react";
23+
import { ListChildComponentProps, VariableSizeList as List } from "react-window";
24+
import { OptionProps, GroupBase } from "react-select";
25+
import { createGetHeight, flattenGroupedChildren, getCurrentIndex } from "./lib";
26+
27+
interface Style extends React.CSSProperties {
28+
top: number;
29+
}
30+
31+
interface ListChildProps extends ListChildComponentProps {
32+
style: Style;
33+
}
34+
35+
interface OptionTypeBase {
36+
[key: string]: any;
37+
}
38+
39+
function MenuList(props) {
40+
const children = React.useMemo(() => {
41+
const children = React.Children.toArray(props.children);
42+
43+
const head = children[0] || {};
44+
45+
if (React.isValidElement<OptionProps<OptionTypeBase, boolean, GroupBase<OptionTypeBase>>>(head)) {
46+
const { props: { data: { options = [] } = {} } = {} } = head;
47+
const groupedChildrenLength = options.length;
48+
const isGrouped = groupedChildrenLength > 0;
49+
const flattenedChildren = isGrouped && flattenGroupedChildren(children);
50+
51+
return isGrouped ? flattenedChildren : children;
52+
} else {
53+
return [];
54+
}
55+
}, [props.children]);
56+
57+
const { getStyles } = props;
58+
const groupHeadingStyles = getStyles("groupHeading", props);
59+
const loadingMsgStyles = getStyles("loadingMessage", props);
60+
const noOptionsMsgStyles = getStyles("noOptionsMessage", props);
61+
const optionStyles = getStyles("option", props);
62+
const getHeight = createGetHeight({
63+
groupHeadingStyles,
64+
noOptionsMsgStyles,
65+
optionStyles,
66+
loadingMsgStyles,
67+
});
68+
69+
const heights = React.useMemo(() => children.map(getHeight), [children]);
70+
const currentIndex = React.useMemo(() => getCurrentIndex(children), [children]);
71+
72+
const itemCount = children.length;
73+
74+
const [measuredHeights, setMeasuredHeights] = React.useState({});
75+
76+
// calc menu height
77+
const { maxHeight, paddingBottom = 0, paddingTop = 0, ...menuListStyle } = getStyles("menuList", props);
78+
const totalHeight = React.useMemo(() => {
79+
return heights.reduce((sum, height, idx) => {
80+
if (measuredHeights[idx]) {
81+
return sum + measuredHeights[idx];
82+
} else {
83+
return sum + height;
84+
}
85+
}, 0);
86+
}, [heights, measuredHeights]);
87+
const totalMenuHeight = totalHeight + paddingBottom + paddingTop;
88+
const menuHeight = Math.min(maxHeight, totalMenuHeight);
89+
const estimatedItemSize = Math.floor(totalHeight / itemCount);
90+
91+
const { innerRef, selectProps } = props;
92+
93+
const { classNamePrefix, isMulti } = selectProps || {};
94+
const list = React.useRef<List>(null);
95+
96+
React.useEffect(() => {
97+
setMeasuredHeights({});
98+
}, [props.children]);
99+
100+
// method to pass to inner item to set this items outer height
101+
const setMeasuredHeight = ({ index, measuredHeight }) => {
102+
if (measuredHeights[index] !== undefined && measuredHeights[index] === measuredHeight) {
103+
return;
104+
}
105+
106+
setMeasuredHeights((measuredHeights) => ({
107+
...measuredHeights,
108+
[index]: measuredHeight,
109+
}));
110+
111+
// this forces the list to rerender items after the item positions resizing
112+
if (list.current) {
113+
list.current.resetAfterIndex(index);
114+
}
115+
};
116+
117+
React.useEffect(() => {
118+
/**
119+
* enables scrolling on key down arrow
120+
*/
121+
if (currentIndex >= 0 && list.current !== null) {
122+
list.current.scrollToItem(currentIndex);
123+
}
124+
}, [currentIndex, children, list]);
125+
126+
return (
127+
<List
128+
className={
129+
classNamePrefix
130+
? `${classNamePrefix}__menu-list${isMulti ? ` ${classNamePrefix}__menu-list--is-multi` : ""}`
131+
: ""
132+
}
133+
style={menuListStyle}
134+
ref={list}
135+
outerRef={innerRef}
136+
estimatedItemSize={estimatedItemSize}
137+
innerElementType={React.forwardRef(({ style, ...rest }, ref) => (
138+
<div
139+
ref={ref}
140+
style={{
141+
...style,
142+
height: `${parseFloat(style.height) + paddingBottom + paddingTop}px`,
143+
}}
144+
{...rest}
145+
/>
146+
))}
147+
height={menuHeight}
148+
width="100%"
149+
itemCount={itemCount}
150+
itemData={children}
151+
itemSize={(index) => measuredHeights[index] || heights[index]}
152+
>
153+
{/*@ts-ignore*/}
154+
{({ data, index, style }: ListChildProps) => {
155+
return (
156+
<div
157+
style={{
158+
...style,
159+
top: `${parseFloat(style.top.toString()) + paddingTop}px`,
160+
}}
161+
>
162+
<MenuItem data={data[index]} index={index} setMeasuredHeight={setMeasuredHeight} />
163+
</div>
164+
);
165+
}}
166+
</List>
167+
);
168+
}
169+
170+
function MenuItem({ data, index, setMeasuredHeight }) {
171+
const ref = React.useRef<HTMLDivElement>(null);
172+
173+
// using useLayoutEffect prevents bounciness of options of re-renders
174+
React.useLayoutEffect(() => {
175+
if (ref.current) {
176+
const measuredHeight = ref.current.getBoundingClientRect().height;
177+
178+
setMeasuredHeight({ index, measuredHeight });
179+
}
180+
}, [ref.current]);
181+
182+
return (
183+
<div key={`option-${index}`} ref={ref}>
184+
{data}
185+
</div>
186+
);
187+
}
188+
export default MenuList;

src/Select/Select.spec.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import { fireEvent } from "@testing-library/react";
33
import { renderWithNDSProvider } from "../NDSProvider/renderWithNDSProvider.spec-utils";
44
import { selectOption } from "./Select.spec-utils";
5-
import { UsingRefToControlFocus, WithCustomProps, WithMultiselect, WithState } from "./Select.story";
5+
import { UsingRefToControlFocus, WithMultiselect, WithState } from "./Select.story";
66
import { Select } from ".";
77

88
describe("select", () => {
@@ -38,14 +38,6 @@ describe("select", () => {
3838
expect(container).toHaveTextContent("Three");
3939
});
4040

41-
it("passes along the custom props to custom components", () => {
42-
const { container, queryByText } = renderWithNDSProvider(<WithCustomProps />);
43-
44-
selectOption("custom prop value", container, queryByText);
45-
46-
expect(container).toHaveTextContent("custom prop value");
47-
});
48-
4941
describe("with state", () => {
5042
it("clears the selected option", () => {
5143
const { container, queryByText } = renderWithNDSProvider(<WithState />);

src/Select/Select.story.fixture.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from "react";
2+
import styled from "styled-components";
3+
import { SelectOption, SelectOptionProps } from "./SelectOption";
4+
import { NDSOption } from "./Select";
5+
6+
export const errorList = ["Error message 1", "Error message 2"];
7+
8+
export const options: NDSOption[] = [
9+
{ value: "accepted", label: "Accepted" },
10+
{ value: "assigned", label: "Assigned to a line" },
11+
{ value: "hold", label: "On hold" },
12+
{ value: "rejected", label: "Rejected" },
13+
{ value: "open", label: "Open" },
14+
{ value: "progress", label: "In progress" },
15+
{ value: "quarantine", label: "In quarantine" },
16+
];
17+
18+
export const partnerCompanyName = [
19+
{ value: "2", label: "PCN2 12387387484895884957848576867587685780" },
20+
{ value: "4", label: "PCN4 12387387484895884957848576867587685780" },
21+
{ value: "1", label: "PCN1 12387387484895884957848576867587685780" },
22+
{ value: "9", label: "PCN9 12387387484895884957848576867587685780" },
23+
{ value: "7", label: "PCN7 12387387484895884957848576867587685780" },
24+
{ value: "6", label: "PCN6 12387387484895884957848576867587685780" },
25+
{ value: "3", label: "PCN3 12387387484895884957848576867587685780e" },
26+
];
27+
28+
export const wrappingOptions = [
29+
{
30+
value: "onestring",
31+
label:
32+
"Onelongstringonelongstringonelongstringonelongstringonelongstringonelongstringonelongstringonelongstringonelongstringonelongstringonelongstring",
33+
},
34+
{
35+
value: "manywords",
36+
label:
37+
"Many words many words many words many words many words many words many words many words many words many words many words many words many words",
38+
},
39+
];
40+
41+
export const PCNList = [
42+
{ value: "2", label: "PCN2" },
43+
{ value: "4", label: "PCN4" },
44+
{ value: "1", label: "PCN1" },
45+
{ value: "9", label: "PCN9" },
46+
];
47+
48+
export const getPhotos = async () => {
49+
// returns 5000 items
50+
const data = await fetch("https://jsonplaceholder.typicode.com/photos");
51+
const json = await data.json();
52+
return json.map(({ title, id }) => ({
53+
label: title,
54+
value: id,
55+
}));
56+
};
57+
58+
const Indicator = styled.span(() => ({
59+
borderRadius: "25%",
60+
background: "green",
61+
lineHeight: "0",
62+
display: "inline-block",
63+
width: "10px",
64+
height: "10px",
65+
marginRight: "5px",
66+
}));
67+
68+
export const CustomOption = ({ children, ...props }: SelectOptionProps) => {
69+
const newChildren = (
70+
<>
71+
<Indicator />
72+
{children}
73+
</>
74+
);
75+
return <SelectOption {...props}>{newChildren}</SelectOption>;
76+
};

0 commit comments

Comments
 (0)