这是我参与「第五届青训营 」伴学笔记创作活动的第17天
需求分析
- 点击
年份按钮出现年份下拉菜单,可以快速切换年份;点击月份按钮出现月份下拉菜单,可以快速切换月份。 - 在下拉菜单中,点击
上下箭头可以切换到上/下一年/月,滑动鼠标滚轮也可以实现相同效果。 - 滚轮速度要控制在一定值。
- 点击下拉菜单中的具体年/月,会将日历切换到选择的日期并关闭下拉菜单。
- 下拉菜单展开时,点击菜单以外的地方会直接关闭下拉菜单。
完成图
文件解释
generator
新增createYears和createMonths函数用于生成具体年/月,每次生成5个数据,返回值为number[]。
在生成月份时,若已经到达1月或12月,则不再往上/下生成月份。
CalendarDropdown
下拉菜单组件,当触发鼠标滑动或箭头点击事件时,会更新具体的年/月数据。
根据事件触发时deltaY的正负控制更新方向,并采用节流的方式控制鼠标滚动速度。
高亮显示选择的日期。
MonthYearPicker
同时管理年份选择和月份选择,相当于MonthPicker和YearPicker(其实也可以拆出来)。
管理下拉菜单的显示状态。使用FocusManager管理聚焦和失焦事件,以此控制菜单的显示和隐藏。
初始化具体的年/月数据。
开发难点
chrome中的事件监听采用被动的方式,要如何防止默认滚动事件?
使用useRef+useEffect,在节点挂载时添加eventlistener,并手动设置passive: false
为什么每次触发onwheel事件,dates的值都会重置?
每次触发onwheel事件时,会调用changeDate函数,而changeDate函数会调用setDates函数来更新dates的值。由于React在更新state时是异步的,所以在onwheel事件中调用changeDate函数时,可能会出现state还没有被更新完成,就触发了下一次onwheel事件的情况,导致state值被重置。
为了解决这个问题,可以将setDates改为使用函数式更新。函数式更新可以保证更新state时使用最新的state值,从而避免出现state值被重置的问题。
重要组件实现
CalendarDropdown
const CalendarDropdown: FC<CalendarDropdownProps> = ({
items,
mode,
calendar,
selectCalendar,
setDispay,
}) => {
const [dates, setDates] = useState(items);
const divRef = useRef<HTMLDivElement>(null);
const n = items.length;
useEffect(() => {
// 解决passive event listener的问题
if (divRef.current) {
divRef.current.addEventListener('wheel', onwheel, { passive: false });
}
return () => {
// 组件卸载时移除listener
if (divRef.current) {
divRef.current.removeEventListener('wheel', onwheel);
}
};
}, []);
// 点击时重新生成
const changeDate = (step: number) => {
// 因为setState是异步的,所以必须使用函数式更新
setDates((prevDates) => {
let newDate;
if (mode === 'year') {
newDate = createYears(prevDates[Math.floor(n / 2)] + step);
} else {
newDate = createMonths(prevDates[Math.floor(n / 2)] + step);
}
return newDate;
});
};
let _timer: any = null;
const onwheel = (evt: WheelEvent) => {
evt.preventDefault();
const step = evt.deltaY > 0 ? 1 : -1; // 根据deltaY控制方向
// 节流控制速度
if (_timer) return;
_timer = setTimeout(() => {
changeDate(step);
_timer = null;
}, 60);
};
const clickDate = (date: number) => {
if (mode === 'year') {
selectCalendar({ year: date, monthIndex: calendar.monthIndex });
} else {
selectCalendar({ year: calendar.year, monthIndex: date - 1 });
}
setDispay(false);
};
return (
<div className="dropdown" ref={divRef}>
<ArrowButton icon="angle-up" onClick={() => changeDate(-1)} />
<ul>
{dates.map((date, i) => {
// 如果是当前日期,高亮显示
const isDate =
mode === 'year'
? date === calendar.year
: date - 1 === calendar.monthIndex;
const classes = classnames('dropdown-button', {
'is-date': isDate,
});
return (
<li key={i}>
<CalendarButton
className={classes}
onClick={() => clickDate(date)}
>
{date}
</CalendarButton>
</li>
);
})}
</ul>
<ArrowButton icon="angle-down" onClick={() => changeDate(1)} />
</div>
);
};
MonthYearPicker
const MonthYearPicker: FC<CalendarProps> = ({ calendar, selectCalendar }) => {
const { year, monthIndex } = calendar;
const [yearDispay, setyearDispay] = useState(false);
const [monthDispay, setmonthDispay] = useState(false);
const years = createYears(year);
const months = createMonths(monthIndex + 1);
const month = monthIndex >= 9 ? `${monthIndex + 1}` : `0${monthIndex + 1}`;
return (
<div className="month-year-picker">
<FocusManager
openPicker={() => setyearDispay(true)}
closePicker={() => setyearDispay(false)}
>
<div className="year-picker">
<CalendarButton className="picker-button">{year}</CalendarButton>
{yearDispay && (
<CalendarDropdown
items={years}
mode="year"
calendar={calendar}
selectCalendar={selectCalendar}
setDispay={setyearDispay}
/>
)}
</div>
</FocusManager>
<FocusManager
openPicker={() => setmonthDispay(true)}
closePicker={() => setmonthDispay(false)}
>
<div className="month-picker">
<CalendarButton className="picker-button">{month}</CalendarButton>
{monthDispay && (
<CalendarDropdown
items={months}
mode="month"
calendar={calendar}
selectCalendar={selectCalendar}
setDispay={setmonthDispay}
/>
)}
</div>
</FocusManager>
</div>
);
};
总结与反思
刚开始开发时没什么规划,导致后面越写乱。。。以后的开发工作要做好规划,像是要抽离几个组件、每个组件有什么功能,需要提供给哪几个组件使用等等,并以图表的形式列出来。