iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0
Modern Web

【 我不會寫 React Component 】系列 第 15

如何製作日期選擇 Date Picker 3【 date picker | 我不會寫 React Component 】

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20220928/20107239f8eOuictHa.jpg

hashtags: #react, #components, #accessibility, #datepicker

本篇接續前篇 如何製作日期選擇 Date Picker 2【 我不會寫 React Component 】
可以先看完上一篇在接續此篇。

Spec: Choose Date Button

按下 Space 或是 Enter 時,
會打開日期選擇對話視窗。

將焦點對焦到用戶選擇的日期上,
如果當前還沒選擇日期則對焦到當天日期。

describe("choose date button", () => {
  describe("space", () => {
    it("open the date picker dialog", async () => {
      setup(new Date(0));
      screen.getByRole("button").focus();
      await user.keyboard("{Space}");
      expect(screen.queryByRole("dialog")).toBeInTheDocument();
    });

    it("move focus to selected date, i.e., the date displayed in the date input text field", async () => {
      setup(new Date(0));
      screen.getByRole("button").focus();
      await user.keyboard("{Space}");
      expect(screen.queryByText("01")).toHaveFocus();
    });

    it("no date has been selected, places focus on the current date.", async () => {
      setup();
      screen.getByRole("button").focus();
      await user.keyboard("{Space}");
      expect(screen.queryByText(new Date().getDate())).toHaveFocus();
    });
  });

  describe("enter", () => {
    it("open the date picker dialog", async () => {
      setup(new Date(0));
      screen.getByRole("button").focus();
      await user.keyboard("{Enter}");
      expect(screen.queryByRole("dialog")).toBeInTheDocument();
    });

    it("move focus to selected date, i.e., the date displayed in the date input text field", async () => {
      setup(new Date(0));
      screen.getByRole("button").focus();
      await user.keyboard("{Enter}");
      expect(screen.queryByText("01")).toHaveFocus();
    });

    it("no date has been selected, places focus on the current date.", async () => {
      setup();
      screen.getByRole("button").focus();
      await user.keyboard("{Enter}");
      expect(screen.queryByText(new Date().getDate())).toHaveFocus();
    });
  });
});

Solution

瀏覽器預設,當用戶按下 Enter 時,button 會被點擊,
但 Space 還是需要我們自己實作。

const Button = forwardRef<HTMLButtonElement, ButtonProps>((_props, ref) => {
  const [state, dispatch] = useDatePickerContext(
    `<DatePicker.Button /> cannot be rendered outside <DatePicker />`
  );

  const { action, children, ...props } = _props;

  const onClick = () => dispatch(action);

  let element: ReactNode | null = null;
  if (typeof children === "function") {
    element ??= children(state);
  } else {
    element ??= children;
  }

  if (action.type === "select date") {
    const isSelected = state.value && isSameDay(action.value, state.value);

    return (
      <button
        {...props}
        type="button"
        onClick={onClick}
        ref={ref}
        aria-selected={isSelected}
      >
        {element}
      </button>
    );
  }

  const onKeyDown = (event: KeyboardEvent) => {
    if (event.key === "Space") {
      event.preventDefault();
      return dispatch(action);
    }
  };

  return (
    <button
      {...props}
      type="button"
      onClick={onClick}
      onKeyDown={onKeyDown}
      ref={ref}
    >
      {element}
    </button>
  );
});

Spec: Close Dialog

當用戶按下 Esc 時,
必須關閉 dialog 並對焦於 choose date 按鈕上。

describe("esc", () => {
  it('closes the dialog and returns focus to the "choose date" button', async () => {
    setup();
    screen.getByRole("button", { name: "choose date" }).focus();
    await user.keyboard("{Enter}");
    expect(screen.queryByRole("dialog")).toBeInTheDocument();
    await user.keyboard("{Escape}");
    expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
    expect(screen.getByRole("button", { name: "choose date" })).toHaveFocus();
  });
});

Solution

因為我們在 如何製作對話視窗 interaction【 dialog | 我不會寫 React Component 】 已經實作這項規格,
我們只要記得將 previousFocusRef 拋進 Dialog 就行了。

function TestCalendar(props: {
  value?: Date;
  onDismiss: () => void;
  previousFocusRef: RefObject<HTMLElement>;
}) {
  const ref = useRef<HTMLTableCellElement>(null);
  const [focusWithinGrid, setFocusWithinGrid] = useState(false);

  return (
    <Calendar
      value={props.value}
      as={Dialog}
      initialFocusRef={ref}
      previousFocusRef={props.previousFocusRef}
      aria-label="Choose Date"
      onDismiss={props.onDismiss}
    >
      <Calendar.Header>
        <Calendar.Title />
        <Calendar.Button action="previous month" />
        <Calendar.Button action="next month" />
        <Calendar.Button action="previous year" />
        <Calendar.Button action="next year" />
      </Calendar.Header>

      <MonthCalendar.Grid
        onFocusCapture={(event) =>
          setFocusWithinGrid(
            event.currentTarget.contains(document.activeElement)
          )
        }
        onBlurCapture={() => setFocusWithinGrid(false)}
      >
        <MonthCalendar.ColumnHeader />

        <MonthCalendar.GridCell ref={ref}>
          {(date) => (
            <DatePicker.Button action={{ type: "select date", value: date }}>
              {format(date, "dd")}
            </DatePicker.Button>
          )}
        </MonthCalendar.GridCell>
      </MonthCalendar.Grid>

      <span aria-live="polite">
        {focusWithinGrid && "Cursor keys can navigate dates"}
      </span>
    </Calendar>
  );
}

function TestDatePicker(props: { value?: Date }) {
  const previousFocus = useRef<HTMLButtonElement>(null);
  return (
    <DatePicker value={props.value}>
      <DatePicker.Field />
      <DatePicker.Button
        action={{ type: "trigger calendar" }}
        ref={previousFocus}
      >
        {({ value }) =>
          value ? `change date, ${format(value, "MM/dd/yyyy")}` : "choose date"
        }
      </DatePicker.Button>

      <DatePicker.Description>(date format: mm/dd/yyyy)</DatePicker.Description>

      {([{ open, value }, dispatch]) =>
        open && (
          <TestCalendar
            value={value}
            previousFocusRef={previousFocus}
            onDismiss={() => dispatch({ type: "close calendar" })}
          />
        )
      }
    </DatePicker>
  );
}

名詞對照

中文 英文
日期選擇 date picker
對話視窗 dialog

上一篇
如何製作日期選擇 Date Picker 2【 date picker | 我不會寫 React Component 】
下一篇
如何製作日期選擇 Date Picker 4【 date picker | 我不會寫 React Component 】
系列文
【 我不會寫 React Component 】30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言