smarthr-ui 99.3.1 → 99.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/lib/_virtual/index.cjs +2 -2
  2. package/lib/_virtual/index.js +2 -2
  3. package/lib/_virtual/index2.cjs +2 -2
  4. package/lib/_virtual/index2.js +2 -2
  5. package/lib/components/Combobox/ItemButton.cjs +11 -2
  6. package/lib/components/Combobox/ItemButton.cjs.map +1 -1
  7. package/lib/components/Combobox/ItemButton.js +11 -2
  8. package/lib/components/Combobox/ItemButton.js.map +1 -1
  9. package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs +1 -1
  10. package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs.map +1 -1
  11. package/lib/components/Combobox/MultiCombobox/MultiCombobox.js +1 -1
  12. package/lib/components/Combobox/MultiCombobox/MultiCombobox.js.map +1 -1
  13. package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs +1 -1
  14. package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs.map +1 -1
  15. package/lib/components/Combobox/SingleCombobox/SingleCombobox.js +1 -1
  16. package/lib/components/Combobox/SingleCombobox/SingleCombobox.js.map +1 -1
  17. package/lib/components/DatePicker/DatePicker.cjs +1 -1
  18. package/lib/components/DatePicker/DatePicker.cjs.map +1 -1
  19. package/lib/components/DatePicker/DatePicker.js +1 -1
  20. package/lib/components/DatePicker/DatePicker.js.map +1 -1
  21. package/lib/components/Dialog/FocusTrap.cjs +8 -8
  22. package/lib/components/Dialog/FocusTrap.cjs.map +1 -1
  23. package/lib/components/Dialog/FocusTrap.js +9 -9
  24. package/lib/components/Dialog/FocusTrap.js.map +1 -1
  25. package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs +2 -1
  26. package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs.map +1 -1
  27. package/lib/components/Dialog/ModelessDialog/ModelessDialog.js +2 -1
  28. package/lib/components/Dialog/ModelessDialog/ModelessDialog.js.map +1 -1
  29. package/lib/components/Input/CurrencyInput/CurrencyInput.cjs +2 -2
  30. package/lib/components/Input/CurrencyInput/CurrencyInput.cjs.map +1 -1
  31. package/lib/components/Input/CurrencyInput/CurrencyInput.js +2 -2
  32. package/lib/components/Input/CurrencyInput/CurrencyInput.js.map +1 -1
  33. package/lib/components/Input/Input.cjs +14 -16
  34. package/lib/components/Input/Input.cjs.map +1 -1
  35. package/lib/components/Input/Input.js +14 -16
  36. package/lib/components/Input/Input.js.map +1 -1
  37. package/lib/components/InputFile/InputFileMultiplyAppendable.cjs +8 -7
  38. package/lib/components/InputFile/InputFileMultiplyAppendable.cjs.map +1 -1
  39. package/lib/components/InputFile/InputFileMultiplyAppendable.js +8 -7
  40. package/lib/components/InputFile/InputFileMultiplyAppendable.js.map +1 -1
  41. package/lib/components/InputFile/InputFileNative.cjs +6 -5
  42. package/lib/components/InputFile/InputFileNative.cjs.map +1 -1
  43. package/lib/components/InputFile/InputFileNative.js +6 -5
  44. package/lib/components/InputFile/InputFileNative.js.map +1 -1
  45. package/lib/components/Layout/Container/Container.cjs.map +1 -1
  46. package/lib/components/Layout/Container/Container.js.map +1 -1
  47. package/lib/components/Pagination/Pagination.cjs +3 -2
  48. package/lib/components/Pagination/Pagination.cjs.map +1 -1
  49. package/lib/components/Pagination/Pagination.js +2 -1
  50. package/lib/components/Pagination/Pagination.js.map +1 -1
  51. package/lib/components/Panel/Groupbox/Groupbox.cjs +1 -0
  52. package/lib/components/Panel/Groupbox/Groupbox.cjs.map +1 -1
  53. package/lib/components/Panel/Groupbox/Groupbox.js +1 -0
  54. package/lib/components/Panel/Groupbox/Groupbox.js.map +1 -1
  55. package/lib/components/Select/Select.cjs +12 -14
  56. package/lib/components/Select/Select.cjs.map +1 -1
  57. package/lib/components/Select/Select.js +12 -14
  58. package/lib/components/Select/Select.js.map +1 -1
  59. package/lib/components/Table/TableScroller.cjs +13 -15
  60. package/lib/components/Table/TableScroller.cjs.map +1 -1
  61. package/lib/components/Table/TableScroller.js +14 -16
  62. package/lib/components/Table/TableScroller.js.map +1 -1
  63. package/lib/components/Textarea/Textarea.cjs +18 -12
  64. package/lib/components/Textarea/Textarea.cjs.map +1 -1
  65. package/lib/components/Textarea/Textarea.js +19 -13
  66. package/lib/components/Textarea/Textarea.js.map +1 -1
  67. package/lib/components/Tooltip/TooltipPortal.cjs +1 -0
  68. package/lib/components/Tooltip/TooltipPortal.cjs.map +1 -1
  69. package/lib/components/Tooltip/TooltipPortal.js +1 -0
  70. package/lib/components/Tooltip/TooltipPortal.js.map +1 -1
  71. package/lib/hooks/useClick.cjs +7 -4
  72. package/lib/hooks/useClick.cjs.map +1 -1
  73. package/lib/hooks/useClick.js +7 -4
  74. package/lib/hooks/useClick.js.map +1 -1
  75. package/lib/hooks/useOuterClick.cjs +5 -6
  76. package/lib/hooks/useOuterClick.cjs.map +1 -1
  77. package/lib/hooks/useOuterClick.js +6 -7
  78. package/lib/hooks/useOuterClick.js.map +1 -1
  79. package/lib/hooks/usePortal.cjs +24 -17
  80. package/lib/hooks/usePortal.cjs.map +1 -1
  81. package/lib/hooks/usePortal.d.ts +1 -1
  82. package/lib/hooks/usePortal.js +25 -18
  83. package/lib/hooks/usePortal.js.map +1 -1
  84. package/lib/hooks/useResponseStatus.cjs +20 -23
  85. package/lib/hooks/useResponseStatus.cjs.map +1 -1
  86. package/lib/hooks/useResponseStatus.js +20 -23
  87. package/lib/hooks/useResponseStatus.js.map +1 -1
  88. package/lib/intl/locales/en_us.json.cjs +1 -0
  89. package/lib/intl/locales/en_us.json.cjs.map +1 -1
  90. package/lib/intl/locales/en_us.json.js +1 -0
  91. package/lib/intl/locales/en_us.json.js.map +1 -1
  92. package/lib/intl/locales/id_id.json.cjs +0 -8
  93. package/lib/intl/locales/id_id.json.cjs.map +1 -1
  94. package/lib/intl/locales/id_id.json.js +0 -8
  95. package/lib/intl/locales/id_id.json.js.map +1 -1
  96. package/lib/intl/locales/index.d.ts +6 -16
  97. package/lib/intl/locales/ja_easy.json.cjs +1 -0
  98. package/lib/intl/locales/ja_easy.json.cjs.map +1 -1
  99. package/lib/intl/locales/ja_easy.json.js +1 -0
  100. package/lib/intl/locales/ja_easy.json.js.map +1 -1
  101. package/lib/intl/locales/ko_kr.json.cjs +1 -0
  102. package/lib/intl/locales/ko_kr.json.cjs.map +1 -1
  103. package/lib/intl/locales/ko_kr.json.js +1 -0
  104. package/lib/intl/locales/ko_kr.json.js.map +1 -1
  105. package/lib/intl/locales/pt_br.json.cjs +0 -8
  106. package/lib/intl/locales/pt_br.json.cjs.map +1 -1
  107. package/lib/intl/locales/pt_br.json.js +0 -8
  108. package/lib/intl/locales/pt_br.json.js.map +1 -1
  109. package/lib/intl/locales/vi_vn.json.cjs +1 -0
  110. package/lib/intl/locales/vi_vn.json.cjs.map +1 -1
  111. package/lib/intl/locales/vi_vn.json.js +1 -0
  112. package/lib/intl/locales/vi_vn.json.js.map +1 -1
  113. package/lib/intl/locales/zh_hans_cn.json.cjs +1 -0
  114. package/lib/intl/locales/zh_hans_cn.json.cjs.map +1 -1
  115. package/lib/intl/locales/zh_hans_cn.json.js +1 -0
  116. package/lib/intl/locales/zh_hans_cn.json.js.map +1 -1
  117. package/lib/intl/locales/zh_hant_tw.json.cjs +1 -0
  118. package/lib/intl/locales/zh_hant_tw.json.cjs.map +1 -1
  119. package/lib/intl/locales/zh_hant_tw.json.js +1 -0
  120. package/lib/intl/locales/zh_hant_tw.json.js.map +1 -1
  121. package/lib/intl/useDateFormat.cjs +1 -2
  122. package/lib/intl/useDateFormat.cjs.map +1 -1
  123. package/lib/intl/useDateFormat.js +1 -2
  124. package/lib/intl/useDateFormat.js.map +1 -1
  125. package/lib/intl/useIntl.cjs +2 -4
  126. package/lib/intl/useIntl.cjs.map +1 -1
  127. package/lib/intl/useIntl.js +2 -4
  128. package/lib/intl/useIntl.js.map +1 -1
  129. package/lib/libs/debounce.cjs +10 -2
  130. package/lib/libs/debounce.cjs.map +1 -1
  131. package/lib/libs/debounce.d.ts +4 -2
  132. package/lib/libs/debounce.js +10 -2
  133. package/lib/libs/debounce.js.map +1 -1
  134. package/lib/libs/lodash.cjs +3 -4
  135. package/lib/libs/lodash.cjs.map +1 -1
  136. package/lib/libs/lodash.d.ts +2 -11
  137. package/lib/libs/lodash.js +2 -7
  138. package/lib/libs/lodash.js.map +1 -1
  139. package/lib/libs/map.cjs +1 -5
  140. package/lib/libs/map.cjs.map +1 -1
  141. package/lib/libs/map.js +1 -5
  142. package/lib/libs/map.js.map +1 -1
  143. package/lib/themes/createBackgroundColor.cjs +3 -2
  144. package/lib/themes/createBackgroundColor.cjs.map +1 -1
  145. package/lib/themes/createBackgroundColor.js +2 -1
  146. package/lib/themes/createBackgroundColor.js.map +1 -1
  147. package/lib/themes/createBorder.cjs +3 -2
  148. package/lib/themes/createBorder.cjs.map +1 -1
  149. package/lib/themes/createBorder.js +2 -1
  150. package/lib/themes/createBorder.js.map +1 -1
  151. package/lib/themes/createBreakpoint.cjs +3 -2
  152. package/lib/themes/createBreakpoint.cjs.map +1 -1
  153. package/lib/themes/createBreakpoint.js +2 -1
  154. package/lib/themes/createBreakpoint.js.map +1 -1
  155. package/lib/themes/createColor.cjs +3 -2
  156. package/lib/themes/createColor.cjs.map +1 -1
  157. package/lib/themes/createColor.js +2 -1
  158. package/lib/themes/createColor.js.map +1 -1
  159. package/lib/themes/createFontSize.cjs +3 -2
  160. package/lib/themes/createFontSize.cjs.map +1 -1
  161. package/lib/themes/createFontSize.js +2 -1
  162. package/lib/themes/createFontSize.js.map +1 -1
  163. package/lib/themes/createInteraction.cjs +3 -2
  164. package/lib/themes/createInteraction.cjs.map +1 -1
  165. package/lib/themes/createInteraction.js +2 -1
  166. package/lib/themes/createInteraction.js.map +1 -1
  167. package/lib/themes/createLeading.cjs +3 -2
  168. package/lib/themes/createLeading.cjs.map +1 -1
  169. package/lib/themes/createLeading.js +2 -1
  170. package/lib/themes/createLeading.js.map +1 -1
  171. package/lib/themes/createRadius.cjs +3 -2
  172. package/lib/themes/createRadius.cjs.map +1 -1
  173. package/lib/themes/createRadius.js +2 -1
  174. package/lib/themes/createRadius.js.map +1 -1
  175. package/lib/themes/createShadow/createShadow.cjs +3 -2
  176. package/lib/themes/createShadow/createShadow.cjs.map +1 -1
  177. package/lib/themes/createShadow/createShadow.js +2 -1
  178. package/lib/themes/createShadow/createShadow.js.map +1 -1
  179. package/lib/themes/createTextColor.cjs +3 -2
  180. package/lib/themes/createTextColor.cjs.map +1 -1
  181. package/lib/themes/createTextColor.js +2 -1
  182. package/lib/themes/createTextColor.js.map +1 -1
  183. package/lib/themes/createZIndex.cjs +3 -2
  184. package/lib/themes/createZIndex.cjs.map +1 -1
  185. package/lib/themes/createZIndex.js +2 -1
  186. package/lib/themes/createZIndex.js.map +1 -1
  187. package/metadata.json +1 -1
  188. package/package.json +1 -1
  189. package/smarthr-ui.css +27 -7
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.cjs","sources":["../../../src/components/Textarea/Textarea.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type FC,\n type ReactNode,\n type Ref,\n forwardRef,\n startTransition,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Localizer } from '../../intl'\nimport { debounce } from '../../libs/debounce'\nimport { defaultHtmlFontSize } from '../../themes'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\ntype BaseProps = {\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 = BaseProps & Omit<ComponentPropsWithRef<'textarea'>, keyof BaseProps>\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 base: [\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})\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>(({ maxLetters, ...rest }, ref) =>\n maxLetters ? (\n <MaxLettersTextarea {...rest} externalRef={ref} maxLetters={maxLetters} />\n ) : (\n <ActualTextarea {...rest} externalRef={ref} />\n ),\n)\n\ntype LocalTextareaProps = ComponentProps<typeof Textarea> & {\n externalRef?: Ref<HTMLTextAreaElement>\n}\n\nconst MaxLettersTextarea: FC<\n Omit<LocalTextareaProps, 'maxLetters'> & {\n maxLetters: number\n }\n> = ({ maxLetters, error, value, defaultValue, onChange, id, ...rest }) => {\n const maxLettersId = useId()\n const textareaId = id || `${maxLettersId}-textarea`\n const maxLettersNoticeId = `${maxLettersId}-notice`\n\n const counterSpanRef = useRef<HTMLSpanElement>(null)\n const [count, setCount] = useState(() => {\n const currentValue = defaultValue || value\n return currentValue ? getStringLength(currentValue) : 0\n })\n const [srCounterMessage, setSrCounterMessage] = useState<ReactNode>('')\n\n const countError = count > maxLetters\n\n const latest = useLatest({\n onChange,\n })\n\n const functions = useMemo(() => {\n // counter spanのテキスト変更を監視してスクリーンリーダーメッセージを更新\n // countが連続で更新されると、スクリーンリーダーが古い値を読み上げてしまうため、メッセージの更新を遅延しています\n const updateSrMessage = debounce(() => {\n startTransition(() => {\n if (counterSpanRef.current) {\n setSrCounterMessage(counterSpanRef.current.textContent || '')\n }\n })\n }, 1000)\n const actualUpdateCount = debounce((newValue: TextareaValue) => {\n startTransition(() => {\n setCount(getStringLength(newValue))\n })\n }, 200)\n\n // 初回レンダリング時はスクリーンリーダー向けメッセージなどを更新したくないためskipする\n // (実際のユーザー操作による変更でのみ更新すれば良い)\n // useEffectでupdateCountが必ず呼びだされる\n let firstCallUpdateCount = true\n const updateCount = (newValue: TextareaValue) => {\n if (firstCallUpdateCount) {\n firstCallUpdateCount = false\n return\n }\n\n actualUpdateCount(newValue)\n updateSrMessage()\n }\n\n return {\n updateCount,\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n updateCount(e.target.value)\n latest.onChange?.(e)\n },\n }\n }, [latest])\n\n useEffect(() => {\n functions.updateCount(value ?? '')\n }, [value, functions])\n\n return (\n <span className=\"shr-relative\">\n <ActualTextarea\n {...rest}\n id={textareaId}\n value={value}\n defaultValue={defaultValue}\n onChange={functions.handleChange}\n aria-describedby={`${maxLettersNoticeId} ${maxLettersId}`}\n error={error || countError}\n />\n <VisuallyHiddenText id={maxLettersNoticeId}>\n <Localizer\n id=\"smarthr-ui/Textarea/screenReaderMaxLettersDescription\"\n defaultText=\"最大{maxLetters}文字入力できます\"\n values={{ maxLetters }}\n />\n </VisuallyHiddenText>\n <VisuallyHiddenText as=\"output\" role=\"status\" htmlFor={textareaId}>\n {srCounterMessage}\n </VisuallyHiddenText>\n <span\n ref={counterSpanRef}\n id={maxLettersId}\n aria-hidden={true}\n data-error={countError || undefined}\n className=\"smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black data-[error]:shr-text-danger\"\n >\n {count > maxLetters ? (\n <Localizer\n id=\"smarthr-ui/Textarea/maxLettersExceeded\"\n defaultText=\"{exceededLetters}文字オーバー\"\n values={{ exceededLetters: count - maxLetters }}\n />\n ) : (\n <Localizer\n id=\"smarthr-ui/Textarea/availableLetters\"\n defaultText=\"あと{availableLetters}文字\"\n values={{ availableLetters: maxLetters - count }}\n />\n )}\n </span>\n </span>\n )\n}\n\nconst ActualTextarea: FC<Omit<LocalTextareaProps, 'maxLetters'>> = ({\n autoFocus,\n width,\n className,\n autoResize = false,\n maxRows = Infinity,\n rows = 2,\n error,\n onChange,\n externalRef,\n ...rest\n}) => {\n const theme = useTheme()\n\n const textareaRef = useRef<HTMLTextAreaElement | null>(null)\n const [interimRows, setInterimRows] = useState(rows)\n\n const actualClassName = useMemo(() => classNameGenerator({ className }), [className])\n\n const latest = useLatest({\n onChange,\n autoFocus,\n autoResize,\n maxRows,\n theme,\n rows,\n })\n\n const functions = useMemo(\n () => ({\n callbackRef: (element: HTMLTextAreaElement | null) => {\n textareaRef.current = element\n if (element) {\n // autoFocus時に、フォーカスを当てる\n if (latest.autoFocus) {\n element.focus()\n }\n // autoResize時に、初期値での高さを指定\n if (latest.autoResize) {\n setInterimRows(calculateIdealRows(element, latest.maxRows, latest.theme.leading.NORMAL))\n }\n }\n },\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n // rowsを初期化 TextareaのscrollHeightが文字列削除時に変更されないため\n e.target.rows = latest.rows\n\n if (latest.autoResize) {\n const currentRows = calculateIdealRows(\n e.target,\n latest.maxRows,\n latest.theme.leading.NORMAL,\n )\n // rowsを直接反映 Textareaのrows propsが状態を変更しても反映されないため\n e.target.rows = currentRows\n setInterimRows(currentRows)\n }\n\n latest.onChange?.(e)\n },\n }),\n [latest],\n )\n\n useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(\n externalRef,\n () => textareaRef.current,\n [],\n )\n\n return (\n <textarea\n {...rest}\n data-smarthr-ui-input=\"true\"\n onChange={functions.handleChange}\n ref={functions.callbackRef}\n aria-invalid={error || undefined}\n rows={interimRows}\n className={actualClassName}\n style={{ width: typeof width === 'number' ? `${width}px` : width }}\n />\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA;;;AAIM;AACE;;;;AAKN;AACF;AAEA;AACE;;;;;;;;AAQC;AACF;AAED;;AAMI;;;AAIF;;AAKF;AAEO;AAYP;AAKE;AACA;AACA;AAEA;;AAEE;AACA;AACF;;AAGA;;;AAIC;AAED;;;AAGE;;AAEI;;;AAGF;;AAEF;;AAEI;AACF;;;;;;AAOF;;;;;;AAOE;AACF;;;AAIE;AACE;AACA;;;AAGN;;AAGE;AACF;AAEA;AA4CF;AAEA;AAYE;AAEA;;AAGA;;;;;;;;AASC;AAED;AAEI;AACE;;;AAGE;;;;AAIA;AACE;;;;AAIN;;;AAIE;;;AAOE;;;AAIF;;AAEH;AAIH;;AAkBF;;"}
1
+ {"version":3,"file":"Textarea.cjs","sources":["../../../src/components/Textarea/Textarea.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type FC,\n type ReactNode,\n type Ref,\n forwardRef,\n startTransition,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Localizer } from '../../intl'\nimport { debounce } from '../../libs/debounce'\nimport { defaultHtmlFontSize } from '../../themes'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\ntype BaseProps = {\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 = BaseProps & Omit<ComponentPropsWithRef<'textarea'>, keyof BaseProps>\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 base: [\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})\n\nconst calculateIdealRows = (\n node: HTMLTextAreaElement | null | undefined,\n maxRows: number,\n lineHeightNormal: number,\n): number => {\n if (!node) {\n return 0\n }\n\n // 現在の入力値に応じた行数\n const currentInputValueRows = Math.floor(\n node.scrollHeight / (defaultHtmlFontSize * lineHeightNormal),\n )\n\n return currentInputValueRows < maxRows ? currentInputValueRows : maxRows\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ maxLetters, ...rest }, ref) =>\n maxLetters ? (\n <MaxLettersTextarea {...rest} externalRef={ref} maxLetters={maxLetters} />\n ) : (\n <ActualTextarea {...rest} externalRef={ref} />\n ),\n)\n\ntype LocalTextareaProps = ComponentProps<typeof Textarea> & {\n externalRef?: Ref<HTMLTextAreaElement>\n}\n\nconst MaxLettersTextarea: FC<\n Omit<LocalTextareaProps, 'maxLetters'> & {\n maxLetters: number\n }\n> = ({ maxLetters, error, value, defaultValue, onChange, id, ...rest }) => {\n const maxLettersId = useId()\n const textareaId = id || `${maxLettersId}-textarea`\n const maxLettersNoticeId = `${maxLettersId}-notice`\n\n const counterSpanRef = useRef<HTMLSpanElement>(null)\n const [count, setCount] = useState(() => {\n const currentValue = defaultValue || value\n return currentValue ? getStringLength(currentValue) : 0\n })\n const [srCounterMessage, setSrCounterMessage] = useState<ReactNode>('')\n\n const countError = count > maxLetters\n\n const latest = useLatest({\n onChange,\n })\n\n const functions = useMemo(() => {\n // counter spanのテキスト変更を監視してスクリーンリーダーメッセージを更新\n // countが連続で更新されると、スクリーンリーダーが古い値を読み上げてしまうため、メッセージの更新を遅延しています\n const updateSrMessage = debounce(() => {\n startTransition(() => {\n if (counterSpanRef.current) {\n setSrCounterMessage(counterSpanRef.current.textContent || '')\n }\n })\n }, 1000)\n const actualUpdateCount = debounce((newValue: TextareaValue) => {\n startTransition(() => {\n setCount(getStringLength(newValue))\n })\n }, 200)\n\n // 初回レンダリング時はスクリーンリーダー向けメッセージなどを更新したくないためskipする\n // (実際のユーザー操作による変更でのみ更新すれば良い)\n // useEffectでupdateCountが必ず呼びだされる\n let firstCallUpdateCount = true\n const updateCount = (newValue: TextareaValue) => {\n if (firstCallUpdateCount) {\n firstCallUpdateCount = false\n return\n }\n\n actualUpdateCount(newValue)\n updateSrMessage()\n }\n\n return {\n updateCount,\n cancelDebounce: () => {\n updateSrMessage.cancel()\n actualUpdateCount.cancel()\n },\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n updateCount(e.target.value)\n latest.onChange?.(e)\n },\n }\n }, [latest])\n\n useEffect(() => {\n functions.updateCount(value ?? '')\n return functions.cancelDebounce\n }, [value, functions])\n\n return (\n <span className=\"shr-relative\">\n <ActualTextarea\n {...rest}\n id={textareaId}\n value={value}\n defaultValue={defaultValue}\n aria-describedby={`${maxLettersNoticeId} ${maxLettersId}`}\n error={error || countError}\n onChange={functions.handleChange}\n />\n <VisuallyHiddenText id={maxLettersNoticeId}>\n <Localizer\n id=\"smarthr-ui/Textarea/screenReaderMaxLettersDescription\"\n defaultText=\"最大{maxLetters}文字入力できます\"\n values={{ maxLetters }}\n />\n </VisuallyHiddenText>\n <VisuallyHiddenText as=\"output\" role=\"status\" htmlFor={textareaId}>\n {srCounterMessage}\n </VisuallyHiddenText>\n <span\n ref={counterSpanRef}\n id={maxLettersId}\n aria-hidden={true}\n data-error={countError || undefined}\n className=\"smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black data-[error]:shr-text-danger\"\n >\n {count > maxLetters ? (\n <Localizer\n id=\"smarthr-ui/Textarea/maxLettersExceeded\"\n defaultText=\"{exceededLetters}文字オーバー\"\n values={{ exceededLetters: count - maxLetters }}\n />\n ) : (\n <Localizer\n id=\"smarthr-ui/Textarea/availableLetters\"\n defaultText=\"あと{availableLetters}文字\"\n values={{ availableLetters: maxLetters - count }}\n />\n )}\n </span>\n </span>\n )\n}\n\nconst ActualTextarea: FC<Omit<LocalTextareaProps, 'maxLetters'>> = ({\n autoFocus,\n width,\n className,\n autoResize = false,\n maxRows = Infinity,\n rows = 2,\n error,\n onChange,\n externalRef,\n ...rest\n}) => {\n const theme = useTheme()\n\n const innerRef = useRef<HTMLTextAreaElement | null>(null)\n const [interimRows, setInterimRows] = useState(rows)\n\n const actualClassName = useMemo(() => classNameGenerator({ className }), [className])\n\n const latest = useLatest({\n onChange,\n autoFocus,\n autoResize,\n maxRows,\n theme,\n rows,\n })\n\n const functions = useMemo(\n () => ({\n callbackRef: (node: HTMLTextAreaElement | null) => {\n // TODO: useMergeRefs, useOnceCallbackが実装されたら適用する\n innerRef.current = node\n\n if (node) {\n // autoFocus時に、フォーカスを当てる\n if (latest.autoFocus) {\n node.focus()\n }\n // autoResize時に、初期値での高さを指定\n if (latest.autoResize) {\n setInterimRows(calculateIdealRows(node, latest.maxRows, latest.theme.leading.NORMAL))\n }\n }\n },\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n // rowsを初期化 TextareaのscrollHeightが文字列削除時に変更されないため\n e.target.rows = latest.rows\n\n if (latest.autoResize) {\n const currentRows = calculateIdealRows(\n e.target,\n latest.maxRows,\n latest.theme.leading.NORMAL,\n )\n // rowsを直接反映 Textareaのrows propsが状態を変更しても反映されないため\n e.target.rows = currentRows\n setInterimRows(currentRows)\n }\n\n latest.onChange?.(e)\n },\n }),\n [latest],\n )\n\n useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(\n externalRef,\n () => innerRef.current,\n [],\n )\n\n return (\n <textarea\n {...rest}\n ref={functions.callbackRef}\n aria-invalid={error || undefined}\n data-smarthr-ui-input=\"true\"\n rows={interimRows}\n onChange={functions.handleChange}\n className={actualClassName}\n style={{ width: typeof width === 'number' ? `${width}px` : width }}\n />\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA;;;AAIM;AACE;;;;AAKN;AACF;AAEA;AACE;;;;;;;;AAQC;AACF;AAED;;AAMI;;;AAIF;;AAKF;AAEO;AAYP;AAKE;AACA;AACA;AAEA;;AAEE;AACA;AACF;;AAGA;;;AAIC;AAED;;;AAGE;;AAEI;;;AAGF;;AAEF;;AAEI;AACF;;;;;;AAOF;;;;;;AAOE;AACF;;;;;;;AAQE;AACE;AACA;;;AAGN;;AAGE;;AAEF;AAEA;AA4CF;AAEA;AAYE;AAEA;;AAGA;;;;;;;;AASC;AAED;AAEI;;AAEE;;;AAIE;;;;AAIA;AACE;;;;AAIN;;;AAIE;;;AAOE;;;AAIF;;AAEH;AAIH;;AAkBF;;"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { forwardRef, useId, useRef, useState, useMemo, useEffect, useImperativeHandle, startTransition } from 'react';
3
+ import { forwardRef, useId, useRef, useState, useMemo, startTransition, useEffect, useImperativeHandle } from 'react';
4
4
  import { tv as ce } from './../../vendor/.pnpm/tailwind-variants@0.3.1_tailwindcss@3.4.19_ts-node@10.9.2_@swc_core@1.15.46_@types_node@20.19.43_typescript@5.9.3__/vendor/tailwind-variants/dist/index.js';
5
5
  import { useLatest } from '../../hooks/useLatest.js';
6
6
  import { useTheme } from '../../hooks/useTheme.js';
@@ -44,12 +44,12 @@ const classNameGenerator = ce({
44
44
  'aria-[invalid]:shr-border-danger',
45
45
  ],
46
46
  });
47
- const calculateIdealRows = (element, maxRows, lineHeightNormal) => {
48
- if (!element) {
47
+ const calculateIdealRows = (node, maxRows, lineHeightNormal) => {
48
+ if (!node) {
49
49
  return 0;
50
50
  }
51
51
  // 現在の入力値に応じた行数
52
- const currentInputValueRows = Math.floor(element.scrollHeight / (defaultHtmlFontSize * lineHeightNormal));
52
+ const currentInputValueRows = Math.floor(node.scrollHeight / (defaultHtmlFontSize * lineHeightNormal));
53
53
  return currentInputValueRows < maxRows ? currentInputValueRows : maxRows;
54
54
  };
55
55
  const Textarea = forwardRef(({ maxLetters, ...rest }, ref) => maxLetters ? (jsx(MaxLettersTextarea, { ...rest, externalRef: ref, maxLetters: maxLetters })) : (jsx(ActualTextarea, { ...rest, externalRef: ref })));
@@ -96,6 +96,10 @@ const MaxLettersTextarea = ({ maxLetters, error, value, defaultValue, onChange,
96
96
  };
97
97
  return {
98
98
  updateCount,
99
+ cancelDebounce: () => {
100
+ updateSrMessage.cancel();
101
+ actualUpdateCount.cancel();
102
+ },
99
103
  handleChange: (e) => {
100
104
  updateCount(e.target.value);
101
105
  latest.onChange?.(e);
@@ -104,12 +108,13 @@ const MaxLettersTextarea = ({ maxLetters, error, value, defaultValue, onChange,
104
108
  }, [latest]);
105
109
  useEffect(() => {
106
110
  functions.updateCount(value ?? '');
111
+ return functions.cancelDebounce;
107
112
  }, [value, functions]);
108
- return (jsxs("span", { className: "shr-relative", children: [jsx(ActualTextarea, { ...rest, id: textareaId, value: value, defaultValue: defaultValue, onChange: functions.handleChange, "aria-describedby": `${maxLettersNoticeId} ${maxLettersId}`, error: error || countError }), jsx(VisuallyHiddenText, { id: maxLettersNoticeId, children: jsx(Localizer, { id: "smarthr-ui/Textarea/screenReaderMaxLettersDescription", defaultText: "\u6700\u5927{maxLetters}\u6587\u5B57\u5165\u529B\u3067\u304D\u307E\u3059", values: { maxLetters } }) }), jsx(VisuallyHiddenText, { as: "output", role: "status", htmlFor: textareaId, children: srCounterMessage }), jsx("span", { ref: counterSpanRef, id: maxLettersId, "aria-hidden": true, "data-error": countError || undefined, className: "smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black data-[error]:shr-text-danger", children: count > maxLetters ? (jsx(Localizer, { id: "smarthr-ui/Textarea/maxLettersExceeded", defaultText: "{exceededLetters}\u6587\u5B57\u30AA\u30FC\u30D0\u30FC", values: { exceededLetters: count - maxLetters } })) : (jsx(Localizer, { id: "smarthr-ui/Textarea/availableLetters", defaultText: "\u3042\u3068{availableLetters}\u6587\u5B57", values: { availableLetters: maxLetters - count } })) })] }));
113
+ return (jsxs("span", { className: "shr-relative", children: [jsx(ActualTextarea, { ...rest, id: textareaId, value: value, defaultValue: defaultValue, "aria-describedby": `${maxLettersNoticeId} ${maxLettersId}`, error: error || countError, onChange: functions.handleChange }), jsx(VisuallyHiddenText, { id: maxLettersNoticeId, children: jsx(Localizer, { id: "smarthr-ui/Textarea/screenReaderMaxLettersDescription", defaultText: "\u6700\u5927{maxLetters}\u6587\u5B57\u5165\u529B\u3067\u304D\u307E\u3059", values: { maxLetters } }) }), jsx(VisuallyHiddenText, { as: "output", role: "status", htmlFor: textareaId, children: srCounterMessage }), jsx("span", { ref: counterSpanRef, id: maxLettersId, "aria-hidden": true, "data-error": countError || undefined, className: "smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black data-[error]:shr-text-danger", children: count > maxLetters ? (jsx(Localizer, { id: "smarthr-ui/Textarea/maxLettersExceeded", defaultText: "{exceededLetters}\u6587\u5B57\u30AA\u30FC\u30D0\u30FC", values: { exceededLetters: count - maxLetters } })) : (jsx(Localizer, { id: "smarthr-ui/Textarea/availableLetters", defaultText: "\u3042\u3068{availableLetters}\u6587\u5B57", values: { availableLetters: maxLetters - count } })) })] }));
109
114
  };
110
115
  const ActualTextarea = ({ autoFocus, width, className, autoResize = false, maxRows = Infinity, rows = 2, error, onChange, externalRef, ...rest }) => {
111
116
  const theme = useTheme();
112
- const textareaRef = useRef(null);
117
+ const innerRef = useRef(null);
113
118
  const [interimRows, setInterimRows] = useState(rows);
114
119
  const actualClassName = useMemo(() => classNameGenerator({ className }), [className]);
115
120
  const latest = useLatest({
@@ -121,16 +126,17 @@ const ActualTextarea = ({ autoFocus, width, className, autoResize = false, maxRo
121
126
  rows,
122
127
  });
123
128
  const functions = useMemo(() => ({
124
- callbackRef: (element) => {
125
- textareaRef.current = element;
126
- if (element) {
129
+ callbackRef: (node) => {
130
+ // TODO: useMergeRefs, useOnceCallbackが実装されたら適用する
131
+ innerRef.current = node;
132
+ if (node) {
127
133
  // autoFocus時に、フォーカスを当てる
128
134
  if (latest.autoFocus) {
129
- element.focus();
135
+ node.focus();
130
136
  }
131
137
  // autoResize時に、初期値での高さを指定
132
138
  if (latest.autoResize) {
133
- setInterimRows(calculateIdealRows(element, latest.maxRows, latest.theme.leading.NORMAL));
139
+ setInterimRows(calculateIdealRows(node, latest.maxRows, latest.theme.leading.NORMAL));
134
140
  }
135
141
  }
136
142
  },
@@ -146,8 +152,8 @@ const ActualTextarea = ({ autoFocus, width, className, autoResize = false, maxRo
146
152
  latest.onChange?.(e);
147
153
  },
148
154
  }), [latest]);
149
- useImperativeHandle(externalRef, () => textareaRef.current, []);
150
- return (jsx("textarea", { ...rest, "data-smarthr-ui-input": "true", onChange: functions.handleChange, ref: functions.callbackRef, "aria-invalid": error || undefined, rows: interimRows, className: actualClassName, style: { width: typeof width === 'number' ? `${width}px` : width } }));
155
+ useImperativeHandle(externalRef, () => innerRef.current, []);
156
+ return (jsx("textarea", { ...rest, ref: functions.callbackRef, "aria-invalid": error || undefined, "data-smarthr-ui-input": "true", rows: interimRows, onChange: functions.handleChange, className: actualClassName, style: { width: typeof width === 'number' ? `${width}px` : width } }));
151
157
  };
152
158
 
153
159
  export { Textarea };
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.js","sources":["../../../src/components/Textarea/Textarea.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type FC,\n type ReactNode,\n type Ref,\n forwardRef,\n startTransition,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Localizer } from '../../intl'\nimport { debounce } from '../../libs/debounce'\nimport { defaultHtmlFontSize } from '../../themes'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\ntype BaseProps = {\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 = BaseProps & Omit<ComponentPropsWithRef<'textarea'>, keyof BaseProps>\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 base: [\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})\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>(({ maxLetters, ...rest }, ref) =>\n maxLetters ? (\n <MaxLettersTextarea {...rest} externalRef={ref} maxLetters={maxLetters} />\n ) : (\n <ActualTextarea {...rest} externalRef={ref} />\n ),\n)\n\ntype LocalTextareaProps = ComponentProps<typeof Textarea> & {\n externalRef?: Ref<HTMLTextAreaElement>\n}\n\nconst MaxLettersTextarea: FC<\n Omit<LocalTextareaProps, 'maxLetters'> & {\n maxLetters: number\n }\n> = ({ maxLetters, error, value, defaultValue, onChange, id, ...rest }) => {\n const maxLettersId = useId()\n const textareaId = id || `${maxLettersId}-textarea`\n const maxLettersNoticeId = `${maxLettersId}-notice`\n\n const counterSpanRef = useRef<HTMLSpanElement>(null)\n const [count, setCount] = useState(() => {\n const currentValue = defaultValue || value\n return currentValue ? getStringLength(currentValue) : 0\n })\n const [srCounterMessage, setSrCounterMessage] = useState<ReactNode>('')\n\n const countError = count > maxLetters\n\n const latest = useLatest({\n onChange,\n })\n\n const functions = useMemo(() => {\n // counter spanのテキスト変更を監視してスクリーンリーダーメッセージを更新\n // countが連続で更新されると、スクリーンリーダーが古い値を読み上げてしまうため、メッセージの更新を遅延しています\n const updateSrMessage = debounce(() => {\n startTransition(() => {\n if (counterSpanRef.current) {\n setSrCounterMessage(counterSpanRef.current.textContent || '')\n }\n })\n }, 1000)\n const actualUpdateCount = debounce((newValue: TextareaValue) => {\n startTransition(() => {\n setCount(getStringLength(newValue))\n })\n }, 200)\n\n // 初回レンダリング時はスクリーンリーダー向けメッセージなどを更新したくないためskipする\n // (実際のユーザー操作による変更でのみ更新すれば良い)\n // useEffectでupdateCountが必ず呼びだされる\n let firstCallUpdateCount = true\n const updateCount = (newValue: TextareaValue) => {\n if (firstCallUpdateCount) {\n firstCallUpdateCount = false\n return\n }\n\n actualUpdateCount(newValue)\n updateSrMessage()\n }\n\n return {\n updateCount,\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n updateCount(e.target.value)\n latest.onChange?.(e)\n },\n }\n }, [latest])\n\n useEffect(() => {\n functions.updateCount(value ?? '')\n }, [value, functions])\n\n return (\n <span className=\"shr-relative\">\n <ActualTextarea\n {...rest}\n id={textareaId}\n value={value}\n defaultValue={defaultValue}\n onChange={functions.handleChange}\n aria-describedby={`${maxLettersNoticeId} ${maxLettersId}`}\n error={error || countError}\n />\n <VisuallyHiddenText id={maxLettersNoticeId}>\n <Localizer\n id=\"smarthr-ui/Textarea/screenReaderMaxLettersDescription\"\n defaultText=\"最大{maxLetters}文字入力できます\"\n values={{ maxLetters }}\n />\n </VisuallyHiddenText>\n <VisuallyHiddenText as=\"output\" role=\"status\" htmlFor={textareaId}>\n {srCounterMessage}\n </VisuallyHiddenText>\n <span\n ref={counterSpanRef}\n id={maxLettersId}\n aria-hidden={true}\n data-error={countError || undefined}\n className=\"smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black data-[error]:shr-text-danger\"\n >\n {count > maxLetters ? (\n <Localizer\n id=\"smarthr-ui/Textarea/maxLettersExceeded\"\n defaultText=\"{exceededLetters}文字オーバー\"\n values={{ exceededLetters: count - maxLetters }}\n />\n ) : (\n <Localizer\n id=\"smarthr-ui/Textarea/availableLetters\"\n defaultText=\"あと{availableLetters}文字\"\n values={{ availableLetters: maxLetters - count }}\n />\n )}\n </span>\n </span>\n )\n}\n\nconst ActualTextarea: FC<Omit<LocalTextareaProps, 'maxLetters'>> = ({\n autoFocus,\n width,\n className,\n autoResize = false,\n maxRows = Infinity,\n rows = 2,\n error,\n onChange,\n externalRef,\n ...rest\n}) => {\n const theme = useTheme()\n\n const textareaRef = useRef<HTMLTextAreaElement | null>(null)\n const [interimRows, setInterimRows] = useState(rows)\n\n const actualClassName = useMemo(() => classNameGenerator({ className }), [className])\n\n const latest = useLatest({\n onChange,\n autoFocus,\n autoResize,\n maxRows,\n theme,\n rows,\n })\n\n const functions = useMemo(\n () => ({\n callbackRef: (element: HTMLTextAreaElement | null) => {\n textareaRef.current = element\n if (element) {\n // autoFocus時に、フォーカスを当てる\n if (latest.autoFocus) {\n element.focus()\n }\n // autoResize時に、初期値での高さを指定\n if (latest.autoResize) {\n setInterimRows(calculateIdealRows(element, latest.maxRows, latest.theme.leading.NORMAL))\n }\n }\n },\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n // rowsを初期化 TextareaのscrollHeightが文字列削除時に変更されないため\n e.target.rows = latest.rows\n\n if (latest.autoResize) {\n const currentRows = calculateIdealRows(\n e.target,\n latest.maxRows,\n latest.theme.leading.NORMAL,\n )\n // rowsを直接反映 Textareaのrows propsが状態を変更しても反映されないため\n e.target.rows = currentRows\n setInterimRows(currentRows)\n }\n\n latest.onChange?.(e)\n },\n }),\n [latest],\n )\n\n useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(\n externalRef,\n () => textareaRef.current,\n [],\n )\n\n return (\n <textarea\n {...rest}\n data-smarthr-ui-input=\"true\"\n onChange={functions.handleChange}\n ref={functions.callbackRef}\n aria-invalid={error || undefined}\n rows={interimRows}\n className={actualClassName}\n style={{ width: typeof width === 'number' ? `${width}px` : width }}\n />\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkDA;;;AAIM;AACE;;;;AAKN;AACF;AAEA;AACE;;;;;;;;AAQC;AACF;AAED;;AAMI;;;AAIF;;AAKF;AAEO;AAYP;AAKE;AACA;AACA;AAEA;;AAEE;AACA;AACF;;AAGA;;;AAIC;AAED;;;AAGE;;AAEI;;;AAGF;;AAEF;;AAEI;AACF;;;;;;AAOF;;;;;;AAOE;AACF;;;AAIE;AACE;AACA;;;AAGN;;AAGE;AACF;AAEA;AA4CF;AAEA;AAYE;AAEA;;AAGA;;;;;;;;AASC;AAED;AAEI;AACE;;;AAGE;;;;AAIA;AACE;;;;AAIN;;;AAIE;;;AAOE;;;AAIF;;AAEH;AAIH;;AAkBF;;"}
1
+ {"version":3,"file":"Textarea.js","sources":["../../../src/components/Textarea/Textarea.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentProps,\n type ComponentPropsWithRef,\n type FC,\n type ReactNode,\n type Ref,\n forwardRef,\n startTransition,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../hooks/useLatest'\nimport { useTheme } from '../../hooks/useTheme'\nimport { Localizer } from '../../intl'\nimport { debounce } from '../../libs/debounce'\nimport { defaultHtmlFontSize } from '../../themes'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\ntype BaseProps = {\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 = BaseProps & Omit<ComponentPropsWithRef<'textarea'>, keyof BaseProps>\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 base: [\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})\n\nconst calculateIdealRows = (\n node: HTMLTextAreaElement | null | undefined,\n maxRows: number,\n lineHeightNormal: number,\n): number => {\n if (!node) {\n return 0\n }\n\n // 現在の入力値に応じた行数\n const currentInputValueRows = Math.floor(\n node.scrollHeight / (defaultHtmlFontSize * lineHeightNormal),\n )\n\n return currentInputValueRows < maxRows ? currentInputValueRows : maxRows\n}\n\nexport const Textarea = forwardRef<HTMLTextAreaElement, Props>(({ maxLetters, ...rest }, ref) =>\n maxLetters ? (\n <MaxLettersTextarea {...rest} externalRef={ref} maxLetters={maxLetters} />\n ) : (\n <ActualTextarea {...rest} externalRef={ref} />\n ),\n)\n\ntype LocalTextareaProps = ComponentProps<typeof Textarea> & {\n externalRef?: Ref<HTMLTextAreaElement>\n}\n\nconst MaxLettersTextarea: FC<\n Omit<LocalTextareaProps, 'maxLetters'> & {\n maxLetters: number\n }\n> = ({ maxLetters, error, value, defaultValue, onChange, id, ...rest }) => {\n const maxLettersId = useId()\n const textareaId = id || `${maxLettersId}-textarea`\n const maxLettersNoticeId = `${maxLettersId}-notice`\n\n const counterSpanRef = useRef<HTMLSpanElement>(null)\n const [count, setCount] = useState(() => {\n const currentValue = defaultValue || value\n return currentValue ? getStringLength(currentValue) : 0\n })\n const [srCounterMessage, setSrCounterMessage] = useState<ReactNode>('')\n\n const countError = count > maxLetters\n\n const latest = useLatest({\n onChange,\n })\n\n const functions = useMemo(() => {\n // counter spanのテキスト変更を監視してスクリーンリーダーメッセージを更新\n // countが連続で更新されると、スクリーンリーダーが古い値を読み上げてしまうため、メッセージの更新を遅延しています\n const updateSrMessage = debounce(() => {\n startTransition(() => {\n if (counterSpanRef.current) {\n setSrCounterMessage(counterSpanRef.current.textContent || '')\n }\n })\n }, 1000)\n const actualUpdateCount = debounce((newValue: TextareaValue) => {\n startTransition(() => {\n setCount(getStringLength(newValue))\n })\n }, 200)\n\n // 初回レンダリング時はスクリーンリーダー向けメッセージなどを更新したくないためskipする\n // (実際のユーザー操作による変更でのみ更新すれば良い)\n // useEffectでupdateCountが必ず呼びだされる\n let firstCallUpdateCount = true\n const updateCount = (newValue: TextareaValue) => {\n if (firstCallUpdateCount) {\n firstCallUpdateCount = false\n return\n }\n\n actualUpdateCount(newValue)\n updateSrMessage()\n }\n\n return {\n updateCount,\n cancelDebounce: () => {\n updateSrMessage.cancel()\n actualUpdateCount.cancel()\n },\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n updateCount(e.target.value)\n latest.onChange?.(e)\n },\n }\n }, [latest])\n\n useEffect(() => {\n functions.updateCount(value ?? '')\n return functions.cancelDebounce\n }, [value, functions])\n\n return (\n <span className=\"shr-relative\">\n <ActualTextarea\n {...rest}\n id={textareaId}\n value={value}\n defaultValue={defaultValue}\n aria-describedby={`${maxLettersNoticeId} ${maxLettersId}`}\n error={error || countError}\n onChange={functions.handleChange}\n />\n <VisuallyHiddenText id={maxLettersNoticeId}>\n <Localizer\n id=\"smarthr-ui/Textarea/screenReaderMaxLettersDescription\"\n defaultText=\"最大{maxLetters}文字入力できます\"\n values={{ maxLetters }}\n />\n </VisuallyHiddenText>\n <VisuallyHiddenText as=\"output\" role=\"status\" htmlFor={textareaId}>\n {srCounterMessage}\n </VisuallyHiddenText>\n <span\n ref={counterSpanRef}\n id={maxLettersId}\n aria-hidden={true}\n data-error={countError || undefined}\n className=\"smarthr-ui-Textarea-counter shr-block shr-text-sm shr-text-black data-[error]:shr-text-danger\"\n >\n {count > maxLetters ? (\n <Localizer\n id=\"smarthr-ui/Textarea/maxLettersExceeded\"\n defaultText=\"{exceededLetters}文字オーバー\"\n values={{ exceededLetters: count - maxLetters }}\n />\n ) : (\n <Localizer\n id=\"smarthr-ui/Textarea/availableLetters\"\n defaultText=\"あと{availableLetters}文字\"\n values={{ availableLetters: maxLetters - count }}\n />\n )}\n </span>\n </span>\n )\n}\n\nconst ActualTextarea: FC<Omit<LocalTextareaProps, 'maxLetters'>> = ({\n autoFocus,\n width,\n className,\n autoResize = false,\n maxRows = Infinity,\n rows = 2,\n error,\n onChange,\n externalRef,\n ...rest\n}) => {\n const theme = useTheme()\n\n const innerRef = useRef<HTMLTextAreaElement | null>(null)\n const [interimRows, setInterimRows] = useState(rows)\n\n const actualClassName = useMemo(() => classNameGenerator({ className }), [className])\n\n const latest = useLatest({\n onChange,\n autoFocus,\n autoResize,\n maxRows,\n theme,\n rows,\n })\n\n const functions = useMemo(\n () => ({\n callbackRef: (node: HTMLTextAreaElement | null) => {\n // TODO: useMergeRefs, useOnceCallbackが実装されたら適用する\n innerRef.current = node\n\n if (node) {\n // autoFocus時に、フォーカスを当てる\n if (latest.autoFocus) {\n node.focus()\n }\n // autoResize時に、初期値での高さを指定\n if (latest.autoResize) {\n setInterimRows(calculateIdealRows(node, latest.maxRows, latest.theme.leading.NORMAL))\n }\n }\n },\n handleChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n // rowsを初期化 TextareaのscrollHeightが文字列削除時に変更されないため\n e.target.rows = latest.rows\n\n if (latest.autoResize) {\n const currentRows = calculateIdealRows(\n e.target,\n latest.maxRows,\n latest.theme.leading.NORMAL,\n )\n // rowsを直接反映 Textareaのrows propsが状態を変更しても反映されないため\n e.target.rows = currentRows\n setInterimRows(currentRows)\n }\n\n latest.onChange?.(e)\n },\n }),\n [latest],\n )\n\n useImperativeHandle<HTMLTextAreaElement | null, HTMLTextAreaElement | null>(\n externalRef,\n () => innerRef.current,\n [],\n )\n\n return (\n <textarea\n {...rest}\n ref={functions.callbackRef}\n aria-invalid={error || undefined}\n data-smarthr-ui-input=\"true\"\n rows={interimRows}\n onChange={functions.handleChange}\n className={actualClassName}\n style={{ width: typeof width === 'number' ? `${width}px` : width }}\n />\n )\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAkDA;;;AAIM;AACE;;;;AAKN;AACF;AAEA;AACE;;;;;;;;AAQC;AACF;AAED;;AAMI;;;AAIF;;AAKF;AAEO;AAYP;AAKE;AACA;AACA;AAEA;;AAEE;AACA;AACF;;AAGA;;;AAIC;AAED;;;AAGE;;AAEI;;;AAGF;;AAEF;;AAEI;AACF;;;;;;AAOF;;;;;;AAOE;AACF;;;;;;;AAQE;AACE;AACA;;;AAGN;;AAGE;;AAEF;AAEA;AA4CF;AAEA;AAYE;AAEA;;AAGA;;;;;;;;AASC;AAED;AAEI;;AAEE;;;AAIE;;;;AAIA;AACE;;;;AAIN;;;AAIE;;;AAOE;;;AAIF;;AAEH;AAIH;;AAkBF;;"}
@@ -52,6 +52,7 @@ const TooltipPortal = ({ messageId, message, isVisible, parentRect, isIcon }) =>
52
52
  window.addEventListener('resize', debouncedAction);
53
53
  return () => {
54
54
  window.removeEventListener('resize', debouncedAction);
55
+ debouncedAction.cancel();
55
56
  };
56
57
  }, [parentRect, theme]);
57
58
  return (jsxRuntime.jsx("div", { ref: portalRef, role: "tooltip", "aria-hidden": !isVisible, className: CLASS_NAMES.container, style: isVisible ? style : undefined, children: jsxRuntime.jsx(components_Tooltip_ControlledTooltip.ControlledTooltip, { horizontal: actualHorizontal, vertical: actualVertical, triggerIcon: isIcon, className: CLASS_NAMES.balloon, children: jsxRuntime.jsx("div", { id: messageId, className: CLASS_NAMES.balloonText, children: message }) }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"TooltipPortal.cjs","sources":["../../../src/components/Tooltip/TooltipPortal.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactNode, useCallback, useState } from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useTheme } from '../../hooks/useTheme'\nimport { debounce } from '../../libs/debounce'\n\nimport { ControlledTooltip } from './ControlledTooltip'\n\ntype Props = {\n messageId: string\n message: ReactNode\n isVisible: boolean\n parentRect: DOMRect | null\n isIcon?: boolean\n}\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-Tooltip-popup shr-absolute shr-z-overlap aria-hidden:shr-hidden',\n balloon: 'shr-max-w-full [&&&]:shr-whitespace-normal',\n balloonText: 'shr-m-0 shr-px-1 shr-py-0.5',\n },\n})\n\nconst CLASS_NAMES = (() => {\n const { container, balloon, balloonText } = classNameGenerator()\n\n return {\n container: container(),\n balloon: balloon(),\n balloonText: balloonText(),\n }\n})()\n\nconst OUTER_MARGIN = 10\nconst SPACING = 5\n\ntype HorizontalType = 'left' | 'center' | 'right'\ntype VerticalType = 'top' | 'middle' | 'bottom'\n\nexport const TooltipPortal: FC<Props> = ({ messageId, message, isVisible, parentRect, isIcon }) => {\n const theme = useTheme()\n const [style, setStyle] = useState<{ [key: string]: undefined | string }>({})\n const [actualHorizontal, setActualHorizontal] = useState<HorizontalType>('center')\n const [actualVertical, setActualVertical] = useState<VerticalType>('bottom')\n\n const portalRef = useCallback(\n (element: HTMLDivElement | null) => {\n if (!element || !parentRect) {\n return\n }\n\n const action = () => {\n const vertical = calculateVertical(element.offsetHeight, parentRect)\n const horizontal = calculateHorizontal(element.offsetWidth, parentRect, theme)\n\n setStyle({\n insetBlockStart: vertical.insetBlockStart,\n insetInlineStart: horizontal.insetInlineStart,\n insetInlineEnd: horizontal.insetInlineEnd,\n maxWidth: horizontal.maxWidth,\n maxHeight: vertical.maxHeight,\n })\n setActualVertical(vertical.alignment)\n setActualHorizontal(horizontal.alignment)\n }\n const debouncedAction = debounce(action, 100)\n\n action()\n\n window.addEventListener('resize', debouncedAction)\n\n return () => {\n window.removeEventListener('resize', debouncedAction)\n }\n },\n [parentRect, theme],\n )\n\n return (\n <div\n ref={portalRef}\n role=\"tooltip\"\n aria-hidden={!isVisible}\n className={CLASS_NAMES.container}\n style={isVisible ? style : undefined}\n >\n <ControlledTooltip\n horizontal={actualHorizontal}\n vertical={actualVertical}\n triggerIcon={isIcon}\n className={CLASS_NAMES.balloon}\n >\n <div id={messageId} className={CLASS_NAMES.balloonText}>\n {message}\n </div>\n </ControlledTooltip>\n </div>\n )\n}\n\nconst calculateVertical = (\n portalHeight: number,\n parentRect: DOMRect,\n): { insetBlockStart: string; maxHeight: string | undefined; alignment: VerticalType } => {\n // トリガの上側の領域に収まる場合\n if (parentRect.top - portalHeight >= 0) {\n return {\n insetBlockStart: `${scrollY + parentRect.top - portalHeight - SPACING}px`,\n maxHeight: undefined,\n alignment: 'bottom',\n }\n }\n\n // トリガの下側の領域に収まる場合\n if (parentRect.bottom + portalHeight <= innerHeight) {\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: undefined,\n alignment: 'top',\n }\n }\n\n const triggerHeight = parentRect.bottom - parentRect.top\n\n // 上側の領域のほうが広い場合\n if (parentRect.top + triggerHeight / 2 >= innerHeight / 2) {\n return {\n insetBlockStart: `${scrollY + OUTER_MARGIN - SPACING}px`,\n maxHeight: `${parentRect.top - OUTER_MARGIN}px`,\n alignment: 'bottom',\n }\n }\n\n // 下側の領域のほうが広い場合\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: `${innerHeight - parentRect.bottom - OUTER_MARGIN}px`,\n alignment: 'top',\n }\n}\n\ntype ReturnCalculateHorizontalType = {\n insetInlineStart: string | undefined\n insetInlineEnd: string | undefined\n maxWidth: string\n alignment: HorizontalType\n}\nconst calculateHorizontal = (\n portalWidth: number,\n parentRect: DOMRect,\n theme: ReturnType<typeof useTheme>,\n): ReturnCalculateHorizontalType => {\n const triggerAlignCenter = parentRect.left + parentRect.width / 2\n const portalHalfWidth = portalWidth / 2\n const edgeSpacing = theme.spacingByChar(0.5)\n\n const leftSpacing = triggerAlignCenter - portalHalfWidth\n const rightSpacingEdge = document.body.clientWidth - SPACING\n\n // トリガを中心に左右に十分な余白がある場合\n if (leftSpacing > SPACING && triggerAlignCenter + portalHalfWidth < rightSpacingEdge) {\n const insetInlineStart = `${leftSpacing}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'center',\n }\n }\n\n // トリガが画面左寄りの場合\n if (triggerAlignCenter <= document.body.clientWidth / 2) {\n const insetInlineStart = `${scrollX + parentRect.left - SPACING}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'left',\n }\n }\n\n // トリガが画面右寄りの場合\n const insetInlineEnd = `${rightSpacingEdge - parentRect.right - scrollX}px`\n\n return {\n insetInlineStart: undefined,\n insetInlineEnd,\n maxWidth: `calc(100% - ${edgeSpacing} - max(${insetInlineEnd}, 0px))`,\n alignment: 'right',\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;AAkBA;AACE;AACE;AACA;AACA;AACD;AACF;AAED;;;;;;;AAQA;AAEA;AACA;AAKO;AACL;;;;AAKA;AAEI;;;;;AAME;AAEA;;;;;;AAMC;AACD;AACA;AACF;;AAGA;AAEA;AAEA;AACE;AACF;AACF;AAIF;AAoBF;AAEA;;;;;AAQM;AACA;;;;;;;AAQA;AACA;;;;;AAOJ;;AAEI;AACA;AACA;;;;;;;AAQF;;AAEJ;AAQA;;AAME;;AAGA;;;;AAKE;;;AAIE;AACA;AACA;;;;;;;;AAUA;AACA;AACA;;;;;;AAQF;;AAEA;AACA;;AAEJ;;"}
1
+ {"version":3,"file":"TooltipPortal.cjs","sources":["../../../src/components/Tooltip/TooltipPortal.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactNode, useCallback, useState } from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useTheme } from '../../hooks/useTheme'\nimport { debounce } from '../../libs/debounce'\n\nimport { ControlledTooltip } from './ControlledTooltip'\n\ntype Props = {\n messageId: string\n message: ReactNode\n isVisible: boolean\n parentRect: DOMRect | null\n isIcon?: boolean\n}\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-Tooltip-popup shr-absolute shr-z-overlap aria-hidden:shr-hidden',\n balloon: 'shr-max-w-full [&&&]:shr-whitespace-normal',\n balloonText: 'shr-m-0 shr-px-1 shr-py-0.5',\n },\n})\n\nconst CLASS_NAMES = (() => {\n const { container, balloon, balloonText } = classNameGenerator()\n\n return {\n container: container(),\n balloon: balloon(),\n balloonText: balloonText(),\n }\n})()\n\nconst OUTER_MARGIN = 10\nconst SPACING = 5\n\ntype HorizontalType = 'left' | 'center' | 'right'\ntype VerticalType = 'top' | 'middle' | 'bottom'\n\nexport const TooltipPortal: FC<Props> = ({ messageId, message, isVisible, parentRect, isIcon }) => {\n const theme = useTheme()\n const [style, setStyle] = useState<{ [key: string]: undefined | string }>({})\n const [actualHorizontal, setActualHorizontal] = useState<HorizontalType>('center')\n const [actualVertical, setActualVertical] = useState<VerticalType>('bottom')\n\n const portalRef = useCallback(\n (element: HTMLDivElement | null) => {\n if (!element || !parentRect) {\n return\n }\n\n const action = () => {\n const vertical = calculateVertical(element.offsetHeight, parentRect)\n const horizontal = calculateHorizontal(element.offsetWidth, parentRect, theme)\n\n setStyle({\n insetBlockStart: vertical.insetBlockStart,\n insetInlineStart: horizontal.insetInlineStart,\n insetInlineEnd: horizontal.insetInlineEnd,\n maxWidth: horizontal.maxWidth,\n maxHeight: vertical.maxHeight,\n })\n setActualVertical(vertical.alignment)\n setActualHorizontal(horizontal.alignment)\n }\n const debouncedAction = debounce(action, 100)\n\n action()\n\n window.addEventListener('resize', debouncedAction)\n\n return () => {\n window.removeEventListener('resize', debouncedAction)\n debouncedAction.cancel()\n }\n },\n [parentRect, theme],\n )\n\n return (\n <div\n ref={portalRef}\n role=\"tooltip\"\n aria-hidden={!isVisible}\n className={CLASS_NAMES.container}\n style={isVisible ? style : undefined}\n >\n <ControlledTooltip\n horizontal={actualHorizontal}\n vertical={actualVertical}\n triggerIcon={isIcon}\n className={CLASS_NAMES.balloon}\n >\n <div id={messageId} className={CLASS_NAMES.balloonText}>\n {message}\n </div>\n </ControlledTooltip>\n </div>\n )\n}\n\nconst calculateVertical = (\n portalHeight: number,\n parentRect: DOMRect,\n): { insetBlockStart: string; maxHeight: string | undefined; alignment: VerticalType } => {\n // トリガの上側の領域に収まる場合\n if (parentRect.top - portalHeight >= 0) {\n return {\n insetBlockStart: `${scrollY + parentRect.top - portalHeight - SPACING}px`,\n maxHeight: undefined,\n alignment: 'bottom',\n }\n }\n\n // トリガの下側の領域に収まる場合\n if (parentRect.bottom + portalHeight <= innerHeight) {\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: undefined,\n alignment: 'top',\n }\n }\n\n const triggerHeight = parentRect.bottom - parentRect.top\n\n // 上側の領域のほうが広い場合\n if (parentRect.top + triggerHeight / 2 >= innerHeight / 2) {\n return {\n insetBlockStart: `${scrollY + OUTER_MARGIN - SPACING}px`,\n maxHeight: `${parentRect.top - OUTER_MARGIN}px`,\n alignment: 'bottom',\n }\n }\n\n // 下側の領域のほうが広い場合\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: `${innerHeight - parentRect.bottom - OUTER_MARGIN}px`,\n alignment: 'top',\n }\n}\n\ntype ReturnCalculateHorizontalType = {\n insetInlineStart: string | undefined\n insetInlineEnd: string | undefined\n maxWidth: string\n alignment: HorizontalType\n}\nconst calculateHorizontal = (\n portalWidth: number,\n parentRect: DOMRect,\n theme: ReturnType<typeof useTheme>,\n): ReturnCalculateHorizontalType => {\n const triggerAlignCenter = parentRect.left + parentRect.width / 2\n const portalHalfWidth = portalWidth / 2\n const edgeSpacing = theme.spacingByChar(0.5)\n\n const leftSpacing = triggerAlignCenter - portalHalfWidth\n const rightSpacingEdge = document.body.clientWidth - SPACING\n\n // トリガを中心に左右に十分な余白がある場合\n if (leftSpacing > SPACING && triggerAlignCenter + portalHalfWidth < rightSpacingEdge) {\n const insetInlineStart = `${leftSpacing}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'center',\n }\n }\n\n // トリガが画面左寄りの場合\n if (triggerAlignCenter <= document.body.clientWidth / 2) {\n const insetInlineStart = `${scrollX + parentRect.left - SPACING}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'left',\n }\n }\n\n // トリガが画面右寄りの場合\n const insetInlineEnd = `${rightSpacingEdge - parentRect.right - scrollX}px`\n\n return {\n insetInlineStart: undefined,\n insetInlineEnd,\n maxWidth: `calc(100% - ${edgeSpacing} - max(${insetInlineEnd}, 0px))`,\n alignment: 'right',\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;AAkBA;AACE;AACE;AACA;AACA;AACD;AACF;AAED;;;;;;;AAQA;AAEA;AACA;AAKO;AACL;;;;AAKA;AAEI;;;;;AAME;AAEA;;;;;;AAMC;AACD;AACA;AACF;;AAGA;AAEA;AAEA;AACE;;AAEF;AACF;AAIF;AAoBF;AAEA;;;;;AAQM;AACA;;;;;;;AAQA;AACA;;;;;AAOJ;;AAEI;AACA;AACA;;;;;;;AAQF;;AAEJ;AAQA;;AAME;;AAGA;;;;AAKE;;;AAIE;AACA;AACA;;;;;;;;AAUA;AACA;AACA;;;;;;AAQF;;AAEA;AACA;;AAEJ;;"}
@@ -50,6 +50,7 @@ const TooltipPortal = ({ messageId, message, isVisible, parentRect, isIcon }) =>
50
50
  window.addEventListener('resize', debouncedAction);
51
51
  return () => {
52
52
  window.removeEventListener('resize', debouncedAction);
53
+ debouncedAction.cancel();
53
54
  };
54
55
  }, [parentRect, theme]);
55
56
  return (jsx("div", { ref: portalRef, role: "tooltip", "aria-hidden": !isVisible, className: CLASS_NAMES.container, style: isVisible ? style : undefined, children: jsx(ControlledTooltip, { horizontal: actualHorizontal, vertical: actualVertical, triggerIcon: isIcon, className: CLASS_NAMES.balloon, children: jsx("div", { id: messageId, className: CLASS_NAMES.balloonText, children: message }) }) }));
@@ -1 +1 @@
1
- {"version":3,"file":"TooltipPortal.js","sources":["../../../src/components/Tooltip/TooltipPortal.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactNode, useCallback, useState } from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useTheme } from '../../hooks/useTheme'\nimport { debounce } from '../../libs/debounce'\n\nimport { ControlledTooltip } from './ControlledTooltip'\n\ntype Props = {\n messageId: string\n message: ReactNode\n isVisible: boolean\n parentRect: DOMRect | null\n isIcon?: boolean\n}\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-Tooltip-popup shr-absolute shr-z-overlap aria-hidden:shr-hidden',\n balloon: 'shr-max-w-full [&&&]:shr-whitespace-normal',\n balloonText: 'shr-m-0 shr-px-1 shr-py-0.5',\n },\n})\n\nconst CLASS_NAMES = (() => {\n const { container, balloon, balloonText } = classNameGenerator()\n\n return {\n container: container(),\n balloon: balloon(),\n balloonText: balloonText(),\n }\n})()\n\nconst OUTER_MARGIN = 10\nconst SPACING = 5\n\ntype HorizontalType = 'left' | 'center' | 'right'\ntype VerticalType = 'top' | 'middle' | 'bottom'\n\nexport const TooltipPortal: FC<Props> = ({ messageId, message, isVisible, parentRect, isIcon }) => {\n const theme = useTheme()\n const [style, setStyle] = useState<{ [key: string]: undefined | string }>({})\n const [actualHorizontal, setActualHorizontal] = useState<HorizontalType>('center')\n const [actualVertical, setActualVertical] = useState<VerticalType>('bottom')\n\n const portalRef = useCallback(\n (element: HTMLDivElement | null) => {\n if (!element || !parentRect) {\n return\n }\n\n const action = () => {\n const vertical = calculateVertical(element.offsetHeight, parentRect)\n const horizontal = calculateHorizontal(element.offsetWidth, parentRect, theme)\n\n setStyle({\n insetBlockStart: vertical.insetBlockStart,\n insetInlineStart: horizontal.insetInlineStart,\n insetInlineEnd: horizontal.insetInlineEnd,\n maxWidth: horizontal.maxWidth,\n maxHeight: vertical.maxHeight,\n })\n setActualVertical(vertical.alignment)\n setActualHorizontal(horizontal.alignment)\n }\n const debouncedAction = debounce(action, 100)\n\n action()\n\n window.addEventListener('resize', debouncedAction)\n\n return () => {\n window.removeEventListener('resize', debouncedAction)\n }\n },\n [parentRect, theme],\n )\n\n return (\n <div\n ref={portalRef}\n role=\"tooltip\"\n aria-hidden={!isVisible}\n className={CLASS_NAMES.container}\n style={isVisible ? style : undefined}\n >\n <ControlledTooltip\n horizontal={actualHorizontal}\n vertical={actualVertical}\n triggerIcon={isIcon}\n className={CLASS_NAMES.balloon}\n >\n <div id={messageId} className={CLASS_NAMES.balloonText}>\n {message}\n </div>\n </ControlledTooltip>\n </div>\n )\n}\n\nconst calculateVertical = (\n portalHeight: number,\n parentRect: DOMRect,\n): { insetBlockStart: string; maxHeight: string | undefined; alignment: VerticalType } => {\n // トリガの上側の領域に収まる場合\n if (parentRect.top - portalHeight >= 0) {\n return {\n insetBlockStart: `${scrollY + parentRect.top - portalHeight - SPACING}px`,\n maxHeight: undefined,\n alignment: 'bottom',\n }\n }\n\n // トリガの下側の領域に収まる場合\n if (parentRect.bottom + portalHeight <= innerHeight) {\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: undefined,\n alignment: 'top',\n }\n }\n\n const triggerHeight = parentRect.bottom - parentRect.top\n\n // 上側の領域のほうが広い場合\n if (parentRect.top + triggerHeight / 2 >= innerHeight / 2) {\n return {\n insetBlockStart: `${scrollY + OUTER_MARGIN - SPACING}px`,\n maxHeight: `${parentRect.top - OUTER_MARGIN}px`,\n alignment: 'bottom',\n }\n }\n\n // 下側の領域のほうが広い場合\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: `${innerHeight - parentRect.bottom - OUTER_MARGIN}px`,\n alignment: 'top',\n }\n}\n\ntype ReturnCalculateHorizontalType = {\n insetInlineStart: string | undefined\n insetInlineEnd: string | undefined\n maxWidth: string\n alignment: HorizontalType\n}\nconst calculateHorizontal = (\n portalWidth: number,\n parentRect: DOMRect,\n theme: ReturnType<typeof useTheme>,\n): ReturnCalculateHorizontalType => {\n const triggerAlignCenter = parentRect.left + parentRect.width / 2\n const portalHalfWidth = portalWidth / 2\n const edgeSpacing = theme.spacingByChar(0.5)\n\n const leftSpacing = triggerAlignCenter - portalHalfWidth\n const rightSpacingEdge = document.body.clientWidth - SPACING\n\n // トリガを中心に左右に十分な余白がある場合\n if (leftSpacing > SPACING && triggerAlignCenter + portalHalfWidth < rightSpacingEdge) {\n const insetInlineStart = `${leftSpacing}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'center',\n }\n }\n\n // トリガが画面左寄りの場合\n if (triggerAlignCenter <= document.body.clientWidth / 2) {\n const insetInlineStart = `${scrollX + parentRect.left - SPACING}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'left',\n }\n }\n\n // トリガが画面右寄りの場合\n const insetInlineEnd = `${rightSpacingEdge - parentRect.right - scrollX}px`\n\n return {\n insetInlineStart: undefined,\n insetInlineEnd,\n maxWidth: `calc(100% - ${edgeSpacing} - max(${insetInlineEnd}, 0px))`,\n alignment: 'right',\n }\n}\n"],"names":[],"mappings":";;;;;;;;AAkBA;AACE;AACE;AACA;AACA;AACD;AACF;AAED;;;;;;;AAQA;AAEA;AACA;AAKO;AACL;;;;AAKA;AAEI;;;;;AAME;AAEA;;;;;;AAMC;AACD;AACA;AACF;;AAGA;AAEA;AAEA;AACE;AACF;AACF;AAIF;AAoBF;AAEA;;;;;AAQM;AACA;;;;;;;AAQA;AACA;;;;;AAOJ;;AAEI;AACA;AACA;;;;;;;AAQF;;AAEJ;AAQA;;AAME;;AAGA;;;;AAKE;;;AAIE;AACA;AACA;;;;;;;;AAUA;AACA;AACA;;;;;;AAQF;;AAEA;AACA;;AAEJ;;"}
1
+ {"version":3,"file":"TooltipPortal.js","sources":["../../../src/components/Tooltip/TooltipPortal.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactNode, useCallback, useState } from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useTheme } from '../../hooks/useTheme'\nimport { debounce } from '../../libs/debounce'\n\nimport { ControlledTooltip } from './ControlledTooltip'\n\ntype Props = {\n messageId: string\n message: ReactNode\n isVisible: boolean\n parentRect: DOMRect | null\n isIcon?: boolean\n}\n\nconst classNameGenerator = tv({\n slots: {\n container: 'smarthr-ui-Tooltip-popup shr-absolute shr-z-overlap aria-hidden:shr-hidden',\n balloon: 'shr-max-w-full [&&&]:shr-whitespace-normal',\n balloonText: 'shr-m-0 shr-px-1 shr-py-0.5',\n },\n})\n\nconst CLASS_NAMES = (() => {\n const { container, balloon, balloonText } = classNameGenerator()\n\n return {\n container: container(),\n balloon: balloon(),\n balloonText: balloonText(),\n }\n})()\n\nconst OUTER_MARGIN = 10\nconst SPACING = 5\n\ntype HorizontalType = 'left' | 'center' | 'right'\ntype VerticalType = 'top' | 'middle' | 'bottom'\n\nexport const TooltipPortal: FC<Props> = ({ messageId, message, isVisible, parentRect, isIcon }) => {\n const theme = useTheme()\n const [style, setStyle] = useState<{ [key: string]: undefined | string }>({})\n const [actualHorizontal, setActualHorizontal] = useState<HorizontalType>('center')\n const [actualVertical, setActualVertical] = useState<VerticalType>('bottom')\n\n const portalRef = useCallback(\n (element: HTMLDivElement | null) => {\n if (!element || !parentRect) {\n return\n }\n\n const action = () => {\n const vertical = calculateVertical(element.offsetHeight, parentRect)\n const horizontal = calculateHorizontal(element.offsetWidth, parentRect, theme)\n\n setStyle({\n insetBlockStart: vertical.insetBlockStart,\n insetInlineStart: horizontal.insetInlineStart,\n insetInlineEnd: horizontal.insetInlineEnd,\n maxWidth: horizontal.maxWidth,\n maxHeight: vertical.maxHeight,\n })\n setActualVertical(vertical.alignment)\n setActualHorizontal(horizontal.alignment)\n }\n const debouncedAction = debounce(action, 100)\n\n action()\n\n window.addEventListener('resize', debouncedAction)\n\n return () => {\n window.removeEventListener('resize', debouncedAction)\n debouncedAction.cancel()\n }\n },\n [parentRect, theme],\n )\n\n return (\n <div\n ref={portalRef}\n role=\"tooltip\"\n aria-hidden={!isVisible}\n className={CLASS_NAMES.container}\n style={isVisible ? style : undefined}\n >\n <ControlledTooltip\n horizontal={actualHorizontal}\n vertical={actualVertical}\n triggerIcon={isIcon}\n className={CLASS_NAMES.balloon}\n >\n <div id={messageId} className={CLASS_NAMES.balloonText}>\n {message}\n </div>\n </ControlledTooltip>\n </div>\n )\n}\n\nconst calculateVertical = (\n portalHeight: number,\n parentRect: DOMRect,\n): { insetBlockStart: string; maxHeight: string | undefined; alignment: VerticalType } => {\n // トリガの上側の領域に収まる場合\n if (parentRect.top - portalHeight >= 0) {\n return {\n insetBlockStart: `${scrollY + parentRect.top - portalHeight - SPACING}px`,\n maxHeight: undefined,\n alignment: 'bottom',\n }\n }\n\n // トリガの下側の領域に収まる場合\n if (parentRect.bottom + portalHeight <= innerHeight) {\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: undefined,\n alignment: 'top',\n }\n }\n\n const triggerHeight = parentRect.bottom - parentRect.top\n\n // 上側の領域のほうが広い場合\n if (parentRect.top + triggerHeight / 2 >= innerHeight / 2) {\n return {\n insetBlockStart: `${scrollY + OUTER_MARGIN - SPACING}px`,\n maxHeight: `${parentRect.top - OUTER_MARGIN}px`,\n alignment: 'bottom',\n }\n }\n\n // 下側の領域のほうが広い場合\n return {\n insetBlockStart: `${scrollY + parentRect.bottom + SPACING}px`,\n maxHeight: `${innerHeight - parentRect.bottom - OUTER_MARGIN}px`,\n alignment: 'top',\n }\n}\n\ntype ReturnCalculateHorizontalType = {\n insetInlineStart: string | undefined\n insetInlineEnd: string | undefined\n maxWidth: string\n alignment: HorizontalType\n}\nconst calculateHorizontal = (\n portalWidth: number,\n parentRect: DOMRect,\n theme: ReturnType<typeof useTheme>,\n): ReturnCalculateHorizontalType => {\n const triggerAlignCenter = parentRect.left + parentRect.width / 2\n const portalHalfWidth = portalWidth / 2\n const edgeSpacing = theme.spacingByChar(0.5)\n\n const leftSpacing = triggerAlignCenter - portalHalfWidth\n const rightSpacingEdge = document.body.clientWidth - SPACING\n\n // トリガを中心に左右に十分な余白がある場合\n if (leftSpacing > SPACING && triggerAlignCenter + portalHalfWidth < rightSpacingEdge) {\n const insetInlineStart = `${leftSpacing}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'center',\n }\n }\n\n // トリガが画面左寄りの場合\n if (triggerAlignCenter <= document.body.clientWidth / 2) {\n const insetInlineStart = `${scrollX + parentRect.left - SPACING}px`\n\n return {\n insetInlineStart,\n insetInlineEnd: undefined,\n maxWidth: `calc(100% - max(${insetInlineStart}, 0px) - ${edgeSpacing})`,\n alignment: 'left',\n }\n }\n\n // トリガが画面右寄りの場合\n const insetInlineEnd = `${rightSpacingEdge - parentRect.right - scrollX}px`\n\n return {\n insetInlineStart: undefined,\n insetInlineEnd,\n maxWidth: `calc(100% - ${edgeSpacing} - max(${insetInlineEnd}, 0px))`,\n alignment: 'right',\n }\n}\n"],"names":[],"mappings":";;;;;;;;AAkBA;AACE;AACE;AACA;AACA;AACD;AACF;AAED;;;;;;;AAQA;AAEA;AACA;AAKO;AACL;;;;AAKA;AAEI;;;;;AAME;AAEA;;;;;;AAMC;AACD;AACA;AACF;;AAGA;AAEA;AAEA;AACE;;AAEF;AACF;AAIF;AAoBF;AAEA;;;;;AAQM;AACA;;;;;;;AAQA;AACA;;;;;AAOJ;;AAEI;AACA;AACA;;;;;;;AAQF;;AAEJ;AAQA;;AAME;;AAGA;;;;AAKE;;;AAIE;AACA;AACA;;;;;;;;AAUA;AACA;AACA;;;;;;AAQF;;AAEA;AACA;;AAEJ;;"}
@@ -1,21 +1,24 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
+ var hooks_useLatest = require('./useLatest.cjs');
4
5
 
6
+ // TODO: useOuterClickと統合する
5
7
  function useClick(innerRefs, innerCallback, outerCallback) {
8
+ const latest = hooks_useLatest.useLatest({ innerRefs, innerCallback, outerCallback });
6
9
  React.useEffect(() => {
7
10
  const handleClick = (e) => {
8
- if (innerRefs.some((target) => isEventIncludedParent(e, target.current))) {
9
- innerCallback(e);
11
+ if (latest.innerRefs.some((target) => isEventIncludedParent(e, target.current))) {
12
+ latest.innerCallback(e);
10
13
  return;
11
14
  }
12
- outerCallback(e);
15
+ latest.outerCallback(e);
13
16
  };
14
17
  window.addEventListener('click', handleClick);
15
18
  return () => {
16
19
  window.removeEventListener('click', handleClick);
17
20
  };
18
- }, [innerRefs, innerCallback, outerCallback]);
21
+ }, [latest]);
19
22
  }
20
23
  function isEventIncludedParent(e, parent) {
21
24
  if (!parent)
@@ -1 +1 @@
1
- {"version":3,"file":"useClick.cjs","sources":["../../src/hooks/useClick.ts"],"sourcesContent":["import { type RefObject, useEffect } from 'react'\n\nexport function useClick(\n innerRefs: Array<RefObject<HTMLElement>>,\n innerCallback: (e: MouseEvent) => void,\n outerCallback: (e: MouseEvent) => void,\n) {\n useEffect(() => {\n const handleClick = (e: MouseEvent) => {\n if (innerRefs.some((target) => isEventIncludedParent(e, target.current))) {\n innerCallback(e)\n\n return\n }\n\n outerCallback(e)\n }\n\n window.addEventListener('click', handleClick)\n\n return () => {\n window.removeEventListener('click', handleClick)\n }\n }, [innerRefs, innerCallback, outerCallback])\n}\n\nfunction isEventIncludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return path.includes(parent)\n}\n"],"names":["useEffect"],"mappings":";;;;SAEgB,QAAQ,CACtB,SAAwC,EACxC,aAAsC,EACtC,aAAsC,EAAA;IAEtCA,eAAS,CAAC,MAAK;AACb,QAAA,MAAM,WAAW,GAAG,CAAC,CAAa,KAAI;AACpC,YAAA,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;gBACxE,aAAa,CAAC,CAAC,CAAC;gBAEhB;YACF;YAEA,aAAa,CAAC,CAAC,CAAC;AAClB,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC;AAE7C,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC;AAClD,QAAA,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;AAC/C;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC9B;;;;"}
1
+ {"version":3,"file":"useClick.cjs","sources":["../../src/hooks/useClick.ts"],"sourcesContent":["import { type RefObject, useEffect } from 'react'\n\nimport { useLatest } from './useLatest'\n\n// TODO: useOuterClickと統合する\nexport function useClick(\n innerRefs: Array<RefObject<HTMLElement>>,\n innerCallback: (e: MouseEvent) => void,\n outerCallback: (e: MouseEvent) => void,\n) {\n const latest = useLatest({ innerRefs, innerCallback, outerCallback })\n\n useEffect(() => {\n const handleClick = (e: MouseEvent) => {\n if (latest.innerRefs.some((target) => isEventIncludedParent(e, target.current))) {\n latest.innerCallback(e)\n\n return\n }\n\n latest.outerCallback(e)\n }\n\n window.addEventListener('click', handleClick)\n\n return () => {\n window.removeEventListener('click', handleClick)\n }\n }, [latest])\n}\n\nfunction isEventIncludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return path.includes(parent)\n}\n"],"names":["useLatest","useEffect"],"mappings":";;;;;AAIA;SACgB,QAAQ,CACtB,SAAwC,EACxC,aAAsC,EACtC,aAAsC,EAAA;AAEtC,IAAA,MAAM,MAAM,GAAGA,yBAAS,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;IAErEC,eAAS,CAAC,MAAK;AACb,QAAA,MAAM,WAAW,GAAG,CAAC,CAAa,KAAI;YACpC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;AAC/E,gBAAA,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;gBAEvB;YACF;AAEA,YAAA,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;AACzB,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC;AAE7C,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC;AAClD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AACd;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC9B;;;;"}
@@ -1,19 +1,22 @@
1
1
  import { useEffect } from 'react';
2
+ import { useLatest } from './useLatest.js';
2
3
 
4
+ // TODO: useOuterClickと統合する
3
5
  function useClick(innerRefs, innerCallback, outerCallback) {
6
+ const latest = useLatest({ innerRefs, innerCallback, outerCallback });
4
7
  useEffect(() => {
5
8
  const handleClick = (e) => {
6
- if (innerRefs.some((target) => isEventIncludedParent(e, target.current))) {
7
- innerCallback(e);
9
+ if (latest.innerRefs.some((target) => isEventIncludedParent(e, target.current))) {
10
+ latest.innerCallback(e);
8
11
  return;
9
12
  }
10
- outerCallback(e);
13
+ latest.outerCallback(e);
11
14
  };
12
15
  window.addEventListener('click', handleClick);
13
16
  return () => {
14
17
  window.removeEventListener('click', handleClick);
15
18
  };
16
- }, [innerRefs, innerCallback, outerCallback]);
19
+ }, [latest]);
17
20
  }
18
21
  function isEventIncludedParent(e, parent) {
19
22
  if (!parent)
@@ -1 +1 @@
1
- {"version":3,"file":"useClick.js","sources":["../../src/hooks/useClick.ts"],"sourcesContent":["import { type RefObject, useEffect } from 'react'\n\nexport function useClick(\n innerRefs: Array<RefObject<HTMLElement>>,\n innerCallback: (e: MouseEvent) => void,\n outerCallback: (e: MouseEvent) => void,\n) {\n useEffect(() => {\n const handleClick = (e: MouseEvent) => {\n if (innerRefs.some((target) => isEventIncludedParent(e, target.current))) {\n innerCallback(e)\n\n return\n }\n\n outerCallback(e)\n }\n\n window.addEventListener('click', handleClick)\n\n return () => {\n window.removeEventListener('click', handleClick)\n }\n }, [innerRefs, innerCallback, outerCallback])\n}\n\nfunction isEventIncludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return path.includes(parent)\n}\n"],"names":[],"mappings":";;SAEgB,QAAQ,CACtB,SAAwC,EACxC,aAAsC,EACtC,aAAsC,EAAA;IAEtC,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,WAAW,GAAG,CAAC,CAAa,KAAI;AACpC,YAAA,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;gBACxE,aAAa,CAAC,CAAC,CAAC;gBAEhB;YACF;YAEA,aAAa,CAAC,CAAC,CAAC;AAClB,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC;AAE7C,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC;AAClD,QAAA,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,EAAE,aAAa,EAAE,aAAa,CAAC,CAAC;AAC/C;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC9B;;;;"}
1
+ {"version":3,"file":"useClick.js","sources":["../../src/hooks/useClick.ts"],"sourcesContent":["import { type RefObject, useEffect } from 'react'\n\nimport { useLatest } from './useLatest'\n\n// TODO: useOuterClickと統合する\nexport function useClick(\n innerRefs: Array<RefObject<HTMLElement>>,\n innerCallback: (e: MouseEvent) => void,\n outerCallback: (e: MouseEvent) => void,\n) {\n const latest = useLatest({ innerRefs, innerCallback, outerCallback })\n\n useEffect(() => {\n const handleClick = (e: MouseEvent) => {\n if (latest.innerRefs.some((target) => isEventIncludedParent(e, target.current))) {\n latest.innerCallback(e)\n\n return\n }\n\n latest.outerCallback(e)\n }\n\n window.addEventListener('click', handleClick)\n\n return () => {\n window.removeEventListener('click', handleClick)\n }\n }, [latest])\n}\n\nfunction isEventIncludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return path.includes(parent)\n}\n"],"names":[],"mappings":";;;AAIA;SACgB,QAAQ,CACtB,SAAwC,EACxC,aAAsC,EACtC,aAAsC,EAAA;AAEtC,IAAA,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;IAErE,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,WAAW,GAAG,CAAC,CAAa,KAAI;YACpC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;AAC/E,gBAAA,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;gBAEvB;YACF;AAEA,YAAA,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;AACzB,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC;AAE7C,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,WAAW,CAAC;AAClD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AACd;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC9B;;;;"}
@@ -1,22 +1,21 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
+ var hooks_useLatest = require('./useLatest.cjs');
4
5
 
5
6
  function useOuterClick(targets, callback) {
6
- // callbackをrefに保存(毎レンダリング時に最新の値を設定)
7
- const callbackRef = React.useRef(callback);
8
- callbackRef.current = callback;
7
+ const latest = hooks_useLatest.useLatest({ targets, callback });
9
8
  React.useEffect(() => {
10
9
  const handleOuterClick = (e) => {
11
- if (targets.every((target) => isEventExcludedParent(e, target.current))) {
12
- callbackRef.current(e);
10
+ if (latest.targets.every((target) => isEventExcludedParent(e, target.current))) {
11
+ latest.callback(e);
13
12
  }
14
13
  };
15
14
  window.addEventListener('click', handleOuterClick);
16
15
  return () => {
17
16
  window.removeEventListener('click', handleOuterClick);
18
17
  };
19
- }, [targets]);
18
+ }, [latest]);
20
19
  }
21
20
  function isEventExcludedParent(e, parent) {
22
21
  if (!parent)
@@ -1 +1 @@
1
- {"version":3,"file":"useOuterClick.cjs","sources":["../../src/hooks/useOuterClick.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react'\n\nexport function useOuterClick(\n targets: Array<RefObject<HTMLElement>>,\n callback: (e: MouseEvent) => void,\n) {\n // callbackをrefに保存(毎レンダリング時に最新の値を設定)\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n const handleOuterClick = (e: MouseEvent) => {\n if (targets.every((target) => isEventExcludedParent(e, target.current))) {\n callbackRef.current(e)\n }\n }\n\n window.addEventListener('click', handleOuterClick)\n\n return () => {\n window.removeEventListener('click', handleOuterClick)\n }\n }, [targets])\n}\n\nfunction isEventExcludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return !path.includes(parent)\n}\n"],"names":["useRef","useEffect"],"mappings":";;;;AAEM,SAAU,aAAa,CAC3B,OAAsC,EACtC,QAAiC,EAAA;;AAGjC,IAAA,MAAM,WAAW,GAAGA,YAAM,CAAC,QAAQ,CAAC;AACpC,IAAA,WAAW,CAAC,OAAO,GAAG,QAAQ;IAE9BC,eAAS,CAAC,MAAK;AACb,QAAA,MAAM,gBAAgB,GAAG,CAAC,CAAa,KAAI;AACzC,YAAA,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;AACvE,gBAAA,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;YACxB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AAElD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AACvD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AACf;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC/B;;;;"}
1
+ {"version":3,"file":"useOuterClick.cjs","sources":["../../src/hooks/useOuterClick.ts"],"sourcesContent":["import { type RefObject, useEffect } from 'react'\n\nimport { useLatest } from './useLatest'\n\nexport function useOuterClick(\n targets: Array<RefObject<HTMLElement>>,\n callback: (e: MouseEvent) => void,\n) {\n const latest = useLatest({ targets, callback })\n\n useEffect(() => {\n const handleOuterClick = (e: MouseEvent) => {\n if (latest.targets.every((target) => isEventExcludedParent(e, target.current))) {\n latest.callback(e)\n }\n }\n\n window.addEventListener('click', handleOuterClick)\n\n return () => {\n window.removeEventListener('click', handleOuterClick)\n }\n }, [latest])\n}\n\nfunction isEventExcludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return !path.includes(parent)\n}\n"],"names":["useLatest","useEffect"],"mappings":";;;;;AAIM,SAAU,aAAa,CAC3B,OAAsC,EACtC,QAAiC,EAAA;IAEjC,MAAM,MAAM,GAAGA,yBAAS,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAE/CC,eAAS,CAAC,MAAK;AACb,QAAA,MAAM,gBAAgB,GAAG,CAAC,CAAa,KAAI;YACzC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;AAC9E,gBAAA,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YACpB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AAElD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AACvD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AACd;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC/B;;;;"}
@@ -1,20 +1,19 @@
1
- import { useRef, useEffect } from 'react';
1
+ import { useEffect } from 'react';
2
+ import { useLatest } from './useLatest.js';
2
3
 
3
4
  function useOuterClick(targets, callback) {
4
- // callbackをrefに保存(毎レンダリング時に最新の値を設定)
5
- const callbackRef = useRef(callback);
6
- callbackRef.current = callback;
5
+ const latest = useLatest({ targets, callback });
7
6
  useEffect(() => {
8
7
  const handleOuterClick = (e) => {
9
- if (targets.every((target) => isEventExcludedParent(e, target.current))) {
10
- callbackRef.current(e);
8
+ if (latest.targets.every((target) => isEventExcludedParent(e, target.current))) {
9
+ latest.callback(e);
11
10
  }
12
11
  };
13
12
  window.addEventListener('click', handleOuterClick);
14
13
  return () => {
15
14
  window.removeEventListener('click', handleOuterClick);
16
15
  };
17
- }, [targets]);
16
+ }, [latest]);
18
17
  }
19
18
  function isEventExcludedParent(e, parent) {
20
19
  if (!parent)
@@ -1 +1 @@
1
- {"version":3,"file":"useOuterClick.js","sources":["../../src/hooks/useOuterClick.ts"],"sourcesContent":["import { type RefObject, useEffect, useRef } from 'react'\n\nexport function useOuterClick(\n targets: Array<RefObject<HTMLElement>>,\n callback: (e: MouseEvent) => void,\n) {\n // callbackをrefに保存(毎レンダリング時に最新の値を設定)\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n const handleOuterClick = (e: MouseEvent) => {\n if (targets.every((target) => isEventExcludedParent(e, target.current))) {\n callbackRef.current(e)\n }\n }\n\n window.addEventListener('click', handleOuterClick)\n\n return () => {\n window.removeEventListener('click', handleOuterClick)\n }\n }, [targets])\n}\n\nfunction isEventExcludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return !path.includes(parent)\n}\n"],"names":[],"mappings":";;AAEM,SAAU,aAAa,CAC3B,OAAsC,EACtC,QAAiC,EAAA;;AAGjC,IAAA,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC;AACpC,IAAA,WAAW,CAAC,OAAO,GAAG,QAAQ;IAE9B,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,gBAAgB,GAAG,CAAC,CAAa,KAAI;AACzC,YAAA,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;AACvE,gBAAA,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;YACxB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AAElD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AACvD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC;AACf;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC/B;;;;"}
1
+ {"version":3,"file":"useOuterClick.js","sources":["../../src/hooks/useOuterClick.ts"],"sourcesContent":["import { type RefObject, useEffect } from 'react'\n\nimport { useLatest } from './useLatest'\n\nexport function useOuterClick(\n targets: Array<RefObject<HTMLElement>>,\n callback: (e: MouseEvent) => void,\n) {\n const latest = useLatest({ targets, callback })\n\n useEffect(() => {\n const handleOuterClick = (e: MouseEvent) => {\n if (latest.targets.every((target) => isEventExcludedParent(e, target.current))) {\n latest.callback(e)\n }\n }\n\n window.addEventListener('click', handleOuterClick)\n\n return () => {\n window.removeEventListener('click', handleOuterClick)\n }\n }, [latest])\n}\n\nfunction isEventExcludedParent(e: MouseEvent, parent: Element | null): boolean {\n if (!parent) return false\n\n const path = e.composedPath()\n\n if (path.length === 0) return false\n\n return !path.includes(parent)\n}\n"],"names":[],"mappings":";;;AAIM,SAAU,aAAa,CAC3B,OAAsC,EACtC,QAAiC,EAAA;IAEjC,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAE/C,SAAS,CAAC,MAAK;AACb,QAAA,MAAM,gBAAgB,GAAG,CAAC,CAAa,KAAI;YACzC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE;AAC9E,gBAAA,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YACpB;AACF,QAAA,CAAC;AAED,QAAA,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AAElD,QAAA,OAAO,MAAK;AACV,YAAA,MAAM,CAAC,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,CAAC;AACvD,QAAA,CAAC;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;AACd;AAEA,SAAS,qBAAqB,CAAC,CAAa,EAAE,MAAsB,EAAA;AAClE,IAAA,IAAI,CAAC,MAAM;AAAE,QAAA,OAAO,KAAK;AAEzB,IAAA,MAAM,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE;AAE7B,IAAA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,KAAK;AAEnC,IAAA,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;AAC/B;;;;"}
@@ -4,6 +4,7 @@ var jsxRuntime = require('react/jsx-runtime');
4
4
  var React = require('react');
5
5
  var ReactDOM = require('react-dom');
6
6
  var hooks_useEnhancedEffect = require('./useEnhancedEffect.cjs');
7
+ var hooks_useLatest = require('./useLatest.cjs');
7
8
 
8
9
  const ParentContext = React.createContext({
9
10
  seqs: [],
@@ -11,7 +12,7 @@ const ParentContext = React.createContext({
11
12
  let portalSeq = 0;
12
13
  function usePortal({ rootId } = {}) {
13
14
  const [portalRoot, setPortalRoot] = React.useState(null);
14
- const currentSeq = React.useMemo(() => ++portalSeq, []);
15
+ const [currentSeq] = React.useState(() => ++portalSeq);
15
16
  const parent = React.useContext(ParentContext);
16
17
  const calculatedSeqs = React.useMemo(() => {
17
18
  const parentSeqs = parent.seqs.concat(currentSeq);
@@ -20,6 +21,25 @@ function usePortal({ rootId } = {}) {
20
21
  portalChildOf: parentSeqs.join(','),
21
22
  };
22
23
  }, [currentSeq, parent.seqs]);
24
+ const latest = hooks_useLatest.useLatest({ currentSeq, portalRoot, calculatedSeqs });
25
+ const functions = React.useMemo(() => {
26
+ const PortalParentProvider = ({ children }) => {
27
+ const value = {
28
+ seqs: latest.calculatedSeqs.parentSeqs,
29
+ };
30
+ return jsxRuntime.jsx(ParentContext.Provider, { value: value, children: children });
31
+ };
32
+ return {
33
+ PortalParentProvider,
34
+ isChildPortal: (element) => _isChildPortal(element, new RegExp(`(^|,)${latest.currentSeq}(,|$)`)),
35
+ createPortal: (children) => {
36
+ if (latest.portalRoot === null) {
37
+ return null;
38
+ }
39
+ return ReactDOM.createPortal(children, latest.portalRoot);
40
+ },
41
+ };
42
+ }, [latest]);
23
43
  hooks_useEnhancedEffect.useEnhancedEffect(() => {
24
44
  // Next.jsのhydration error回避のため、初回レンダリング時にdivを作成する
25
45
  setPortalRoot((current) => {
@@ -40,24 +60,11 @@ function usePortal({ rootId } = {}) {
40
60
  portalRoot.remove();
41
61
  };
42
62
  }, [portalRoot, calculatedSeqs.portalChildOf]);
43
- const isChildPortal = React.useCallback((element) => _isChildPortal(element, new RegExp(`(^|,)${currentSeq}(,|$)`)), [currentSeq]);
44
- const PortalParentProvider = React.useCallback(({ children }) => {
45
- const value = {
46
- seqs: calculatedSeqs.parentSeqs,
47
- };
48
- return jsxRuntime.jsx(ParentContext.Provider, { value: value, children: children });
49
- }, [calculatedSeqs.parentSeqs]);
50
- const wrappedCreatePortal = React.useCallback((children) => {
51
- if (portalRoot === null) {
52
- return null;
53
- }
54
- return ReactDOM.createPortal(children, portalRoot);
55
- }, [portalRoot]);
56
63
  return {
57
64
  portalRoot,
58
- isChildPortal,
59
- PortalParentProvider,
60
- createPortal: wrappedCreatePortal,
65
+ isChildPortal: functions.isChildPortal,
66
+ createPortal: functions.createPortal,
67
+ PortalParentProvider: functions.PortalParentProvider,
61
68
  };
62
69
  }
63
70
  function _isChildPortal(element, seqRegex) {
@@ -1 +1 @@
1
- {"version":3,"file":"usePortal.cjs","sources":["../../src/hooks/usePortal.tsx"],"sourcesContent":["import {\n type FC,\n type ReactNode,\n createContext,\n useCallback,\n useContext,\n useMemo,\n useState,\n} from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { useEnhancedEffect } from './useEnhancedEffect'\n\ntype ParentContextValue = {\n seqs: number[]\n}\n\nconst ParentContext = createContext<ParentContextValue>({\n seqs: [],\n})\n\nlet portalSeq = 0\n\nexport function usePortal({ rootId }: { rootId?: string } = {}) {\n const [portalRoot, setPortalRoot] = useState<HTMLDivElement | null>(null)\n const currentSeq = useMemo(() => ++portalSeq, [])\n const parent = useContext(ParentContext)\n\n const calculatedSeqs = useMemo(() => {\n const parentSeqs = parent.seqs.concat(currentSeq)\n\n return {\n parentSeqs,\n portalChildOf: parentSeqs.join(','),\n }\n }, [currentSeq, parent.seqs])\n\n useEnhancedEffect(() => {\n // Next.jsのhydration error回避のため、初回レンダリング時にdivを作成する\n setPortalRoot((current) => {\n const root = current || document.createElement('div')\n\n if (rootId) {\n root.setAttribute('id', rootId)\n }\n\n return root\n })\n }, [rootId])\n\n useEnhancedEffect(() => {\n if (!portalRoot) {\n return\n }\n\n portalRoot.dataset.portalChildOf = calculatedSeqs.portalChildOf\n document.body.appendChild(portalRoot)\n\n return () => {\n portalRoot.remove()\n }\n }, [portalRoot, calculatedSeqs.portalChildOf])\n\n const isChildPortal = useCallback(\n (element: HTMLElement | null) => _isChildPortal(element, new RegExp(`(^|,)${currentSeq}(,|$)`)),\n [currentSeq],\n )\n\n const PortalParentProvider: FC<{ children: ReactNode }> = useCallback(\n ({ children }) => {\n const value: ParentContextValue = {\n seqs: calculatedSeqs.parentSeqs,\n }\n\n return <ParentContext.Provider value={value}>{children}</ParentContext.Provider>\n },\n [calculatedSeqs.parentSeqs],\n )\n\n const wrappedCreatePortal = useCallback(\n (children: ReactNode) => {\n if (portalRoot === null) {\n return null\n }\n\n return createPortal(children, portalRoot)\n },\n [portalRoot],\n )\n\n return {\n portalRoot,\n isChildPortal,\n PortalParentProvider,\n createPortal: wrappedCreatePortal,\n }\n}\n\nfunction _isChildPortal(element: HTMLElement | SVGElement | null, seqRegex: RegExp): boolean {\n if (!element) return false\n\n let includesSeq = false\n const childOf = element.dataset?.portalChildOf\n\n if (childOf) {\n includesSeq = seqRegex.test(childOf)\n }\n\n return includesSeq || _isChildPortal(element.parentElement, seqRegex)\n}\n"],"names":["createContext","useState","useMemo","useContext","useEnhancedEffect","useCallback","_jsx","createPortal"],"mappings":";;;;;;;AAiBA,MAAM,aAAa,GAAGA,mBAAa,CAAqB;AACtD,IAAA,IAAI,EAAE,EAAE;AACT,CAAA,CAAC;AAEF,IAAI,SAAS,GAAG,CAAC;SAED,SAAS,CAAC,EAAE,MAAM,KAA0B,EAAE,EAAA;IAC5D,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAGC,cAAQ,CAAwB,IAAI,CAAC;AACzE,IAAA,MAAM,UAAU,GAAGC,aAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC;AACjD,IAAA,MAAM,MAAM,GAAGC,gBAAU,CAAC,aAAa,CAAC;AAExC,IAAA,MAAM,cAAc,GAAGD,aAAO,CAAC,MAAK;QAClC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC;QAEjD,OAAO;YACL,UAAU;AACV,YAAA,aAAa,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC;SACpC;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IAE7BE,yCAAiB,CAAC,MAAK;;AAErB,QAAA,aAAa,CAAC,CAAC,OAAO,KAAI;YACxB,MAAM,IAAI,GAAG,OAAO,IAAI,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC;YAErD,IAAI,MAAM,EAAE;AACV,gBAAA,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC;YACjC;AAEA,YAAA,OAAO,IAAI;AACb,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;IAEZA,yCAAiB,CAAC,MAAK;QACrB,IAAI,CAAC,UAAU,EAAE;YACf;QACF;QAEA,UAAU,CAAC,OAAO,CAAC,aAAa,GAAG,cAAc,CAAC,aAAa;AAC/D,QAAA,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC;AAErC,QAAA,OAAO,MAAK;YACV,UAAU,CAAC,MAAM,EAAE;AACrB,QAAA,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IAE9C,MAAM,aAAa,GAAGC,iBAAW,CAC/B,CAAC,OAA2B,KAAK,cAAc,CAAC,OAAO,EAAE,IAAI,MAAM,CAAC,CAAA,KAAA,EAAQ,UAAU,CAAA,KAAA,CAAO,CAAC,CAAC,EAC/F,CAAC,UAAU,CAAC,CACb;IAED,MAAM,oBAAoB,GAAgCA,iBAAW,CACnE,CAAC,EAAE,QAAQ,EAAE,KAAI;AACf,QAAA,MAAM,KAAK,GAAuB;YAChC,IAAI,EAAE,cAAc,CAAC,UAAU;SAChC;QAED,OAAOC,cAAA,CAAC,aAAa,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,KAAK,EAAA,QAAA,EAAG,QAAQ,EAAA,CAA0B;AAClF,IAAA,CAAC,EACD,CAAC,cAAc,CAAC,UAAU,CAAC,CAC5B;AAED,IAAA,MAAM,mBAAmB,GAAGD,iBAAW,CACrC,CAAC,QAAmB,KAAI;AACtB,QAAA,IAAI,UAAU,KAAK,IAAI,EAAE;AACvB,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,OAAOE,qBAAY,CAAC,QAAQ,EAAE,UAAU,CAAC;AAC3C,IAAA,CAAC,EACD,CAAC,UAAU,CAAC,CACb;IAED,OAAO;QACL,UAAU;QACV,aAAa;QACb,oBAAoB;AACpB,QAAA,YAAY,EAAE,mBAAmB;KAClC;AACH;AAEA,SAAS,cAAc,CAAC,OAAwC,EAAE,QAAgB,EAAA;AAChF,IAAA,IAAI,CAAC,OAAO;AAAE,QAAA,OAAO,KAAK;IAE1B,IAAI,WAAW,GAAG,KAAK;AACvB,IAAA,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,aAAa;IAE9C,IAAI,OAAO,EAAE;AACX,QAAA,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;IACtC;IAEA,OAAO,WAAW,IAAI,cAAc,CAAC,OAAO,CAAC,aAAa,EAAE,QAAQ,CAAC;AACvE;;;;"}
1
+ {"version":3,"file":"usePortal.cjs","sources":["../../src/hooks/usePortal.tsx"],"sourcesContent":["import { type FC, type ReactNode, createContext, useContext, useMemo, useState } from 'react'\nimport { createPortal } from 'react-dom'\n\nimport { useEnhancedEffect } from './useEnhancedEffect'\nimport { useLatest } from './useLatest'\n\ntype ParentContextValue = {\n seqs: number[]\n}\n\nconst ParentContext = createContext<ParentContextValue>({\n seqs: [],\n})\n\nlet portalSeq = 0\n\nexport function usePortal({ rootId }: { rootId?: string } = {}) {\n const [portalRoot, setPortalRoot] = useState<HTMLDivElement | null>(null)\n const [currentSeq] = useState(() => ++portalSeq)\n const parent = useContext(ParentContext)\n\n const calculatedSeqs = useMemo(() => {\n const parentSeqs = parent.seqs.concat(currentSeq)\n\n return {\n parentSeqs,\n portalChildOf: parentSeqs.join(','),\n }\n }, [currentSeq, parent.seqs])\n\n const latest = useLatest({ currentSeq, portalRoot, calculatedSeqs })\n\n const functions = useMemo(() => {\n const PortalParentProvider: FC<{ children: ReactNode }> = ({ children }) => {\n const value: ParentContextValue = {\n seqs: latest.calculatedSeqs.parentSeqs,\n }\n\n return <ParentContext.Provider value={value}>{children}</ParentContext.Provider>\n }\n\n return {\n PortalParentProvider,\n isChildPortal: (element: HTMLElement | null) =>\n _isChildPortal(element, new RegExp(`(^|,)${latest.currentSeq}(,|$)`)),\n createPortal: (children: ReactNode) => {\n if (latest.portalRoot === null) {\n return null\n }\n\n return createPortal(children, latest.portalRoot)\n },\n }\n }, [latest])\n\n useEnhancedEffect(() => {\n // Next.jsのhydration error回避のため、初回レンダリング時にdivを作成する\n setPortalRoot((current) => {\n const root = current || document.createElement('div')\n\n if (rootId) {\n root.setAttribute('id', rootId)\n }\n\n return root\n })\n }, [rootId])\n\n useEnhancedEffect(() => {\n if (!portalRoot) {\n return\n }\n\n portalRoot.dataset.portalChildOf = calculatedSeqs.portalChildOf\n document.body.appendChild(portalRoot)\n\n return () => {\n portalRoot.remove()\n }\n }, [portalRoot, calculatedSeqs.portalChildOf])\n\n return {\n portalRoot,\n isChildPortal: functions.isChildPortal,\n createPortal: functions.createPortal,\n PortalParentProvider: functions.PortalParentProvider,\n }\n}\n\nfunction _isChildPortal(element: HTMLElement | SVGElement | null, seqRegex: RegExp): boolean {\n if (!element) return false\n\n let includesSeq = false\n const childOf = element.dataset?.portalChildOf\n\n if (childOf) {\n includesSeq = seqRegex.test(childOf)\n }\n\n return includesSeq || _isChildPortal(element.parentElement, seqRegex)\n}\n"],"names":["createContext","useState","useContext","useMemo","useLatest","_jsx","createPortal","useEnhancedEffect"],"mappings":"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"}
@@ -4,8 +4,8 @@ export declare function usePortal({ rootId }?: {
4
4
  }): {
5
5
  portalRoot: HTMLDivElement | null;
6
6
  isChildPortal: (element: HTMLElement | null) => boolean;
7
+ createPortal: (children: ReactNode) => import("react").ReactPortal | null;
7
8
  PortalParentProvider: FC<{
8
9
  children: ReactNode;
9
10
  }>;
10
- createPortal: (children: ReactNode) => import("react").ReactPortal | null;
11
11
  };