组件库开发日志-Calendar(2) | 青训营笔记

149 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第17天

需求分析

  • 点击年份按钮出现年份下拉菜单,可以快速切换年份;点击月份按钮出现月份下拉菜单,可以快速切换月份。
  • 在下拉菜单中,点击上下箭头可以切换到上/下一年/月,滑动鼠标滚轮也可以实现相同效果。
  • 滚轮速度要控制在一定值。
  • 点击下拉菜单中的具体年/月,会将日历切换到选择的日期并关闭下拉菜单。
  • 下拉菜单展开时,点击菜单以外的地方会直接关闭下拉菜单。
image.png image.png

完成图

文件解释

generator

新增createYearscreateMonths函数用于生成具体年/月,每次生成5个数据,返回值为number[]

在生成月份时,若已经到达1月或12月,则不再往上/下生成月份。

CalendarDropdown

下拉菜单组件,当触发鼠标滑动或箭头点击事件时,会更新具体的年/月数据。

根据事件触发时deltaY的正负控制更新方向,并采用节流的方式控制鼠标滚动速度。

高亮显示选择的日期。

MonthYearPicker

同时管理年份选择和月份选择,相当于MonthPickerYearPicker(其实也可以拆出来)。

管理下拉菜单的显示状态。使用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>
  );
};

总结与反思

刚开始开发时没什么规划,导致后面越写乱。。。以后的开发工作要做好规划,像是要抽离几个组件、每个组件有什么功能,需要提供给哪几个组件使用等等,并以图表的形式列出来。