Skip to content

Commit b8a572a

Browse files
committed
fix(CanlendarPicker): component freezes when the time interval is too large, using virtual scrolling optimization
1 parent bcbf0c7 commit b8a572a

File tree

3 files changed

+164
-127
lines changed

3 files changed

+164
-127
lines changed

Diff for: package.json

+3
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
"rc-util": "^5.38.1",
7272
"react-fast-compare": "^3.2.2",
7373
"react-is": "^18.2.0",
74+
"react-virtualized-auto-sizer": "^1.0.24",
75+
"react-window": "^1.8.10",
7476
"runes2": "^1.1.2",
7577
"staged-components": "^1.1.3",
7678
"tslib": "^2.5.0",
@@ -106,6 +108,7 @@
106108
"@types/react-helmet": "^6.1.6",
107109
"@types/react-is": "^17.0.3",
108110
"@types/react-virtualized": "^9.21.21",
111+
"@types/react-window": "^1.8.8",
109112
"@types/resize-observer-browser": "^0.1.7",
110113
"@types/testing-library__jest-dom": "^5.14.5",
111114
"@types/use-sync-external-store": "^0.0.3",

Diff for: src/components/calendar-picker-view/calendar-picker-view.less

+7
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,11 @@
144144
text-align: center;
145145
}
146146
}
147+
148+
&-no-scrollbar {
149+
scrollbar-width: none; /* firefox */
150+
-ms-overflow-style: none; /* IE 10+ */
151+
overflow-x: hidden;
152+
overflow-y: auto;
153+
}
147154
}

Diff for: src/components/calendar-picker-view/calendar-picker-view.tsx

+154-127
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import classNames from 'classnames'
22
import dayjs from 'dayjs'
3-
import isoWeek from 'dayjs/plugin/isoWeek'
43
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
4+
import isoWeek from 'dayjs/plugin/isoWeek'
55
import React, {
66
forwardRef,
7-
ReactNode,
87
useContext,
98
useEffect,
109
useImperativeHandle,
1110
useMemo,
1211
useRef,
1312
useState,
1413
} from 'react'
14+
import AutoSizer from 'react-virtualized-auto-sizer'
15+
import { FixedSizeList as List } from 'react-window'
1516
import { NativeProps, withNativeProps } from '../../utils/native-props'
1617
import { usePropsValue } from '../../utils/use-props-value'
1718
import { mergeProps } from '../../utils/with-default-props'
1819
import { useConfig } from '../config-provider'
1920
import {
20-
convertPageToDayjs,
21-
convertValueToRange,
2221
DateRange,
2322
Page,
23+
convertPageToDayjs,
24+
convertValueToRange,
2425
} from './convert'
2526
import useSyncScroll from './useSyncScroll'
2627

@@ -82,6 +83,11 @@ const defaultProps = {
8283
selectionMode: 'single',
8384
}
8485

86+
type RowProps = {
87+
index: number
88+
style: React.CSSProperties
89+
}
90+
8591
export const CalendarPickerView = forwardRef<
8692
CalendarPickerViewRef,
8793
CalendarPickerViewProps
@@ -193,12 +199,28 @@ export const CalendarPickerView = forwardRef<
193199
)
194200

195201
function renderBody() {
196-
const cells: ReactNode[] = []
197-
let monthIterator = minDay
198-
// 遍历月份
202+
const totalMonths = Math.ceil(maxDay.diff(minDay, 'months', true))
203+
// default 每个月的高度是 344px
204+
const monthHeight = 344
205+
const cells: {
206+
year: number
207+
month: number
208+
daysInMonth: number
209+
monthIterator: dayjs.Dayjs
210+
}[] = []
211+
let monthIterator = minDay.clone()
212+
199213
while (monthIterator.isSameOrBefore(maxDay, 'month')) {
200214
const year = monthIterator.year()
201215
const month = monthIterator.month() + 1
216+
const daysInMonth = monthIterator.daysInMonth()
217+
218+
cells.push({ year, month, daysInMonth, monthIterator })
219+
monthIterator = monthIterator.add(1, 'month')
220+
}
221+
222+
const Row = ({ index, style }: RowProps) => {
223+
const { year, month, daysInMonth, monthIterator } = cells[index]
202224

203225
const renderMap = {
204226
year,
@@ -212,17 +234,16 @@ export const CalendarPickerView = forwardRef<
212234
props.weekStartsOn === 'Monday'
213235
? monthIterator.date(1).isoWeekday() - 1
214236
: monthIterator.date(1).isoWeekday()
237+
215238
const presetEmptyCells =
216239
presetEmptyCellCount == 7
217240
? null
218-
: Array(presetEmptyCellCount)
219-
.fill(null)
220-
.map((_, index) => (
221-
<div key={index} className={`${classPrefix}-cell`}></div>
222-
))
223-
224-
cells.push(
225-
<div key={yearMonth} data-year-month={yearMonth}>
241+
: Array.from({ length: presetEmptyCellCount }).map((_, index) => (
242+
<div key={index} className={`${classPrefix}-cell`}></div>
243+
))
244+
245+
return (
246+
<div style={style} key={yearMonth} data-year-month={yearMonth}>
226247
<div className={`${classPrefix}-title`}>
227248
{locale.Calendar.yearAndMonth?.replace(
228249
/\${(.*?)}/g,
@@ -235,135 +256,141 @@ export const CalendarPickerView = forwardRef<
235256
{/* 空格填充 */}
236257
{presetEmptyCells}
237258
{/* 遍历每月 */}
238-
{Array(monthIterator.daysInMonth())
239-
.fill(null)
240-
.map((_, index) => {
241-
const d = monthIterator.date(index + 1)
242-
let isSelect = false
243-
let isBegin = false
244-
let isEnd = false
245-
let isSelectRowBegin = false
246-
let isSelectRowEnd = false
247-
if (dateRange) {
248-
const [begin, end] = dateRange
249-
isBegin = d.isSame(begin, 'day')
250-
isEnd = d.isSame(end, 'day')
251-
isSelect =
252-
isBegin ||
253-
isEnd ||
254-
(d.isAfter(begin, 'day') && d.isBefore(end, 'day'))
255-
if (isSelect) {
256-
isSelectRowBegin =
257-
(cells.length % 7 === 0 ||
258-
d.isSame(d.startOf('month'), 'day')) &&
259-
!isBegin
260-
isSelectRowEnd =
261-
(cells.length % 7 === 6 ||
262-
d.isSame(d.endOf('month'), 'day')) &&
263-
!isEnd
264-
}
259+
{Array.from({ length: daysInMonth }).map((_, index) => {
260+
const d = monthIterator.date(index + 1)
261+
let isSelect = false
262+
let isBegin = false
263+
let isEnd = false
264+
let isSelectRowBegin = false
265+
let isSelectRowEnd = false
266+
if (dateRange) {
267+
const [begin, end] = dateRange
268+
isBegin = d.isSame(begin, 'day')
269+
isEnd = d.isSame(end, 'day')
270+
isSelect =
271+
isBegin ||
272+
isEnd ||
273+
(d.isAfter(begin, 'day') && d.isBefore(end, 'day'))
274+
if (isSelect) {
275+
isSelectRowBegin =
276+
(cells.length % 7 === 0 ||
277+
d.isSame(d.startOf('month'), 'day')) &&
278+
!isBegin
279+
isSelectRowEnd =
280+
(cells.length % 7 === 6 ||
281+
d.isSame(d.endOf('month'), 'day')) &&
282+
!isEnd
265283
}
266-
const disabled = props.shouldDisableDate
267-
? props.shouldDisableDate(d.toDate())
268-
: (maxDay && d.isAfter(maxDay, 'day')) ||
269-
(minDay && d.isBefore(minDay, 'day'))
270-
271-
const renderTop = () => {
272-
const top = props.renderTop?.(d.toDate())
284+
}
285+
const disabled = props.shouldDisableDate
286+
? props.shouldDisableDate(d.toDate())
287+
: (maxDay && d.isAfter(maxDay, 'day')) ||
288+
(minDay && d.isBefore(minDay, 'day'))
273289

274-
if (top) {
275-
return top
276-
}
290+
const renderTop = () => {
291+
const top = props.renderTop?.(d.toDate())
277292

278-
if (props.selectionMode === 'range') {
279-
if (isBegin) {
280-
return locale.Calendar.start
281-
}
293+
if (top) {
294+
return top
295+
}
282296

283-
if (isEnd) {
284-
return locale.Calendar.end
285-
}
297+
if (props.selectionMode === 'range') {
298+
if (isBegin) {
299+
return locale.Calendar.start
286300
}
287301

288-
if (d.isSame(today, 'day') && !isSelect) {
289-
return locale.Calendar.today
302+
if (isEnd) {
303+
return locale.Calendar.end
290304
}
291305
}
292-
return (
293-
<div
294-
key={d.valueOf()}
295-
className={classNames(`${classPrefix}-cell`, {
296-
[`${classPrefix}-cell-today`]: d.isSame(today, 'day'),
297-
[`${classPrefix}-cell-selected`]: isSelect,
298-
[`${classPrefix}-cell-selected-begin`]: isBegin,
299-
[`${classPrefix}-cell-selected-end`]: isEnd,
300-
[`${classPrefix}-cell-selected-row-begin`]:
301-
isSelectRowBegin,
302-
[`${classPrefix}-cell-selected-row-end`]: isSelectRowEnd,
303-
[`${classPrefix}-cell-disabled`]: !!disabled,
304-
})}
305-
onClick={() => {
306-
if (!props.selectionMode) return
307-
if (disabled) return
308-
const date = d.toDate()
309-
function shouldClear() {
310-
if (!props.allowClear) return false
311-
if (!dateRange) return false
312-
const [begin, end] = dateRange
313-
return d.isSame(begin, 'date') && d.isSame(end, 'day')
306+
307+
if (d.isSame(today, 'day') && !isSelect) {
308+
return locale.Calendar.today
309+
}
310+
}
311+
return (
312+
<div
313+
key={d.valueOf()}
314+
className={classNames(`${classPrefix}-cell`, {
315+
[`${classPrefix}-cell-today`]: d.isSame(today, 'day'),
316+
[`${classPrefix}-cell-selected`]: isSelect,
317+
[`${classPrefix}-cell-selected-begin`]: isBegin,
318+
[`${classPrefix}-cell-selected-end`]: isEnd,
319+
[`${classPrefix}-cell-selected-row-begin`]:
320+
isSelectRowBegin,
321+
[`${classPrefix}-cell-selected-row-end`]: isSelectRowEnd,
322+
[`${classPrefix}-cell-disabled`]: !!disabled,
323+
})}
324+
onClick={() => {
325+
if (!props.selectionMode) return
326+
if (disabled) return
327+
const date = d.toDate()
328+
function shouldClear() {
329+
if (!props.allowClear) return false
330+
if (!dateRange) return false
331+
const [begin, end] = dateRange
332+
return d.isSame(begin, 'date') && d.isSame(end, 'day')
333+
}
334+
if (props.selectionMode === 'single') {
335+
if (props.allowClear && shouldClear()) {
336+
onDateChange(null)
337+
return
338+
}
339+
onDateChange([date, date])
340+
} else if (props.selectionMode === 'range') {
341+
if (!dateRange) {
342+
onDateChange([date, date])
343+
setIntermediate(true)
344+
return
345+
}
346+
if (shouldClear()) {
347+
onDateChange(null)
348+
setIntermediate(false)
349+
return
314350
}
315-
if (props.selectionMode === 'single') {
316-
if (props.allowClear && shouldClear()) {
317-
onDateChange(null)
318-
return
319-
}
351+
if (intermediate) {
352+
const another = dateRange[0]
353+
onDateChange(
354+
another > date ? [date, another] : [another, date]
355+
)
356+
setIntermediate(false)
357+
} else {
320358
onDateChange([date, date])
321-
} else if (props.selectionMode === 'range') {
322-
if (!dateRange) {
323-
onDateChange([date, date])
324-
setIntermediate(true)
325-
return
326-
}
327-
if (shouldClear()) {
328-
onDateChange(null)
329-
setIntermediate(false)
330-
return
331-
}
332-
if (intermediate) {
333-
const another = dateRange[0]
334-
onDateChange(
335-
another > date ? [date, another] : [another, date]
336-
)
337-
setIntermediate(false)
338-
} else {
339-
onDateChange([date, date])
340-
setIntermediate(true)
341-
}
359+
setIntermediate(true)
342360
}
343-
}}
344-
>
345-
<div className={`${classPrefix}-cell-top`}>
346-
{renderTop()}
347-
</div>
348-
<div className={`${classPrefix}-cell-date`}>
349-
{props.renderDate
350-
? props.renderDate(d.toDate())
351-
: d.date()}
352-
</div>
353-
<div className={`${classPrefix}-cell-bottom`}>
354-
{props.renderBottom?.(d.toDate())}
355-
</div>
361+
}
362+
}}
363+
>
364+
<div className={`${classPrefix}-cell-top`}>{renderTop()}</div>
365+
<div className={`${classPrefix}-cell-date`}>
366+
{props.renderDate ? props.renderDate(d.toDate()) : d.date()}
367+
</div>
368+
<div className={`${classPrefix}-cell-bottom`}>
369+
{props.renderBottom?.(d.toDate())}
356370
</div>
357-
)
358-
})}
371+
</div>
372+
)
373+
})}
359374
</div>
360375
</div>
361376
)
362-
363-
monthIterator = monthIterator.add(1, 'month')
364377
}
365378

366-
return cells
379+
return (
380+
<AutoSizer>
381+
{({ height, width }) => (
382+
<List
383+
height={height}
384+
itemCount={totalMonths}
385+
itemSize={monthHeight}
386+
width={width}
387+
className={`${classPrefix}-no-scrollbar`}
388+
>
389+
{Row}
390+
</List>
391+
)}
392+
</AutoSizer>
393+
)
367394
}
368395
const body = (
369396
<div className={`${classPrefix}-body`} ref={bodyRef}>

0 commit comments

Comments
 (0)