{"version":3,"file":"SingleCombobox.cjs","sources":["../../../../src/components/Combobox/SingleCombobox/SingleCombobox.tsx"],"sourcesContent":["'use client'\n\nimport {\n  type ChangeEvent,\n  type ComponentPropsWithoutRef,\n  type KeyboardEvent,\n  type MouseEvent,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useClick } from '../../../hooks/useClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useIntl } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { UnstyledButton } from '../../Button'\nimport { FaCaretDownIcon, FaCircleXmarkIcon } from '../../Icon'\nimport { Input } from '../../Input'\nimport { useListbox } from '../useListbox'\nimport { useSingleOptions } from '../useOptions'\n\nimport type { ComboboxItem, AbstractProps as ComboboxProps } from '../types'\n\ntype AbstractProps<T> = ComboboxProps<T> & {\n  /**\n   * 選択されているアイテム\n   */\n  selectedItem: ComboboxItem<T> | null\n  /**\n   * デフォルトで選択されるアイテム\n   */\n  defaultItem?: ComboboxItem<T>\n  /**\n   * コンポーネント内の先頭に表示する内容\n   */\n  prefix?: ReactNode\n  /**\n   * 選択されているアイテムがクリアされた時に発火するコールバック関数\n   */\n  onClear?: () => void\n  /**\n   * 選択されているアイテムがクリアされた時に発火するコールバック関数\n   * 指定している場合、クリア時にonClickを実行せずにonClearClickのみ実行する\n   */\n  onClearClick?: (e: MouseEvent) => void\n  /**\n   * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n   */\n  onChangeSelected?: (selectedItem: ComboboxItem<T> | null) => void\n  /**\n   * コンポーネントがフォーカスされたときに発火するコールバック関数\n   */\n  onFocus?: () => void\n  /**\n   * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n   */\n  onBlur?: () => void\n  /**\n   * 検索結果が0件の時に表示するコンテンツ\n   */\n  noResultText?: ReactNode\n}\ntype Props<T> = AbstractProps<T> &\n  Omit<ComponentPropsWithoutRef<'input'>, keyof AbstractProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_UP_DOWN_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n  currentTarget: { value: '' },\n  target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst classNameGenerator = tv({\n  slots: {\n    wrapper: 'smarthr-ui-SingleCombobox shr-inline-block',\n    input: 'smarthr-ui-SingleCombobox-input shr-w-full',\n    caretDownLayout: [\n      'shr-relative -shr-me-0.5 shr-p-0.5',\n      'before:shr-border-0',\n      'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n    ],\n    caretDownIcon: 'shr-block',\n    clearButton: [\n      'smarthr-ui-SingleCombobox-clearButton',\n      'shr-group/clearButton',\n      'shr-me-0.5',\n      'focus-visible:shr-shadow-none',\n    ],\n    clearButtonIcon: [\n      'shr-block',\n      'group-focus-visible/clearButton:shr-focus-indicator group-focus-visible/clearButton:shr-rounded-full',\n    ],\n  },\n  variants: {\n    disabled: {\n      true: {\n        wrapper: 'shr-cursor-not-allowed',\n      },\n    },\n    hidden: {\n      true: {\n        clearButton: 'shr-hidden',\n      },\n    },\n  },\n})\n\nconst ActualSingleCombobox = <T,>(\n  {\n    items,\n    selectedItem,\n    defaultItem,\n    name,\n    disabled,\n    readOnly,\n    required,\n    prefix,\n    error,\n    creatable,\n    placeholder,\n    autoComplete,\n    dropdownHelpMessage,\n    isLoading,\n    width,\n    dropdownWidth = 'auto',\n    className,\n    onChange,\n    onChangeInput,\n    onAdd,\n    onSelect,\n    onClear,\n    onClearClick,\n    onChangeSelected,\n    onFocus,\n    onBlur,\n    onKeyPress,\n    noResultText,\n    style,\n    ...rest\n  }: Props<T>,\n  ref: Ref<HTMLInputElement>,\n) => {\n  const theme = useTheme()\n  const { localize } = useIntl()\n  const outerRef = useRef<HTMLDivElement>(null)\n  const inputRef = useRef<HTMLInputElement>(null)\n  const clearButtonRef = useRef<HTMLButtonElement>(null)\n  const [isFocused, setIsFocused] = useState(false)\n  const [isExpanded, setIsExpanded] = useState(false)\n  const [inputValue, setInputValue] = useState('')\n  const [isComposing, setIsComposing] = useState(false)\n  const [isEditing, setIsEditing] = useState(false)\n\n  useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n  const { options } = useSingleOptions({\n    items,\n    selected: selectedItem,\n    creatable,\n    inputValue,\n    isFilteringDisabled: !isEditing,\n  })\n\n  const { renderListBox, activeOption, onKeyDownListBox, listBoxId, listBoxRef } = useListbox<T>({\n    options,\n    dropdownHelpMessage,\n    dropdownWidth,\n    onAdd,\n    onSelect: useCallback(\n      (selected: ComboboxItem<T>) => {\n        onSelect?.(selected)\n        onChangeSelected?.(selected)\n\n        // 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n        onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n\n        // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n        // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n        requestAnimationFrame(() => {\n          setIsExpanded(false)\n        })\n\n        setIsEditing(false)\n      },\n      [onChangeSelected, onSelect, onChangeInput],\n    ),\n    isExpanded,\n    isLoading,\n    triggerRef: outerRef,\n    noResultText,\n  })\n\n  const selectDefaultItem = useMemo(\n    () => (onSelect && defaultItem ? () => onSelect(defaultItem) : NOOP),\n    [onSelect, defaultItem],\n  )\n\n  const focus = useCallback(() => {\n    onFocus?.()\n    inputRef.current?.focus()\n    setIsFocused(true)\n\n    if (!isFocused) {\n      setIsExpanded(true)\n    }\n  }, [onFocus, isFocused])\n  const unfocus = useCallback(() => {\n    if (!isFocused) return\n\n    onBlur?.()\n\n    setIsFocused(false)\n    setIsExpanded(false)\n    setIsEditing(false)\n\n    if (selectedItem) {\n      setInputValue(innerText(selectedItem.label))\n    } else {\n      selectDefaultItem()\n    }\n  }, [isFocused, onBlur, selectedItem, selectDefaultItem])\n  const onClickClear = useCallback(\n    (e: MouseEvent) => {\n      e.stopPropagation()\n\n      let isExecutedPreventDefault = false\n\n      onClearClick?.({\n        ...e,\n        preventDefault: () => {\n          e.preventDefault()\n          isExecutedPreventDefault = true\n        },\n      })\n\n      if (!isExecutedPreventDefault) {\n        onClear?.()\n        onChangeSelected?.(null)\n\n        inputRef.current?.focus()\n\n        setIsFocused(true)\n        setIsExpanded(true)\n      }\n    },\n    [onClearClick, onClear, onChangeSelected],\n  )\n  const onClickInput = useCallback(\n    (e: MouseEvent) => {\n      if (disabled || readOnly) {\n        e.stopPropagation()\n\n        return\n      }\n\n      inputRef.current?.focus()\n\n      if (!isExpanded) {\n        setIsExpanded(true)\n      }\n    },\n    [disabled, readOnly, inputRef, isExpanded],\n  )\n  const onDelegateClickIcon = onClickInput\n  const actualOnChangeInput = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      onChange?.(e)\n      onChangeInput?.(e)\n\n      if (!isEditing) setIsEditing(true)\n\n      const { value } = e.currentTarget\n\n      setInputValue(value)\n\n      if (value === '') {\n        onClear?.()\n        onChangeSelected?.(null)\n      }\n    },\n    [isEditing, onChange, onChangeInput, onClear, onChangeSelected],\n  )\n  const onCompositionStart = useCallback(() => setIsComposing(true), [])\n  const onCompositionEnd = useCallback(() => setIsComposing(false), [])\n  const onKeyDownInput = useCallback(\n    (e: KeyboardEvent<HTMLInputElement>) => {\n      if (isComposing) {\n        return\n      }\n\n      if (ESCAPE_KEY_REGEX.test(e.key)) {\n        if (isExpanded) {\n          e.stopPropagation()\n          setIsExpanded(false)\n        }\n      } else if (e.key === 'Tab') {\n        unfocus()\n      } else {\n        if (ARROW_UP_DOWN_REGEX.test(e.key)) {\n          e.preventDefault()\n        }\n\n        inputRef.current?.focus()\n\n        if (!isExpanded) {\n          setIsExpanded(true)\n        }\n      }\n      onKeyDownListBox(e)\n    },\n    [isComposing, isExpanded, unfocus, onKeyDownListBox],\n  )\n\n  // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n  // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n  // submitイベントが発生し、formが送信される場合がある\n  const handleKeyPress = useCallback(\n    (e: KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === 'Enter') e.preventDefault()\n\n      onKeyPress?.(e)\n    },\n    [onKeyPress],\n  )\n\n  const caretIconColor = useMemo(() => {\n    if (isFocused) return theme.textColor.black\n    if (disabled || readOnly) return theme.textColor.disabled\n\n    return theme.textColor.grey\n  }, [disabled, readOnly, isFocused, theme.textColor])\n\n  useClick(\n    useMemo(() => [outerRef, listBoxRef, clearButtonRef], [outerRef, listBoxRef, clearButtonRef]),\n    isFocused || selectedItem ? NOOP : selectDefaultItem,\n    unfocus,\n  )\n\n  // selectedItem.label はプリミティブ値でないデータ型の可能性があり、そのまま useEffect の依存配列に入れると意図せぬエフェクトの実行を引き起こしてしまう可能性があるので、プリミティブ値である string 型に変換したものを依存配列に入れています。\n  const selectedItemLabelText = innerText(selectedItem?.label)\n  useEffect(() => {\n    setInputValue(selectedItemLabelText)\n  }, [selectedItemLabelText])\n\n  const wrapperStyle = useMemo(\n    () => ({\n      ...style,\n      width: typeof width === 'number' ? `${width}px` : width,\n    }),\n    [style, width],\n  )\n\n  const notSelected = selectedItem === null\n\n  const classNames = useMemo(() => {\n    const { wrapper, input, caretDownLayout, caretDownIcon, clearButton, clearButtonIcon } =\n      classNameGenerator()\n\n    return {\n      wrapper: wrapper({ disabled, className }),\n      input: input(),\n      caretDownLayout: caretDownLayout(),\n      caretDownIcon: caretDownIcon(),\n      clearButton: clearButton({ hidden: notSelected || disabled || readOnly }),\n      clearButtonIcon: clearButtonIcon(),\n    }\n  }, [notSelected, disabled, readOnly, className])\n\n  const destroyButtonIconAlt = useMemo(\n    () =>\n      localize({\n        id: 'smarthr-ui/SingleCombobox/destroyButtonIconAlt',\n        defaultText: 'クリア',\n      }),\n    [localize],\n  )\n\n  return (\n    <div role=\"group\" className={classNames.wrapper} style={wrapperStyle} ref={outerRef}>\n      <Input\n        {...rest}\n        ref={inputRef}\n        type=\"text\"\n        role=\"combobox\"\n        name={name}\n        value={inputValue}\n        disabled={disabled}\n        readOnly={readOnly}\n        required={required}\n        autoComplete={autoComplete ?? 'off'}\n        aria-haspopup=\"listbox\"\n        aria-controls={listBoxId}\n        aria-expanded={isFocused}\n        aria-activedescendant={activeOption?.id}\n        aria-autocomplete=\"list\"\n        /* eslint-disable-next-line smarthr/a11y-prohibit-input-placeholder */\n        placeholder={placeholder}\n        onClick={onClickInput}\n        onChange={actualOnChangeInput}\n        onFocus={isFocused ? undefined : focus}\n        onCompositionStart={onCompositionStart}\n        onCompositionEnd={onCompositionEnd}\n        onKeyDown={onKeyDownInput}\n        onKeyPress={handleKeyPress}\n        error={error}\n        prefix={prefix}\n        suffix={\n          <>\n            <UnstyledButton\n              onClick={onClickClear}\n              ref={clearButtonRef}\n              className={classNames.clearButton}\n            >\n              <FaCircleXmarkIcon\n                color=\"TEXT_BLACK\"\n                alt={destroyButtonIconAlt}\n                className={classNames.clearButtonIcon}\n              />\n            </UnstyledButton>\n            <span\n              role=\"presentation\"\n              onClick={onDelegateClickIcon}\n              className={classNames.caretDownLayout}\n            >\n              <FaCaretDownIcon color={caretIconColor} className={classNames.caretDownIcon} />\n            </span>\n          </>\n        }\n        className={classNames.input}\n        data-smarthr-ui-input=\"true\"\n      />\n      {!readOnly && renderListBox()}\n    </div>\n  )\n}\n\nexport const SingleCombobox = genericsForwardRef(ActualSingleCombobox)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyEA;AAEA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;AACA;AACA;;;;AAIC;AACD;AACA;;;;;AAKC;AACD;;;AAGC;AACF;AACD;AACE;AACE;AACE;AACD;AACF;AACD;AACE;AACE;AACD;AACF;AACF;AACF;AAED;AAmCE;AACA;AACA;AACA;AACA;;;;;;;AASA;;AAEE;;;;AAID;AAED;;;;;AAKE;AAEI;AACA;;AAGA;;;;;AAMA;;;;;AAQJ;;AAED;AAED;AAKA;;AAEE;;;;;AAMF;AACA;AACE;;;;;;;;;;AAWE;;;AAGJ;;;AAMI;AACE;;;;;AAKD;;;AAIC;AAEA;;;;;AAQN;AAEI;;;;AAMA;;;;;;AASJ;AAEI;AACA;AAEA;;AAEA;;AAIA;;AAEE;;AAEJ;AAGF;AACA;AACA;;;;;;;;;;AAWW;AACL;;;;;;AAMA;;;;;;;;;;AAcN;AAEI;;AAEA;AACF;AAIF;AACE;AAAe;;AACW;AAE1B;AACF;AAEA;;;;;AAUA;AAEA;AAEI;AACA;AACD;AAIH;AAEA;AACE;;;;;;AAQE;;;;;AAQE;AACA;AACD;AAIL;;AAmBM;AAsCR;;;"}