tutorbookapp/tutorbook

View on GitHub
components/time-select/index.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import { MenuSurface, MenuSurfaceAnchor } from '@rmwc/menu';
import {
  SyntheticEvent,
  memo,
  useCallback,
  useEffect,
  useRef,
  useState,
} from 'react';
import { TextField, TextFieldHTMLProps, TextFieldProps } from '@rmwc/textfield';
import { dequal } from 'dequal/lite';
import { nanoid } from 'nanoid';
import useTranslation from 'next-translate/useTranslation';

import { TCallback } from 'lib/model/callback';
import { Timeslot } from 'lib/model/timeslot';
import { useClickContext } from 'lib/hooks/click-outside';
import { useUser } from 'lib/context/user';

import SelectSurface from './select-surface';
import styles from './time-select.module.scss';

interface Props {
  uid?: string;
  value?: Timeslot;
  onChange: TCallback<Timeslot>;
  renderToPortal?: boolean;
  focused?: boolean;
  onFocused?: () => any;
  onBlurred?: () => any;
  className?: string;
}

type Overrides =
  | keyof Props
  | 'textarea'
  | 'readOnly'
  | 'onFocus'
  | 'onBlur'
  | 'inputRef'
  | 'className';

export type TimeSelectProps = Omit<TextFieldHTMLProps, Overrides> &
  Omit<TextFieldProps, Overrides> &
  Props;

function TimeSelect({
  uid,
  value,
  onChange,
  renderToPortal,
  focused,
  onFocused,
  onBlurred,
  className,
  ...textFieldProps
}: TimeSelectProps): JSX.Element {
  const inputRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    if (focused) inputRef.current?.focus();
  }, [focused]);

  // We use `setTimeout` and `clearTimeout` to wait a "tick" on a blur event
  // before toggling which ensures the user hasn't re-opened the menu.
  // @see {@link https://bit.ly/2x9eM27}
  const [menuOpen, setMenuOpen] = useState<boolean>(false);
  const timeoutId = useRef<ReturnType<typeof setTimeout>>();

  const { user } = useUser();
  const { lang: locale } = useTranslation();
  const { updateEl, removeEl } = useClickContext();

  const elementId = useRef<string>(`time-select-${nanoid()}`);
  const menuSurfaceRef = useCallback(
    (node: HTMLElement | null) => {
      if (!node) return removeEl(elementId.current);
      return updateEl(elementId.current, node);
    },
    [updateEl, removeEl]
  );

  return (
    <MenuSurfaceAnchor className={className}>
      <MenuSurface
        tabIndex={-1}
        open={menuOpen}
        ref={menuSurfaceRef}
        onFocus={(event: SyntheticEvent<HTMLDivElement>) => {
          event.preventDefault();
          event.stopPropagation();
          inputRef.current?.focus();
        }}
        className={styles.surface}
        anchorCorner='bottomStart'
        renderToPortal={renderToPortal ? '#portal' : false}
        data-cy='time-select-surface'
      >
        <SelectSurface uid={uid} onChange={onChange} inputRef={inputRef} />
      </MenuSurface>
      <TextField
        {...textFieldProps}
        readOnly
        textarea={false}
        inputRef={inputRef}
        value={value?.toString(locale, user.timezone) || ''}
        className={styles.field}
        onFocus={() => {
          if (onFocused) onFocused();
          if (timeoutId.current) {
            clearTimeout(timeoutId.current);
            timeoutId.current = undefined;
          }
          setMenuOpen(true);
        }}
        onBlur={() => {
          if (onBlurred) onBlurred();
          timeoutId.current = setTimeout(() => setMenuOpen(false), 0);
        }}
      />
    </MenuSurfaceAnchor>
  );
}

export default memo(TimeSelect, dequal);