{"version":3,"file":"MultiCombobox.cjs","sources":["../../../../src/components/Combobox/MultiCombobox/MultiCombobox.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  memo,\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { useId } from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useOuterClick } from '../../../hooks/useOuterClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useIntl } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { FaCaretDownIcon } from '../../Icon'\nimport { Scroller } from '../../Scroller'\nimport { areItemsEqual } from '../helper'\nimport { useFocusControl } from '../useFocusControl'\nimport { useListbox } from '../useListbox'\nimport { useMultiOptions } from '../useOptions'\n\nimport { MultiSelectedItem } from './MultiSelectedItem'\n\nimport type { ComboboxItem, AbstractProps as ComboboxProps } from '../types'\n\ntype AbstractProps<T> = ComboboxProps<T> & {\n  /**\n   * 選択されているアイテムのリスト\n   */\n  selectedItems: Array<ComboboxItem<T> & { deletable?: boolean }>\n  /**\n   * 選択されているアイテムのラベルを省略表示するかどうか\n   */\n  selectedItemEllipsis?: boolean\n  /**\n   * テキストボックスの `value` 属性の値。\n   * `onChangeInput` と併せて設定することで、テキストボックスの挙動が制御可能になる。\n   */\n  inputValue?: string\n  /**\n   * 選択されているアイテムの削除ボタンがクリックされた時に発火するコールバック関数\n   */\n  onDelete?: (item: ComboboxItem<T>) => void\n  /**\n   * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n   */\n  onChangeSelected?: (selectedItems: Array<ComboboxItem<T>>) => void\n  /**\n   * コンポーネントがフォーカスされたときに発火するコールバック関数\n   */\n  onFocus?: () => void\n  /**\n   * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n   */\n  onBlur?: () => void\n  /**\n   * アイテムが選択されたときに選択済みかどうかを判定するコールバック関数/\n   */\n  isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\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 preventDefaultWithPressEnter = (e: KeyboardEvent<HTMLInputElement>) => {\n  if (e.key === 'Enter') {\n    e.preventDefault()\n  }\n}\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_LEFT_KEY_REGEX = /^(Arrow)?Left$/\nconst ARROW_RIGHT_KEY_REGEX = /^(Arrow)?Right/\nconst ARROW_UP_AND_DOWN_KEY_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: [\n      'smarthr-ui-MultiCombobox',\n      'shr-box-border shr-inline-flex shr-min-w-[15em] shr-rounded-m shr-border shr-border-solid shr-px-0.5 shr-py-0.25 shr-align-bottom',\n      'contrast-more:shr-border-high-contrast',\n      'has-[[aria-invalid]]:shr-border-danger',\n    ],\n    inputArea: 'shr-flex shr-flex-1 shr-flex-wrap shr-gap-0.5',\n    selectedList:\n      'smarthr-ui-MultiCombobox-selectedList shr-contents shr-list-none [&_li]:shr-min-w-0',\n    inputWrapper: 'shr-flex shr-flex-1 shr-items-center',\n    input: [\n      'smarthr-ui-MultiCombobox-input',\n      'shr-w-full shr-min-w-[5em] shr-border-none shr-text-base shr-text-black shr-outline-none shr-outline-0',\n      'disabled:shr-hidden',\n    ],\n    placeholderEl: 'smarthr-ui-MultiCombobox-placeholder shr-my-0 shr-self-center',\n    suffixWrapper: [\n      'shr-relative -shr-me-0.5 shr-ms-0.5 shr-p-0.5',\n      'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n    ],\n    suffixIcon: 'shr-block',\n  },\n  variants: {\n    focused: {\n      true: {\n        wrapper: 'shr-focus-indicator',\n      },\n    },\n    disabled: {\n      true: {\n        wrapper:\n          'shr-cursor-not-allowed shr-border-default/50 shr-bg-white-darken shr-text-disabled',\n      },\n      false: {\n        wrapper: 'shr-cursor-text shr-bg-white',\n      },\n    },\n    hidden: {\n      true: {\n        inputWrapper: 'shr-pointer-events-none shr-absolute shr-opacity-0',\n      },\n    },\n  },\n  compoundVariants: [\n    {\n      disabled: false,\n      className: {\n        wrapper: 'shr-border-default',\n      },\n    },\n  ],\n})\n\nconst ActualMultiCombobox = <T,>(\n  {\n    items,\n    selectedItems,\n    name,\n    disabled = false,\n    required = false,\n    error = false,\n    creatable = false,\n    placeholder = '',\n    autoComplete,\n    dropdownHelpMessage,\n    isLoading,\n    selectedItemEllipsis,\n    width,\n    dropdownWidth = 'auto',\n    inputValue: controlledInputValue,\n    className,\n    onChange,\n    onChangeInput,\n    onAdd,\n    onDelete,\n    onSelect,\n    onChangeSelected,\n    onFocus,\n    onBlur,\n    onKeyPress,\n    isItemSelected,\n    noResultText,\n    style,\n    ...rest\n  }: Props<T>,\n  ref: Ref<HTMLInputElement>,\n) => {\n  const { localize } = useIntl()\n  const outerRef = useRef<HTMLDivElement>(null)\n  const [isFocused, setIsFocused] = useState(false)\n  const [highlighted, setHighlighted] = useState(false)\n  const isInputControlled = controlledInputValue !== undefined\n  const [uncontrolledInputValue, setUncontrolledInputValue] = useState('')\n  const inputValue = isInputControlled ? controlledInputValue : uncontrolledInputValue\n  const [isComposing, setIsComposing] = useState(false)\n  const { options } = useMultiOptions({\n    items,\n    selected: selectedItems,\n    creatable,\n    inputValue,\n    isItemSelected,\n  })\n  const setInputValueIfUncontrolled = isInputControlled ? NOOP : setUncontrolledInputValue\n  const actualOnDelete = useMemo(() => {\n    const handlers: Array<(item: ComboboxItem<T>) => void> = []\n\n    if (onDelete) {\n      handlers.push((item: ComboboxItem<T>) => onDelete(item))\n    }\n    if (onChangeSelected) {\n      handlers.push((item: ComboboxItem<T>) =>\n        onChangeSelected(selectedItems.filter((selected) => !areItemsEqual(selected, item))),\n      )\n    }\n\n    if (handlers.length === 0) {\n      return NOOP\n    }\n\n    return (item: ComboboxItem<T>) => {\n      // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n      // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n      requestAnimationFrame(() => {\n        handlers.forEach((h) => h(item))\n      })\n    }\n  }, [selectedItems, onChangeSelected, onDelete])\n  const actualOnSelect = useCallback(\n    (selected: ComboboxItem<T>) => {\n      // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n      // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n      requestAnimationFrame(() => {\n        const matchedSelectedItem = selectedItems.find((item) => areItemsEqual(item, selected))\n\n        if (matchedSelectedItem === undefined) {\n          onSelect?.(selected)\n          onChangeSelected?.(selectedItems.concat(selected))\n\n          // 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n          onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n        } else if (matchedSelectedItem.deletable !== false) {\n          actualOnDelete(selected)\n        }\n      })\n    },\n    [selectedItems, actualOnDelete, onChangeSelected, onSelect, onChangeInput],\n  )\n\n  const { renderListBox, activeOption, onKeyDownListBox, listBoxId, listBoxRef } = useListbox({\n    options,\n    dropdownHelpMessage,\n    dropdownWidth,\n    onAdd,\n    onSelect: actualOnSelect,\n    isExpanded: isFocused,\n    isLoading,\n    triggerRef: outerRef,\n    noResultText,\n  })\n\n  const {\n    deletionButtonRefs,\n    inputRef,\n    focusPrevDeletionButton,\n    focusNextDeletionButton,\n    resetDeletionButtonFocus,\n  } = useFocusControl(selectedItems.length)\n\n  useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n  const focus = useMemo(() => {\n    const baseAction = () => {\n      setIsFocused(true)\n    }\n\n    if (onFocus) {\n      return () => {\n        onFocus()\n        baseAction()\n      }\n    }\n\n    return baseAction\n  }, [onFocus])\n  const blur = useMemo(() => {\n    if (!isFocused) {\n      return NOOP\n    }\n\n    const baseAction = () => {\n      setIsFocused(false)\n      resetDeletionButtonFocus()\n    }\n\n    if (onBlur) {\n      return () => {\n        onBlur()\n        baseAction()\n      }\n    }\n\n    return baseAction\n  }, [isFocused, onBlur, resetDeletionButtonFocus])\n\n  const outerClickRef = useMemo(() => [outerRef, listBoxRef], [outerRef, listBoxRef])\n  useOuterClick(outerClickRef, blur)\n\n  const highlightedRef = useRef(highlighted)\n\n  useEffect(() => {\n    highlightedRef.current = highlighted\n  }, [highlighted])\n\n  useEffect(() => {\n    if (highlightedRef.current) {\n      setHighlighted(false)\n      inputRef.current?.select()\n    } else {\n      setInputValueIfUncontrolled('')\n    }\n  }, [selectedItems, inputRef, setInputValueIfUncontrolled])\n\n  useEffect(() => {\n    if (isFocused) {\n      inputRef.current?.focus()\n    }\n  }, [inputRef, isFocused, setInputValueIfUncontrolled, selectedItems])\n\n  const onDelegateKeyDown = useMemo(\n    () =>\n      isComposing\n        ? undefined\n        : (e: KeyboardEvent<HTMLDivElement>) => {\n            if (ESCAPE_KEY_REGEX.test(e.key)) {\n              e.stopPropagation()\n              blur()\n            } else if (e.key === 'Tab') {\n              if (isFocused) {\n                // フォーカスがコンポーネントを抜けるように先に input をフォーカスしておく\n                inputRef.current?.focus()\n              }\n\n              blur()\n            } else if (ARROW_LEFT_KEY_REGEX.test(e.key)) {\n              e.stopPropagation()\n              focusPrevDeletionButton()\n            } else if (ARROW_RIGHT_KEY_REGEX.test(e.key)) {\n              e.stopPropagation()\n              focusNextDeletionButton()\n            } else if (\n              e.key === 'Backspace' &&\n              !inputValue &&\n              selectedItems.length > 0 &&\n              selectedItems[selectedItems.length - 1].deletable !== false\n            ) {\n              e.preventDefault()\n              e.stopPropagation()\n\n              const lastItem = selectedItems[selectedItems.length - 1]\n\n              actualOnDelete(lastItem)\n              setHighlighted(true)\n              setInputValueIfUncontrolled(innerText(lastItem.label))\n            } else {\n              e.stopPropagation()\n              inputRef.current?.focus()\n              resetDeletionButtonFocus()\n            }\n\n            onKeyDownListBox(e)\n          },\n    [\n      blur,\n      focusNextDeletionButton,\n      focusPrevDeletionButton,\n      onKeyDownListBox,\n      inputRef,\n      isComposing,\n      isFocused,\n      resetDeletionButtonFocus,\n      actualOnDelete,\n      inputValue,\n      selectedItems,\n      setInputValueIfUncontrolled,\n    ],\n  )\n\n  const onDelegateClick = useMemo(\n    () =>\n      disabled || isFocused\n        ? undefined\n        : (e: MouseEvent<HTMLElement>) => {\n            if (!(e.target as HTMLElement).closest('.smarthr-ui-MultiCombobox-deleteButton')) {\n              focus()\n            }\n          },\n    [isFocused, disabled, focus],\n  )\n  const actualOnChangeInput = useMemo(() => {\n    const handlers = [onChange, onChangeInput].filter((h) => !!h)\n    const onSetValue = (e: ChangeEvent<HTMLInputElement>) => {\n      setInputValueIfUncontrolled(e.currentTarget.value)\n    }\n\n    if (handlers.length === 0) {\n      return onSetValue\n    }\n\n    return (e: ChangeEvent<HTMLInputElement>) => {\n      handlers.forEach((h) => h(e))\n      onSetValue(e)\n    }\n  }, [onChange, onChangeInput, setInputValueIfUncontrolled])\n  const onFocusInput = useMemo(\n    () =>\n      isFocused\n        ? resetDeletionButtonFocus\n        : () => {\n            resetDeletionButtonFocus()\n            focus()\n          },\n    [isFocused, focus, resetDeletionButtonFocus],\n  )\n  const onCompositionStartInput = useCallback(() => setIsComposing(true), [])\n  const onCompositionEndInput = useCallback(() => setIsComposing(false), [])\n  const onKeyDownInput = useCallback((e: KeyboardEvent<HTMLInputElement>) => {\n    if (ARROW_UP_AND_DOWN_KEY_REGEX.test(e.key)) {\n      // 上下キー入力はリストボックスの activeDescendant の移動に用いるため、input 内では作用させない\n      e.preventDefault()\n    }\n  }, [])\n\n  // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n  // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n  // submitイベントが発生し、formが送信される場合がある\n  const onDelegateKeyPress = useMemo(\n    () =>\n      onKeyPress\n        ? (e: KeyboardEvent<HTMLInputElement>) => {\n            preventDefaultWithPressEnter(e)\n            onKeyPress(e)\n          }\n        : preventDefaultWithPressEnter,\n    [onKeyPress],\n  )\n\n  const selectedListId = useId()\n\n  const wrapperStyle = useMemo(\n    () => ({\n      ...style,\n      width: typeof width === 'number' ? `${width}px` : width,\n    }),\n    [style, width],\n  )\n  const classNames = useMemo(() => {\n    const {\n      wrapper,\n      inputArea,\n      selectedList,\n      inputWrapper,\n      input,\n      placeholderEl,\n      suffixWrapper,\n      suffixIcon,\n    } = classNameGenerator()\n\n    return {\n      wrapper: wrapper({ focused: isFocused, disabled, className }),\n      inputArea: inputArea(),\n      selectedList: selectedList(),\n      inputWrapper: inputWrapper({ hidden: !isFocused }),\n      input: input(),\n      placeholder: placeholderEl(),\n      suffixWrapper: suffixWrapper({ disabled }),\n      suffixIcon: suffixIcon(),\n    }\n  }, [isFocused, disabled, className])\n\n  const selectedListAriaLabel = useMemo(\n    () =>\n      localize({\n        id: 'smarthr-ui/MultiCombobox/selectedListAriaLabel',\n        defaultText: '選択済みアイテム',\n      }),\n    [localize],\n  )\n\n  return (\n    // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n    <div\n      ref={outerRef}\n      role=\"group\"\n      onClick={onDelegateClick}\n      onKeyDown={onDelegateKeyDown}\n      onKeyPress={onDelegateKeyPress}\n      className={classNames.wrapper}\n      style={wrapperStyle}\n    >\n      <Scroller className={classNames.inputArea}>\n        <ul\n          id={selectedListId}\n          aria-label={selectedListAriaLabel}\n          className={classNames.selectedList}\n        >\n          {selectedItems.map((selectedItem, i) => (\n            <li key={`${selectedItem.label}-${innerText(selectedItem.value)}`}>\n              <MultiSelectedItem\n                item={selectedItem}\n                disabled={disabled}\n                onDelete={actualOnDelete}\n                enableEllipsis={selectedItemEllipsis}\n                buttonRef={deletionButtonRefs[i]}\n              />\n            </li>\n          ))}\n        </ul>\n\n        <div className={classNames.inputWrapper}>\n          <input\n            {...rest}\n            data-smarthr-ui-input=\"true\"\n            type=\"text\"\n            name={name}\n            value={inputValue}\n            disabled={disabled}\n            required={required && selectedItems.length === 0}\n            ref={inputRef}\n            onChange={actualOnChangeInput}\n            onFocus={onFocusInput}\n            onCompositionStart={onCompositionStartInput}\n            onCompositionEnd={onCompositionEndInput}\n            onKeyDown={onKeyDownInput}\n            autoComplete={autoComplete ?? 'off'}\n            tabIndex={0}\n            role=\"combobox\"\n            aria-activedescendant={activeOption?.id}\n            aria-controls={`${listBoxId} ${selectedListId}`}\n            aria-haspopup=\"listbox\"\n            aria-expanded={isFocused}\n            aria-invalid={error || undefined}\n            aria-disabled={disabled}\n            aria-autocomplete=\"list\"\n            className={classNames.input}\n          />\n        </div>\n\n        {selectedItems.length === 0 && placeholder && !isFocused && (\n          <p className={classNames.placeholder}>{placeholder}</p>\n        )}\n      </Scroller>\n\n      <MemoizedCaretDown\n        disabled={disabled}\n        isFocused={isFocused}\n        className={classNames.suffixWrapper}\n        iconStyle={classNames.suffixIcon}\n      />\n\n      {renderListBox()}\n    </div>\n  )\n}\n\nexport const MultiCombobox = genericsForwardRef(ActualMultiCombobox)\n\nconst MemoizedCaretDown = memo<{\n  className: string\n  iconStyle: string\n  disabled: boolean\n  isFocused: boolean\n}>(({ className, iconStyle, disabled, isFocused }) => {\n  const theme = useTheme()\n  const caretIconColor = useMemo(() => {\n    if (isFocused) return theme.textColor.black\n    if (disabled) return theme.textColor.disabled\n\n    return theme.textColor.grey\n  }, [disabled, isFocused, theme.textColor])\n\n  return (\n    <div className={className}>\n      <FaCaretDownIcon color={caretIconColor} className={iconStyle} />\n    </div>\n  )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA8EA;AAEA;AACE;;;AAGF;AAEA;AACA;AACA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;;;;;AAKC;AACD;AACA;AAEA;AACA;;;;AAIC;AACD;AACA;;;AAGC;AACD;AACD;AACD;AACE;AACE;AACE;AACD;AACF;AACD;AACE;AACE;AAED;AACD;AACE;AACD;AACF;AACD;AACE;AACE;AACD;AACF;AACF;AACD;AACE;AACE;AACA;AACE;AACD;AACF;AACF;AACF;AAED;AAkCE;AACA;;;AAGA;;;;AAIA;;AAEE;;;;AAID;;AAED;;;AAII;;;AAGA;;AAKF;AACE;;;;;;AAOE;AACF;AACF;;AAEF;;;;AAKM;AAEA;AACE;;;AAIA;;AACK;;;AAGT;AACF;AAIF;;;;;AAKE;AACA;;AAEA;;AAED;AAED;;AAUA;;;AAGE;;AAGE;AACE;AACA;AACF;;AAGF;AACF;AACA;;AAEI;;;;AAKA;AACF;;AAGE;AACE;AACA;AACF;;AAGF;;AAGF;AACA;AAEA;;AAGE;AACF;;AAGE;;AAEE;;;;;;;;AAQA;;;AAIJ;AAGM;AACA;;;AAGM;;AACK;;;AAGH;;AAGF;;;;AAGA;;;;AAGA;;AACK;AAEL;;AAEA;;;;;;;;;;AAYA;AACA;;;AAIJ;;;;;;;;;;;;;AAcL;;AAMG;AACA;;AAEM;;;AAKZ;AACE;AACA;AACE;AACF;AAEA;AACE;;;AAIA;;AAEF;;AAEF;AAGM;;AAEI;AACA;;AAIV;AACA;AACA;;;;;;;;;AAUA;AAGM;;;;AAIA;AAIN;AAEA;AAEI;AACA;AACD;AAGH;;;AAaI;;;;;;AAMA;;;;;AAQE;AACA;AACD;;;AAMH;AAwEJ;;AAIA;AAME;AACA;AACE;AAAe;AACf;AAAc;AAEd;;AAGF;AAKF;;"}