Skip to content

Commit

Permalink
fix(CanlendarPicker): component freezes when the time interval is too…
Browse files Browse the repository at this point in the history
… large, using virtual scrolling optimization
  • Loading branch information
LLmoskk committed Oct 24, 2024
1 parent bcbf0c7 commit b8a572a
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 127 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
"rc-util": "^5.38.1",
"react-fast-compare": "^3.2.2",
"react-is": "^18.2.0",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"runes2": "^1.1.2",
"staged-components": "^1.1.3",
"tslib": "^2.5.0",
Expand Down Expand Up @@ -106,6 +108,7 @@
"@types/react-helmet": "^6.1.6",
"@types/react-is": "^17.0.3",
"@types/react-virtualized": "^9.21.21",
"@types/react-window": "^1.8.8",
"@types/resize-observer-browser": "^0.1.7",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/use-sync-external-store": "^0.0.3",
Expand Down
7 changes: 7 additions & 0 deletions src/components/calendar-picker-view/calendar-picker-view.less
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,11 @@
text-align: center;
}
}

&-no-scrollbar {
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
overflow-x: hidden;
overflow-y: auto;
}
}
281 changes: 154 additions & 127 deletions src/components/calendar-picker-view/calendar-picker-view.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import classNames from 'classnames'
import dayjs from 'dayjs'
import isoWeek from 'dayjs/plugin/isoWeek'
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
import isoWeek from 'dayjs/plugin/isoWeek'
import React, {
forwardRef,
ReactNode,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { FixedSizeList as List } from 'react-window'
import { NativeProps, withNativeProps } from '../../utils/native-props'
import { usePropsValue } from '../../utils/use-props-value'
import { mergeProps } from '../../utils/with-default-props'
import { useConfig } from '../config-provider'
import {
convertPageToDayjs,
convertValueToRange,
DateRange,
Page,
convertPageToDayjs,
convertValueToRange,
} from './convert'
import useSyncScroll from './useSyncScroll'

Expand Down Expand Up @@ -82,6 +83,11 @@ const defaultProps = {
selectionMode: 'single',
}

type RowProps = {
index: number
style: React.CSSProperties
}

export const CalendarPickerView = forwardRef<
CalendarPickerViewRef,
CalendarPickerViewProps
Expand Down Expand Up @@ -193,12 +199,28 @@ export const CalendarPickerView = forwardRef<
)

function renderBody() {
const cells: ReactNode[] = []
let monthIterator = minDay
// 遍历月份
const totalMonths = Math.ceil(maxDay.diff(minDay, 'months', true))
// default 每个月的高度是 344px
const monthHeight = 344
const cells: {
year: number
month: number
daysInMonth: number
monthIterator: dayjs.Dayjs
}[] = []
let monthIterator = minDay.clone()

while (monthIterator.isSameOrBefore(maxDay, 'month')) {
const year = monthIterator.year()
const month = monthIterator.month() + 1
const daysInMonth = monthIterator.daysInMonth()

cells.push({ year, month, daysInMonth, monthIterator })
monthIterator = monthIterator.add(1, 'month')
}

const Row = ({ index, style }: RowProps) => {
const { year, month, daysInMonth, monthIterator } = cells[index]

const renderMap = {
year,
Expand All @@ -212,17 +234,16 @@ export const CalendarPickerView = forwardRef<
props.weekStartsOn === 'Monday'
? monthIterator.date(1).isoWeekday() - 1
: monthIterator.date(1).isoWeekday()

const presetEmptyCells =
presetEmptyCellCount == 7
? null
: Array(presetEmptyCellCount)
.fill(null)
.map((_, index) => (
<div key={index} className={`${classPrefix}-cell`}></div>
))

cells.push(
<div key={yearMonth} data-year-month={yearMonth}>
: Array.from({ length: presetEmptyCellCount }).map((_, index) => (
<div key={index} className={`${classPrefix}-cell`}></div>
))

return (
<div style={style} key={yearMonth} data-year-month={yearMonth}>
<div className={`${classPrefix}-title`}>
{locale.Calendar.yearAndMonth?.replace(
/\${(.*?)}/g,
Expand All @@ -235,135 +256,141 @@ export const CalendarPickerView = forwardRef<
{/* 空格填充 */}
{presetEmptyCells}
{/* 遍历每月 */}
{Array(monthIterator.daysInMonth())
.fill(null)
.map((_, index) => {
const d = monthIterator.date(index + 1)
let isSelect = false
let isBegin = false
let isEnd = false
let isSelectRowBegin = false
let isSelectRowEnd = false
if (dateRange) {
const [begin, end] = dateRange
isBegin = d.isSame(begin, 'day')
isEnd = d.isSame(end, 'day')
isSelect =
isBegin ||
isEnd ||
(d.isAfter(begin, 'day') && d.isBefore(end, 'day'))
if (isSelect) {
isSelectRowBegin =
(cells.length % 7 === 0 ||
d.isSame(d.startOf('month'), 'day')) &&
!isBegin
isSelectRowEnd =
(cells.length % 7 === 6 ||
d.isSame(d.endOf('month'), 'day')) &&
!isEnd
}
{Array.from({ length: daysInMonth }).map((_, index) => {
const d = monthIterator.date(index + 1)
let isSelect = false
let isBegin = false
let isEnd = false
let isSelectRowBegin = false
let isSelectRowEnd = false
if (dateRange) {
const [begin, end] = dateRange
isBegin = d.isSame(begin, 'day')
isEnd = d.isSame(end, 'day')
isSelect =
isBegin ||
isEnd ||
(d.isAfter(begin, 'day') && d.isBefore(end, 'day'))
if (isSelect) {
isSelectRowBegin =
(cells.length % 7 === 0 ||
d.isSame(d.startOf('month'), 'day')) &&
!isBegin
isSelectRowEnd =
(cells.length % 7 === 6 ||
d.isSame(d.endOf('month'), 'day')) &&
!isEnd
}
const disabled = props.shouldDisableDate
? props.shouldDisableDate(d.toDate())
: (maxDay && d.isAfter(maxDay, 'day')) ||
(minDay && d.isBefore(minDay, 'day'))

const renderTop = () => {
const top = props.renderTop?.(d.toDate())
}
const disabled = props.shouldDisableDate
? props.shouldDisableDate(d.toDate())
: (maxDay && d.isAfter(maxDay, 'day')) ||
(minDay && d.isBefore(minDay, 'day'))

if (top) {
return top
}
const renderTop = () => {
const top = props.renderTop?.(d.toDate())

if (props.selectionMode === 'range') {
if (isBegin) {
return locale.Calendar.start
}
if (top) {
return top
}

if (isEnd) {
return locale.Calendar.end
}
if (props.selectionMode === 'range') {
if (isBegin) {
return locale.Calendar.start
}

if (d.isSame(today, 'day') && !isSelect) {
return locale.Calendar.today
if (isEnd) {
return locale.Calendar.end
}
}
return (
<div
key={d.valueOf()}
className={classNames(`${classPrefix}-cell`, {
[`${classPrefix}-cell-today`]: d.isSame(today, 'day'),
[`${classPrefix}-cell-selected`]: isSelect,
[`${classPrefix}-cell-selected-begin`]: isBegin,
[`${classPrefix}-cell-selected-end`]: isEnd,
[`${classPrefix}-cell-selected-row-begin`]:
isSelectRowBegin,
[`${classPrefix}-cell-selected-row-end`]: isSelectRowEnd,
[`${classPrefix}-cell-disabled`]: !!disabled,
})}
onClick={() => {
if (!props.selectionMode) return
if (disabled) return
const date = d.toDate()
function shouldClear() {
if (!props.allowClear) return false
if (!dateRange) return false
const [begin, end] = dateRange
return d.isSame(begin, 'date') && d.isSame(end, 'day')

if (d.isSame(today, 'day') && !isSelect) {
return locale.Calendar.today
}
}
return (
<div
key={d.valueOf()}
className={classNames(`${classPrefix}-cell`, {
[`${classPrefix}-cell-today`]: d.isSame(today, 'day'),
[`${classPrefix}-cell-selected`]: isSelect,
[`${classPrefix}-cell-selected-begin`]: isBegin,
[`${classPrefix}-cell-selected-end`]: isEnd,
[`${classPrefix}-cell-selected-row-begin`]:
isSelectRowBegin,
[`${classPrefix}-cell-selected-row-end`]: isSelectRowEnd,
[`${classPrefix}-cell-disabled`]: !!disabled,
})}
onClick={() => {
if (!props.selectionMode) return
if (disabled) return
const date = d.toDate()
function shouldClear() {
if (!props.allowClear) return false
if (!dateRange) return false
const [begin, end] = dateRange
return d.isSame(begin, 'date') && d.isSame(end, 'day')
}
if (props.selectionMode === 'single') {
if (props.allowClear && shouldClear()) {
onDateChange(null)
return
}
onDateChange([date, date])
} else if (props.selectionMode === 'range') {
if (!dateRange) {
onDateChange([date, date])
setIntermediate(true)
return
}
if (shouldClear()) {
onDateChange(null)
setIntermediate(false)
return
}
if (props.selectionMode === 'single') {
if (props.allowClear && shouldClear()) {
onDateChange(null)
return
}
if (intermediate) {
const another = dateRange[0]
onDateChange(
another > date ? [date, another] : [another, date]
)
setIntermediate(false)
} else {
onDateChange([date, date])
} else if (props.selectionMode === 'range') {
if (!dateRange) {
onDateChange([date, date])
setIntermediate(true)
return
}
if (shouldClear()) {
onDateChange(null)
setIntermediate(false)
return
}
if (intermediate) {
const another = dateRange[0]
onDateChange(
another > date ? [date, another] : [another, date]
)
setIntermediate(false)
} else {
onDateChange([date, date])
setIntermediate(true)
}
setIntermediate(true)
}
}}
>
<div className={`${classPrefix}-cell-top`}>
{renderTop()}
</div>
<div className={`${classPrefix}-cell-date`}>
{props.renderDate
? props.renderDate(d.toDate())
: d.date()}
</div>
<div className={`${classPrefix}-cell-bottom`}>
{props.renderBottom?.(d.toDate())}
</div>
}
}}
>
<div className={`${classPrefix}-cell-top`}>{renderTop()}</div>
<div className={`${classPrefix}-cell-date`}>
{props.renderDate ? props.renderDate(d.toDate()) : d.date()}
</div>
<div className={`${classPrefix}-cell-bottom`}>
{props.renderBottom?.(d.toDate())}
</div>
)
})}
</div>
)
})}
</div>
</div>
)

monthIterator = monthIterator.add(1, 'month')
}

return cells
return (
<AutoSizer>
{({ height, width }) => (
<List
height={height}
itemCount={totalMonths}
itemSize={monthHeight}
width={width}
className={`${classPrefix}-no-scrollbar`}
>
{Row}
</List>
)}
</AutoSizer>
)
}
const body = (
<div className={`${classPrefix}-body`} ref={bodyRef}>
Expand Down

0 comments on commit b8a572a

Please sign in to comment.