{"version":3,"file":"Textarea.cjs","sources":["../../../src/components/Textarea/Textarea.tsx"],"sourcesContent":["'use client'\n\nimport {\n  type ChangeEvent,\n  type ComponentPropsWithRef,\n  type ReactNode,\n  forwardRef,\n  startTransition,\n  useCallback,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useTheme } from '../../hooks/useTheme'\nimport { Localizer } from '../../intl'\nimport { debounce } from '../../libs/debounce'\nimport { defaultHtmlFontSize } from '../../themes'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\ntype AbstractProps = {\n  /** 入力値にエラーがあるかどうか */\n  error?: boolean\n  /** コンポーネントの幅 */\n  width?: number | string\n  /** 自動でフォーカスされるかどうか */\n  autoFocus?: boolean\n  /** 自動で広がるかどうか */\n  autoResize?: boolean\n  /** 最大行数。超えるとスクロールする。初期値は無限 */\n  maxRows?: number\n  /** 行数の初期値。省略した場合は2 */\n  rows?: number\n  /** 入力可能な最大文字数。あと何文字入力できるかの表示が追加される。html的なvalidateは発生しない */\n  maxLetters?: number\n  /**\n   * placeholder属性は非推奨です。別途ヒント用要素の設置を検討してください。\n   */\n  placeholder?: string\n}\ntype Props = AbstractProps & Omit<ComponentPropsWithRef<'textarea'>, keyof AbstractProps>\ntype TextareaValue = string | number | readonly string[]\n\nconst getStringLength = (value: TextareaValue) => {\n  const formattedValue =\n    typeof value === 'number' || typeof value === 'string'\n      ? `${value}`\n      : Array.isArray(value)\n        ? value.join(',')\n        : ''\n\n  // https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt\n  const surrogatePairs = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g\n  return formattedValue.length - (formattedValue.match(surrogatePairs) || []).length\n}\n\nconst classNameGenerator = tv({\n  slots: {\n    textareaEl: [\n      'smarthr-ui-Textarea-textarea',\n      'shr-border-shorthand shr-my-[unset] shr-box-border shr-rounded-m shr-bg-white shr-p-0.5 shr-text-base shr-leading-normal shr-text-black shr-opacity-100',\n      'contrast-more:shr-border-high-contrast',\n      'placeholder:shr-text-grey',\n      'focus-visible:shr-focus-indicator',\n      'disabled:shr-pointer-events-none disabled:shr-bg-column disabled:shr-text-disabled disabled:placeholder:shr-text-disabled',\n      'aria-[invalid]:shr-border-danger',\n    ],\n    counter: 'smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black',\n  },\n  variants: {\n    error: {\n      true: {\n        counter: 'shr-text-danger',\n      },\n    },\n  },\n  defaultVariants: {\n    error: false,\n  },\n})\n\nconst calculateIdealRows = (\n  element: HTMLTextAreaElement | null | undefined,\n  maxRows: number,\n  lineHeightNormal: number,\n): number => {\n  if (!element) {\n    return 0\n  }\n\n  // 現在の入力値に応じた行数\n  const currentInputValueRows = Math.floor(\n    element.scrollHeight / (defaultHtmlFontSize * lineHeightNormal),\n  )\n\n  return currentInputValueRows < maxRows ? currentInputValueRows : maxRows\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, Props>(\n  (\n    {\n      autoFocus,\n      maxLetters,\n      width,\n      className,\n      autoResize = false,\n      maxRows = Infinity,\n      rows = 2,\n      error,\n      onChange,\n      value,\n      defaultValue,\n      ...rest\n    },\n    ref,\n  ) => {\n    const theme = useTheme()\n    const maxLettersId = useId()\n    const maxLettersNoticeId = `${maxLettersId}-notice`\n    const actualMaxLettersId = maxLetters ? maxLettersId : undefined\n\n    const textareaRef = useRef<HTMLTextAreaElement>(null)\n    const currentValue = defaultValue || value\n    const [interimRows, setInterimRows] = useState(rows)\n    const [count, setCount] = useState(currentValue ? getStringLength(currentValue) : 0)\n    const [srCounterMessage, setSrCounterMessage] = useState<ReactNode>('')\n\n    const buildAvailableLetters = useCallback(\n      (availableLetters: number): ReactNode => (\n        <Localizer\n          id=\"smarthr-ui/Textarea/availableLetters\"\n          defaultText=\"あと{availableLetters}文字\"\n          values={{ availableLetters }}\n        />\n      ),\n      [],\n    )\n\n    const buildmaxLettersExceeded = useCallback(\n      (exceededLetters: number): ReactNode => (\n        <Localizer\n          id=\"smarthr-ui/Textarea/maxLettersExceeded\"\n          defaultText=\"{exceededLetters}文字オーバー\"\n          values={{ exceededLetters }}\n        />\n      ),\n      [],\n    )\n\n    const buildScreenReaderMaxLettersDescription = useCallback(\n      (internalMaxLetters: number): ReactNode => (\n        <Localizer\n          id=\"smarthr-ui/Textarea/screenReaderMaxLettersDescription\"\n          defaultText=\"最大{maxLetters}文字入力できます\"\n          values={{ maxLetters: internalMaxLetters }}\n        />\n      ),\n      [],\n    )\n\n    const getCounterMessage = useCallback(\n      (counterValue: number) => {\n        if (maxLetters === undefined) return\n\n        if (counterValue > maxLetters) {\n          // {count}文字オーバー\n          return <>{buildmaxLettersExceeded(counterValue - maxLetters)}</>\n        }\n\n        // あと{count}文字\n        return <>{buildAvailableLetters(maxLetters - counterValue)}</>\n      },\n      [maxLetters, buildAvailableLetters, buildmaxLettersExceeded],\n    )\n\n    const counterVisualMessage = useMemo(() => getCounterMessage(count), [count, getCounterMessage])\n\n    useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(\n      ref,\n      () => textareaRef.current,\n    )\n\n    const debouncedUpdateCount = useMemo(\n      () =>\n        maxLetters\n          ? debounce((newValue: TextareaValue) => {\n              startTransition(() => {\n                setCount(getStringLength(newValue))\n              })\n            }, 200)\n          : undefined,\n      [maxLetters],\n    )\n\n    // countが連続で更新されると、スクリーンリーダーが古い値を読み上げてしまうため、メッセージの更新を遅延しています\n    const debouncedUpdateSrCounterMessage = useMemo(\n      () =>\n        maxLetters\n          ? debounce((newValue: TextareaValue) => {\n              startTransition(() => {\n                const counterText = getCounterMessage(getStringLength(newValue))\n\n                if (counterText) {\n                  setSrCounterMessage(counterText)\n                }\n              })\n            }, 1000)\n          : undefined,\n      [maxLetters, getCounterMessage],\n    )\n\n    const handleChange = useCallback(\n      (e: ChangeEvent<HTMLTextAreaElement>) => {\n        const newValue = e.target.value\n        debouncedUpdateCount?.(newValue)\n        debouncedUpdateSrCounterMessage?.(newValue)\n\n        // rowsを初期化 TextareaのscrollHeightが文字列削除時に変更されないため\n        e.target.rows = rows\n\n        if (autoResize) {\n          const currentRows = calculateIdealRows(e.target, maxRows, theme.leading.NORMAL)\n          // rowsを直接反映 Textareaのrows propsが状態を変更しても反映されないため\n          e.target.rows = currentRows\n          setInterimRows(currentRows)\n        }\n\n        onChange?.(e)\n      },\n      [\n        onChange,\n        debouncedUpdateCount,\n        debouncedUpdateSrCounterMessage,\n        autoResize,\n        maxRows,\n        rows,\n        theme.leading.NORMAL,\n      ],\n    )\n\n    // autoFocus時に、フォーカスを当てる\n    useEffect(() => {\n      if (autoFocus && textareaRef && textareaRef.current) {\n        textareaRef.current.focus()\n      }\n    }, [autoFocus])\n\n    // autoResize時に、初期値での高さを指定\n    useEffect(() => {\n      if (autoResize && textareaRef.current) {\n        setInterimRows(calculateIdealRows(textareaRef.current, maxRows, theme.leading.NORMAL))\n      }\n    }, [setInterimRows, maxRows, autoResize, theme.leading.NORMAL])\n\n    // value 変更時にもカウントを更新する\n    useEffect(() => {\n      if (value && maxLetters) {\n        debouncedUpdateCount?.(value)\n        debouncedUpdateSrCounterMessage?.(value)\n      }\n    }, [maxLetters, debouncedUpdateCount, debouncedUpdateSrCounterMessage, value])\n\n    const textareaStyle = useMemo(\n      () => ({ width: typeof width === 'number' ? `${width}px` : width }),\n      [width],\n    )\n    const countError = maxLetters && count > maxLetters\n    const classNames = useMemo(() => {\n      const { textareaEl, counter } = classNameGenerator()\n\n      return {\n        textarea: textareaEl({ className }),\n        counter: counter({ error: !!countError }),\n      }\n    }, [countError, className])\n\n    const body = (\n      <textarea\n        {...rest}\n        {...(maxLetters && { 'aria-describedby': `${maxLettersNoticeId} ${actualMaxLettersId}` })}\n        data-smarthr-ui-input=\"true\"\n        value={value}\n        defaultValue={defaultValue}\n        onChange={handleChange}\n        ref={textareaRef}\n        aria-invalid={error || countError || undefined}\n        rows={interimRows}\n        className={classNames.textarea}\n        style={textareaStyle}\n      />\n    )\n\n    return maxLetters ? (\n      <span className=\"shr-relative\">\n        {body}\n        <VisuallyHiddenText id={maxLettersNoticeId}>\n          {buildScreenReaderMaxLettersDescription(maxLetters)}\n        </VisuallyHiddenText>\n        <VisuallyHiddenText aria-live=\"polite\">{srCounterMessage}</VisuallyHiddenText>\n        <span id={actualMaxLettersId} aria-hidden={true} className={classNames.counter}>\n          {counterVisualMessage}\n        </span>\n      </span>\n    ) : (\n      body\n    )\n  },\n)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA+CA;;;AAIM;AACE;;;;AAKN;AACF;AAEA;AACE;AACE;;;;;;;;AAQC;AACD;AACD;AACD;AACE;AACE;AACE;AACD;AACF;AACF;AACD;AACE;AACD;AACF;AAED;;AAMI;;;AAIF;;AAKF;AAEO;AAkBH;AACA;AACA;;AAGA;AACA;;;;AAKA;AAWA;AAWA;AAWA;;;AAII;;;;;;;AAWJ;;AAOA;AAGM;;AAEM;AACF;;AAEJ;;AAKN;AAGM;;;;;;AAOI;;;AAMV;AAEI;AACA;AACA;;AAGA;;AAGE;;AAEA;;;AAIF;AACF;;;;;;;;AASC;;;;AAMC;;AAEJ;;;AAIE;AACE;;AAEJ;;;AAIE;AACE;AACA;;;AAIJ;AAIA;AACA;;;AAII;;;AAGJ;;;AAgCF;;"}