Skip to content

Commit dad13d5

Browse files
yihuiliaodevongovettsnowystinger
authored
Tableview crash bug (#5380)
* tableview crash bug * clean up * lint * subtract border width from body width * Use percentage width when elements are full width of their parents * remove old code * Fix headers not scrolling * Revert "Fix headers not scrolling" This reverts commit b4104f8. * Add inner div to headers * try setting overflow-x: hidden instead --------- Co-authored-by: Devon Govett <[email protected]> Co-authored-by: Rob Snow <[email protected]>
1 parent 2bfb2a1 commit dad13d5

File tree

2 files changed

+161
-4
lines changed

2 files changed

+161
-4
lines changed

packages/@react-aria/virtualizer/src/ScrollView.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,16 +159,27 @@ function ScrollView(props: ScrollViewProps, ref: RefObject<HTMLDivElement>) {
159159
if (scrollDirection === 'horizontal') {
160160
style.overflowX = 'auto';
161161
style.overflowY = 'hidden';
162-
} else if (scrollDirection === 'vertical') {
162+
} else if (scrollDirection === 'vertical' || contentSize.width === state.width) {
163+
// Set overflow-x: hidden if content size is equal to the width of the scroll view.
164+
// This prevents horizontal scrollbars from flickering during resizing due to resize observer
165+
// firing slower than the frame rate, which may cause an infinite re-render loop.
163166
style.overflowY = 'auto';
164167
style.overflowX = 'hidden';
165168
} else {
166169
style.overflow = 'auto';
167170
}
168171

172+
innerStyle = {
173+
width: contentSize.width,
174+
height: contentSize.height,
175+
pointerEvents: isScrolling ? 'none' : 'auto',
176+
position: 'relative',
177+
...innerStyle
178+
};
179+
169180
return (
170181
<div {...otherProps} style={style} ref={ref} onScroll={onScroll}>
171-
<div role="presentation" style={{width: contentSize.width, height: contentSize.height, pointerEvents: isScrolling ? 'none' : 'auto', position: 'relative', ...innerStyle}}>
182+
<div role="presentation" style={innerStyle}>
172183
{children}
173184
</div>
174185
</div>

packages/@react-spectrum/table/stories/Table.stories.tsx

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -927,8 +927,7 @@ function AsyncLoadingExample(props) {
927927
let url = new URL('https://www.reddit.com/r/upliftingnews.json');
928928
if (cursor) {
929929
url.searchParams.append('after', cursor);
930-
}
931-
930+
}
932931
let res = await fetch(url.toString(), {signal});
933932
let json = await res.json();
934933
return {items: json.data.children, cursor: json.data.after};
@@ -983,6 +982,153 @@ export const AsyncLoading: TableStory = {
983982
name: 'async loading'
984983
};
985984

985+
async function fakeFetch() {
986+
return new Promise(
987+
(resolve) => {
988+
setTimeout(() => resolve({json: async function () {
989+
return {data: {children: [
990+
{data: {id: 'foo', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
991+
{data: {id: 'fooo', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
992+
{data: {id: 'foooo', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
993+
{data: {id: 'fooooo', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
994+
{data: {id: 'foooooo', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
995+
{data: {id: 'doo', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
996+
{data: {id: '1', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
997+
{data: {id: '2', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
998+
{data: {id: '3', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
999+
{data: {id: '4', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
1000+
{data: {id: '5', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}},
1001+
{data: {id: '6', thumbnail: 'https://i.imgur.com/Z7AzH2c.jpg', ups: '7', title: 'cats', created: Date.now()}}
1002+
]}};
1003+
}}), 1000);
1004+
}
1005+
);
1006+
}
1007+
1008+
export const AsyncLoadingQuarryTest: TableStory = {
1009+
args: {
1010+
'aria-label': 'Top news from Reddit',
1011+
selectionMode: 'multiple',
1012+
height: 400,
1013+
width: '100%'
1014+
},
1015+
render: (args) => <AsyncLoadingExampleQuarryTest {...args} />,
1016+
name: 'async reload on sort'
1017+
};
1018+
1019+
function AsyncLoadingExampleQuarryTest(props) {
1020+
let [filters, setFilters] = React.useState({});
1021+
1022+
const rColumns = [
1023+
{
1024+
key: 'title',
1025+
label: 'Title'
1026+
},
1027+
{
1028+
key: 'ups',
1029+
label: 'Upvotes',
1030+
width: 200
1031+
},
1032+
{
1033+
key: 'created',
1034+
label: 'Created',
1035+
width: 350
1036+
}
1037+
];
1038+
interface Post {
1039+
id: string,
1040+
key: string,
1041+
preview?: string,
1042+
ups: number,
1043+
title: string,
1044+
created: string,
1045+
url: string
1046+
}
1047+
1048+
let list = useAsyncList<Post>({
1049+
getKey: (item) => item.id,
1050+
async load({cursor}) {
1051+
const url = new URL('https://www.reddit.com/r/aww.json');
1052+
1053+
if (cursor) {
1054+
url.searchParams.append('after', cursor);
1055+
}
1056+
1057+
const res = await fakeFetch();
1058+
// @ts-ignore
1059+
const json = await res.json();
1060+
const items = json.data.children.map((item) => {
1061+
return {
1062+
id: item.data.id,
1063+
preview: item.data.thumbnail,
1064+
ups: item.data.ups,
1065+
title: item.data.title,
1066+
created: new Date(item.data.created * 1000).toISOString()
1067+
};
1068+
});
1069+
return {
1070+
items,
1071+
cursor: json.data.after,
1072+
total: 987
1073+
};
1074+
},
1075+
async sort({items, sortDescriptor}) {
1076+
return {
1077+
items: items.slice().sort((a, b) => {
1078+
let cmp = a[sortDescriptor.column] < b[sortDescriptor.column] ? -1 : 1;
1079+
1080+
if (sortDescriptor.direction === 'descending') {
1081+
cmp *= -1;
1082+
}
1083+
1084+
return cmp;
1085+
})
1086+
1087+
};
1088+
}
1089+
});
1090+
1091+
let reloadDeps = [filters];
1092+
1093+
useMountEffect((): void => {
1094+
list.reload();
1095+
}, reloadDeps);
1096+
1097+
return (
1098+
<TableView {...props} width="90vw" sortDescriptor={list.sortDescriptor} selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} onSortChange={setFilters}>
1099+
<TableHeader columns={rColumns}>
1100+
{(column) => (
1101+
<Column key={column.key} allowsSorting>
1102+
{column.label}
1103+
</Column>
1104+
)}
1105+
</TableHeader>
1106+
<TableBody items={list.items} loadingState={list.loadingState} onLoadMore={list.loadMore}>
1107+
{item =>
1108+
(<Row key={item.id}>
1109+
{key =>
1110+
key === 'title'
1111+
? <Cell textValue={item.title}><Link isQuiet><a href={item.url} target="_blank">{item.title}</a></Link></Cell>
1112+
: <Cell>{item[key]}</Cell>
1113+
}
1114+
</Row>)
1115+
}
1116+
</TableBody>
1117+
</TableView>
1118+
);
1119+
}
1120+
1121+
function useMountEffect(fn: () => void, deps: Array<unknown>): void {
1122+
const mounted = React.useRef(false);
1123+
React.useEffect(() => {
1124+
if (mounted.current) {
1125+
fn();
1126+
} else {
1127+
mounted.current = true;
1128+
}
1129+
}, deps);
1130+
}
1131+
9861132
export const HideHeader: TableStory = {
9871133
args: {
9881134
'aria-label': 'TableView with static contents',

0 commit comments

Comments
 (0)