{"version":3,"file":"DatePicker.cjs","sources":["../../../src/components/DatePicker/DatePicker.tsx"],"sourcesContent":["'use client'\n\nimport dayjs from 'dayjs'\nimport {\n  type ChangeEvent,\n  type ComponentProps,\n  type ComponentPropsWithRef,\n  type FocusEventHandler,\n  type MouseEvent,\n  type ReactNode,\n  forwardRef,\n  memo,\n  useCallback,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useOuterClick } from '../../hooks/useOuterClick'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Calendar } from '../Calendar'\nimport { FaCalendarDaysIcon } from '../Icon'\nimport { Input } from '../Input'\n\nimport { Portal } from './Portal'\nimport { parseJpnDateString } from './datePickerHelper'\nimport { useGlobalKeyDown } from './useGlobalKeyDown'\n\ntype ChangeLikeEvent = ChangeEvent | React.KeyboardEvent | MouseEvent\ntype AbstractProps = {\n  /** input 要素の `value` 属性の値 */\n  value?: string | null\n  /** input 要素の `name` 属性の値 */\n  name?: string\n  /** 選択可能な期間の開始日 */\n  from?: Date\n  /** 選択可能な期間の終了日 */\n  to?: Date\n  /** フォームを無効にするかどうか */\n  disabled?: boolean\n  /**\n   * placeholder属性は非推奨です。別途ヒント用要素を設置するか、それらの領域を確保出来ない場合はTooltipコンポーネントの利用を検討してください。\n   */\n  placeholder?: string\n  /** フォームにエラーがあるかどうか */\n  error?: boolean\n  /** コンポーネントの幅 */\n  width?: number | string\n  /** 入力を独自にパースする場合に、パース処理を記述する関数 */\n  parseInput?: (input: string) => Date | null\n  /** 表示する日付を独自にフォーマットする場合に、フォーマット処理を記述する関数 */\n  formatDate?: (date: Date | null) => string\n  /** 入出力用文字列と併記する別フォーマット処理を記述する関数 */\n  showAlternative?: (date: Date | null) => ReactNode\n  /** @deprecated onChangeDate は非推奨です。onChange を使ってください。 */\n  onChangeDate?: (date: Date | null, value: string, other: { errors: string[] }) => void\n  /** 選択された日付が変わった時に発火するコールバック関数 */\n  onChange?: (\n    e: ChangeEvent<HTMLInputElement>,\n    other: { date: Date | null; formatValue: string; errors: string[] },\n  ) => void\n}\ntype Props = AbstractProps &\n  Omit<\n    ComponentPropsWithRef<'input'>,\n    | keyof AbstractProps\n    | 'type'\n    | 'onChange'\n    | 'onKeyPress'\n    | 'onFocus'\n    | 'aria-expanded'\n    | 'aria-controls'\n    | 'aria-haspopup'\n  >\n\nexport const DEFAULT_FROM = new Date(1900, 0, 1)\n\nconst classNameGenerator = tv({\n  slots: {\n    container: 'smarthr-ui-DatePicker shr-inline-block',\n    inputSuffixLayout: 'shr-box-border shr-h-full shr-py-0.5',\n    inputSuffixWrapper:\n      'shr-border-l-shorthand shr-box-border shr-flex shr-h-full shr-items-center shr-justify-center shr-ps-0.5 shr-text-base',\n    inputSuffixText: 'shr-text-gray shr-me-0.5 shr-text-sm',\n  },\n})\n\nconst DEFAULT_DATE_TO_STRING_FORMAT = 'YYYY/MM/DD'\nconst DEFAULT_DATE_TO_STRING = (d: Date | null) =>\n  d ? dayjs(d).format(DEFAULT_DATE_TO_STRING_FORMAT) : ''\nconst RETURN_NULL = () => null\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\n\n/** @deprecated DatePicker は非推奨です。Input[type=date] を使ってください。 */\nexport const DatePicker = forwardRef<HTMLInputElement, Props>(\n  (\n    {\n      value,\n      name,\n      from = DEFAULT_FROM,\n      to,\n      disabled,\n      width,\n      error,\n      className,\n      parseInput,\n      formatDate,\n      showAlternative,\n      onChangeDate,\n      onChange,\n      onBlur,\n      ...rest\n    },\n    ref,\n  ) => {\n    const theme = useTheme()\n    const containerStyle = useMemo(\n      () => ({\n        width: typeof width === 'number' ? `${width}px` : width,\n      }),\n      [width],\n    )\n    const classNames = useMemo(() => {\n      const { container, inputSuffixLayout, inputSuffixWrapper, inputSuffixText } =\n        classNameGenerator()\n\n      return {\n        container: container({ className }),\n        inputSuffixLayout: inputSuffixLayout(),\n        inputSuffixWrapper: inputSuffixWrapper(),\n        inputSuffixText: inputSuffixText(),\n      }\n    }, [className])\n\n    const stringToDate = useMemo(\n      () =>\n        parseInput\n          ? (str?: string | null) => (str ? parseInput(str) : null)\n          : (str?: string | null) => (str ? parseJpnDateString(str) : null),\n      [parseInput],\n    )\n\n    const dateToString = useMemo(() => formatDate || DEFAULT_DATE_TO_STRING, [formatDate])\n    const dateToAlternativeFormat = useMemo(\n      () => (showAlternative ? (d: Date | null) => (d ? showAlternative(d) : null) : RETURN_NULL),\n      [showAlternative],\n    )\n\n    const [selectedDate, setSelectedDate] = useState<Date | null>(stringToDate(value))\n    const inputRef = useRef<HTMLInputElement>(null)\n    const inputWrapperRef = useRef<HTMLDivElement>(null)\n    const calendarPortalRef = useRef<HTMLDivElement>(null)\n    const [inputRect, setInputRect] = useState<DOMRect | null>(null)\n    const [isInputFocused, setIsInputFocused] = useState(false)\n    const [isCalendarShown, setIsCalendarShown] = useState(false)\n    const [alternativeFormat, setAlternativeFormat] = useState<null | ReactNode>(null)\n    const calenderId = useId()\n\n    useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(\n      ref,\n      () => inputRef.current,\n    )\n\n    const baseUpdateDate = useCallback(\n      (newDate: Date | null) => {\n        if (\n          !inputRef.current ||\n          newDate === selectedDate ||\n          (newDate && selectedDate && newDate.getTime() === selectedDate.getTime())\n        ) {\n          // Do not update date if the new date is same with the old one.\n          return\n        }\n\n        const isValid = !newDate || dayjs(newDate).isValid()\n        const errors: string[] = []\n\n        if (!isValid) {\n          errors.push('INVALID_DATE')\n        }\n\n        const nextDate = isValid ? newDate : null\n        const formatValue = dateToString(nextDate)\n\n        inputRef.current.value = formatValue\n        setAlternativeFormat(dateToAlternativeFormat(nextDate))\n        setSelectedDate(nextDate)\n\n        return [nextDate, formatValue, errors] as [Date | null, string, string[]]\n      },\n      [selectedDate, dateToString, dateToAlternativeFormat],\n    )\n    const updateDate = useMemo(() => {\n      if (onChange) {\n        return (e: ChangeLikeEvent, newDate: Date | null) => {\n          const result = baseUpdateDate(newDate)\n\n          if (result) {\n            e.preventDefault()\n            e.stopPropagation()\n\n            const [nextDate, formatValue, errors] = result\n            const event = new Event('change', { bubbles: true })\n            // HINT: resultが存在する時点でinputRef.currentは必ず存在する\n            const input = inputRef.current as HTMLInputElement\n\n            input.dispatchEvent(event)\n            onChange(\n              // HINT: 型問題のため別途オブジェクトをイベントに見立てる\n              {\n                stopPropagation: () => {\n                  event.stopPropagation()\n                },\n                preventDefault: () => {\n                  event.preventDefault()\n                },\n                target: input,\n                currentTarget: input,\n              } as ChangeEvent<HTMLInputElement>,\n              { date: nextDate, formatValue, errors },\n            )\n          }\n        }\n      }\n\n      return onChangeDate\n        ? (_e: ChangeLikeEvent, newDate: Date | null) => {\n            const result = baseUpdateDate(newDate)\n\n            if (result) {\n              const [nextDate, formatValue, errors] = result\n\n              onChangeDate(nextDate, formatValue, { errors })\n            }\n          }\n        : (_e: ChangeLikeEvent, newDate: Date | null) => {\n            baseUpdateDate(newDate)\n          }\n    }, [onChange, onChangeDate, baseUpdateDate])\n\n    const closeCalendar = useCallback(() => setIsCalendarShown(false), [])\n    const openCalendar = useCallback(() => {\n      if (inputWrapperRef.current) {\n        setIsCalendarShown(true)\n        setInputRect(inputWrapperRef.current.getBoundingClientRect())\n      }\n    }, [])\n\n    useEffect(() => {\n      if (value === undefined || !inputRef.current) {\n        return\n      }\n\n      /**\n       * Do not format the given value in the following cases\n       * - while input element is focused.\n       * - if the given value is not date formattable.\n       */\n      if (!isInputFocused) {\n        const newDate = stringToDate(value)\n\n        if (newDate && dayjs(newDate).isValid()) {\n          inputRef.current.value = dateToString(newDate)\n          setAlternativeFormat(dateToAlternativeFormat(newDate))\n          setSelectedDate(newDate)\n\n          return\n        }\n\n        setSelectedDate(null)\n      }\n\n      inputRef.current.value = value || ''\n    }, [value, isInputFocused, dateToString, dateToAlternativeFormat, stringToDate])\n\n    useOuterClick(\n      useMemo(() => [inputWrapperRef, calendarPortalRef], [inputWrapperRef, calendarPortalRef]),\n      closeCalendar,\n    )\n\n    const handleKeyDown = useCallback(\n      (e: KeyboardEvent) => {\n        if (!inputRef.current || !calendarPortalRef.current || e.key !== 'Tab') {\n          return\n        }\n\n        const calendarButtons = calendarPortalRef.current.querySelectorAll('button')\n\n        if (calendarButtons.length === 0) {\n          return\n        }\n\n        const firstCalendarButton = calendarButtons[0]\n\n        if (isInputFocused) {\n          if (e.shiftKey) {\n            // move focus from Input to previous elements of DatePicker\n            closeCalendar()\n\n            return\n          }\n\n          // move focus from Input to Calendar\n          e.preventDefault()\n          firstCalendarButton.focus()\n\n          return\n        }\n\n        const calendarButtonAry = Array.from(calendarButtons)\n        const currentFocused = calendarButtonAry.find((button) => button === e.target)\n\n        if (e.shiftKey) {\n          if (currentFocused === firstCalendarButton) {\n            // move focus from Calendar to Input\n            inputRef.current.focus()\n            e.preventDefault()\n          }\n        } else if (currentFocused === calendarButtonAry.at(-1)) {\n          // move focus from Calendar to next elements of DatePicker\n          inputRef.current.focus()\n          closeCalendar()\n        }\n      },\n      [isInputFocused, closeCalendar],\n    )\n\n    const baseHandleBlur = useCallback<FocusEventHandler<HTMLInputElement>>(\n      (e) => {\n        setIsInputFocused(false)\n        updateDate(e, e.target.value ? stringToDate(e.target.value) : null)\n      },\n      [stringToDate, updateDate],\n    )\n    const handleBlur = useMemo<FocusEventHandler<HTMLInputElement>>(\n      () =>\n        onBlur\n          ? (e) => {\n              baseHandleBlur(e)\n              onBlur(e)\n            }\n          : baseHandleBlur,\n      [onBlur, baseHandleBlur],\n    )\n\n    useGlobalKeyDown(handleKeyDown)\n\n    const caretIconColor = useMemo(() => {\n      if (isInputFocused || isCalendarShown) return theme.textColor.black\n      if (disabled) return theme.textColor.disabled\n\n      return theme.textColor.grey\n    }, [isInputFocused, isCalendarShown, disabled, theme.textColor])\n\n    const onDelegateKeyDown = useMemo(() => {\n      if (!isCalendarShown) {\n        return undefined\n      }\n\n      return (e: React.KeyboardEvent<HTMLInputElement>) => {\n        if (ESCAPE_KEY_REGEX.test(e.key)) {\n          e.stopPropagation()\n          // delay hiding calendar because calendar will be displayed when input is focused\n          requestAnimationFrame(closeCalendar)\n\n          if (inputRef.current) inputRef.current.focus()\n        }\n      }\n    }, [isCalendarShown, closeCalendar])\n    const onKeyPressInput = useCallback(\n      (e: React.KeyboardEvent<HTMLInputElement>) => {\n        if (e.key === 'Enter') {\n          ;(isCalendarShown ? openCalendar : closeCalendar)()\n          updateDate(e, stringToDate(e.currentTarget.value))\n        }\n      },\n      [isCalendarShown, updateDate, closeCalendar, openCalendar, stringToDate],\n    )\n    const onFocusInput = useCallback(() => {\n      setIsInputFocused(true)\n      openCalendar()\n    }, [openCalendar])\n    const onSelectDateCalendar = useCallback(\n      (e: ChangeLikeEvent, selected: Date | null) => {\n        updateDate(e, selected)\n        // delay hiding calendar because calendar will be displayed when input is focused\n        requestAnimationFrame(closeCalendar)\n\n        if (inputRef.current) inputRef.current.focus()\n      },\n      [updateDate, closeCalendar],\n    )\n    const onDelegateClick = !isCalendarShown && !disabled ? openCalendar : undefined\n\n    return (\n      <div\n        onClick={onDelegateClick}\n        onKeyDown={onDelegateKeyDown}\n        role=\"presentation\"\n        className={classNames.container}\n        style={containerStyle}\n      >\n        <div ref={inputWrapperRef}>\n          <Input\n            {...rest}\n            data-smarthr-ui-input=\"true\"\n            width=\"100%\"\n            name={name}\n            onChange={isCalendarShown ? closeCalendar : undefined}\n            onKeyPress={onKeyPressInput}\n            onFocus={onFocusInput}\n            onBlur={handleBlur}\n            suffix={\n              <InputSuffixIcon\n                alternativeFormat={showAlternative ? alternativeFormat : null}\n                caretIconColor={caretIconColor}\n                classNames={classNames}\n              />\n            }\n            disabled={disabled}\n            error={error}\n            ref={inputRef}\n            className=\"smarthr-ui-DatePicker-inputContainer\"\n            aria-expanded={isCalendarShown}\n            aria-controls={calenderId}\n            aria-haspopup={true}\n          />\n        </div>\n        {isCalendarShown && inputRect && (\n          <Portal inputRect={inputRect} ref={calendarPortalRef}>\n            <Calendar\n              id={calenderId}\n              value={selectedDate || undefined}\n              from={from}\n              to={to}\n              onSelectDate={onSelectDateCalendar}\n            />\n          </Portal>\n        )}\n      </div>\n    )\n  },\n)\n\nconst InputSuffixIcon = memo<{\n  classNames: { inputSuffixLayout: string; inputSuffixWrapper: string; inputSuffixText: string }\n  alternativeFormat: null | ReactNode\n  caretIconColor: ComponentProps<typeof FaCalendarDaysIcon>['color']\n}>(({ classNames, alternativeFormat, caretIconColor }) => (\n  <span className={classNames.inputSuffixLayout}>\n    <span className={classNames.inputSuffixWrapper}>\n      {alternativeFormat && <span className={classNames.inputSuffixText}>{alternativeFormat}</span>}\n      <FaCalendarDaysIcon color={caretIconColor} />\n    </span>\n  </span>\n))\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AA+EO;AAEP;AACE;AACE;AACA;AACA;AAEA;AACD;AACF;AAED;AACA;AAEA;AACA;AAEA;;AAsBI;AACA;AAEI;AACD;AAGH;AACE;;AAIE;;;;;AAKJ;AAEA;AAGM;;AAKN;AACA;AAKA;AACA;AACA;AACA;;;;;AAKA;;AAOA;;AAIM;AACA;;;;AAMF;;;AAIE;;;AAIF;AAEA;AACA;;AAGA;;AAIJ;;AAEI;AACE;;;;;AAOE;;AAEA;AAEA;;;AAGE;;;;;;;AAOE;AACA;;;AAKR;;AAGF;AACE;AACI;;;;;;AAQJ;;AAEE;;AAGN;AACA;AACE;;;;;;;;;AAWA;;;;AAIG;;AAED;;;AAIE;;;;;;;AAUN;;AAOA;AAEI;;;;AAMA;;;AAIA;;AAGE;;AAEE;;;;;;;;;AAaJ;AAEA;AACE;;AAEE;;;;;;AAKF;AACA;;AAEJ;AAIF;;;AAIE;AAGF;AAGM;;;;;;AAUN;;AACyC;AACvC;AAAc;AAEd;AACF;AAEA;;AAEI;;;;;;;;AASwB;;AAE1B;AACF;AACA;AAEI;;AAEE;;AAEJ;AAGF;;AAEE;AACF;;AAGI;;;;AAIsB;AACxB;AAGF;AAEA;AA+CF;AAGF;;;"}