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.
- package/lib/_virtual/index.cjs +2 -2
- package/lib/_virtual/index.js +2 -2
- package/lib/_virtual/index2.cjs +2 -2
- package/lib/_virtual/index2.js +2 -2
- package/lib/components/Combobox/ItemButton.cjs +11 -2
- package/lib/components/Combobox/ItemButton.cjs.map +1 -1
- package/lib/components/Combobox/ItemButton.js +11 -2
- package/lib/components/Combobox/ItemButton.js.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.js +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.js.map +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs.map +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.js +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.js.map +1 -1
- package/lib/components/DatePicker/DatePicker.cjs +1 -1
- package/lib/components/DatePicker/DatePicker.cjs.map +1 -1
- package/lib/components/DatePicker/DatePicker.js +1 -1
- package/lib/components/DatePicker/DatePicker.js.map +1 -1
- package/lib/components/Dialog/FocusTrap.cjs +8 -8
- package/lib/components/Dialog/FocusTrap.cjs.map +1 -1
- package/lib/components/Dialog/FocusTrap.js +9 -9
- package/lib/components/Dialog/FocusTrap.js.map +1 -1
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs +2 -1
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.cjs.map +1 -1
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.js +2 -1
- package/lib/components/Dialog/ModelessDialog/ModelessDialog.js.map +1 -1
- package/lib/components/Input/CurrencyInput/CurrencyInput.cjs +2 -2
- package/lib/components/Input/CurrencyInput/CurrencyInput.cjs.map +1 -1
- package/lib/components/Input/CurrencyInput/CurrencyInput.js +2 -2
- package/lib/components/Input/CurrencyInput/CurrencyInput.js.map +1 -1
- package/lib/components/Input/Input.cjs +14 -16
- package/lib/components/Input/Input.cjs.map +1 -1
- package/lib/components/Input/Input.js +14 -16
- package/lib/components/Input/Input.js.map +1 -1
- package/lib/components/InputFile/InputFileMultiplyAppendable.cjs +8 -7
- package/lib/components/InputFile/InputFileMultiplyAppendable.cjs.map +1 -1
- package/lib/components/InputFile/InputFileMultiplyAppendable.js +8 -7
- package/lib/components/InputFile/InputFileMultiplyAppendable.js.map +1 -1
- package/lib/components/InputFile/InputFileNative.cjs +6 -5
- package/lib/components/InputFile/InputFileNative.cjs.map +1 -1
- package/lib/components/InputFile/InputFileNative.js +6 -5
- package/lib/components/InputFile/InputFileNative.js.map +1 -1
- package/lib/components/Layout/Container/Container.cjs.map +1 -1
- package/lib/components/Layout/Container/Container.js.map +1 -1
- package/lib/components/Pagination/Pagination.cjs +3 -2
- package/lib/components/Pagination/Pagination.cjs.map +1 -1
- package/lib/components/Pagination/Pagination.js +2 -1
- package/lib/components/Pagination/Pagination.js.map +1 -1
- package/lib/components/Panel/Groupbox/Groupbox.cjs +1 -0
- package/lib/components/Panel/Groupbox/Groupbox.cjs.map +1 -1
- package/lib/components/Panel/Groupbox/Groupbox.js +1 -0
- package/lib/components/Panel/Groupbox/Groupbox.js.map +1 -1
- package/lib/components/Select/Select.cjs +12 -14
- package/lib/components/Select/Select.cjs.map +1 -1
- package/lib/components/Select/Select.js +12 -14
- package/lib/components/Select/Select.js.map +1 -1
- package/lib/components/Table/TableScroller.cjs +13 -15
- package/lib/components/Table/TableScroller.cjs.map +1 -1
- package/lib/components/Table/TableScroller.js +14 -16
- package/lib/components/Table/TableScroller.js.map +1 -1
- package/lib/components/Textarea/Textarea.cjs +18 -12
- package/lib/components/Textarea/Textarea.cjs.map +1 -1
- package/lib/components/Textarea/Textarea.js +19 -13
- package/lib/components/Textarea/Textarea.js.map +1 -1
- package/lib/components/Tooltip/TooltipPortal.cjs +1 -0
- package/lib/components/Tooltip/TooltipPortal.cjs.map +1 -1
- package/lib/components/Tooltip/TooltipPortal.js +1 -0
- package/lib/components/Tooltip/TooltipPortal.js.map +1 -1
- package/lib/hooks/useClick.cjs +7 -4
- package/lib/hooks/useClick.cjs.map +1 -1
- package/lib/hooks/useClick.js +7 -4
- package/lib/hooks/useClick.js.map +1 -1
- package/lib/hooks/useOuterClick.cjs +5 -6
- package/lib/hooks/useOuterClick.cjs.map +1 -1
- package/lib/hooks/useOuterClick.js +6 -7
- package/lib/hooks/useOuterClick.js.map +1 -1
- package/lib/hooks/usePortal.cjs +24 -17
- package/lib/hooks/usePortal.cjs.map +1 -1
- package/lib/hooks/usePortal.d.ts +1 -1
- package/lib/hooks/usePortal.js +25 -18
- package/lib/hooks/usePortal.js.map +1 -1
- package/lib/hooks/useResponseStatus.cjs +20 -23
- package/lib/hooks/useResponseStatus.cjs.map +1 -1
- package/lib/hooks/useResponseStatus.js +20 -23
- package/lib/hooks/useResponseStatus.js.map +1 -1
- package/lib/intl/locales/en_us.json.cjs +1 -0
- package/lib/intl/locales/en_us.json.cjs.map +1 -1
- package/lib/intl/locales/en_us.json.js +1 -0
- package/lib/intl/locales/en_us.json.js.map +1 -1
- package/lib/intl/locales/id_id.json.cjs +0 -8
- package/lib/intl/locales/id_id.json.cjs.map +1 -1
- package/lib/intl/locales/id_id.json.js +0 -8
- package/lib/intl/locales/id_id.json.js.map +1 -1
- package/lib/intl/locales/index.d.ts +6 -16
- package/lib/intl/locales/ja_easy.json.cjs +1 -0
- package/lib/intl/locales/ja_easy.json.cjs.map +1 -1
- package/lib/intl/locales/ja_easy.json.js +1 -0
- package/lib/intl/locales/ja_easy.json.js.map +1 -1
- package/lib/intl/locales/ko_kr.json.cjs +1 -0
- package/lib/intl/locales/ko_kr.json.cjs.map +1 -1
- package/lib/intl/locales/ko_kr.json.js +1 -0
- package/lib/intl/locales/ko_kr.json.js.map +1 -1
- package/lib/intl/locales/pt_br.json.cjs +0 -8
- package/lib/intl/locales/pt_br.json.cjs.map +1 -1
- package/lib/intl/locales/pt_br.json.js +0 -8
- package/lib/intl/locales/pt_br.json.js.map +1 -1
- package/lib/intl/locales/vi_vn.json.cjs +1 -0
- package/lib/intl/locales/vi_vn.json.cjs.map +1 -1
- package/lib/intl/locales/vi_vn.json.js +1 -0
- package/lib/intl/locales/vi_vn.json.js.map +1 -1
- package/lib/intl/locales/zh_hans_cn.json.cjs +1 -0
- package/lib/intl/locales/zh_hans_cn.json.cjs.map +1 -1
- package/lib/intl/locales/zh_hans_cn.json.js +1 -0
- package/lib/intl/locales/zh_hans_cn.json.js.map +1 -1
- package/lib/intl/locales/zh_hant_tw.json.cjs +1 -0
- package/lib/intl/locales/zh_hant_tw.json.cjs.map +1 -1
- package/lib/intl/locales/zh_hant_tw.json.js +1 -0
- package/lib/intl/locales/zh_hant_tw.json.js.map +1 -1
- package/lib/intl/useDateFormat.cjs +1 -2
- package/lib/intl/useDateFormat.cjs.map +1 -1
- package/lib/intl/useDateFormat.js +1 -2
- package/lib/intl/useDateFormat.js.map +1 -1
- package/lib/intl/useIntl.cjs +2 -4
- package/lib/intl/useIntl.cjs.map +1 -1
- package/lib/intl/useIntl.js +2 -4
- package/lib/intl/useIntl.js.map +1 -1
- package/lib/libs/debounce.cjs +10 -2
- package/lib/libs/debounce.cjs.map +1 -1
- package/lib/libs/debounce.d.ts +4 -2
- package/lib/libs/debounce.js +10 -2
- package/lib/libs/debounce.js.map +1 -1
- package/lib/libs/lodash.cjs +3 -4
- package/lib/libs/lodash.cjs.map +1 -1
- package/lib/libs/lodash.d.ts +2 -11
- package/lib/libs/lodash.js +2 -7
- package/lib/libs/lodash.js.map +1 -1
- package/lib/libs/map.cjs +1 -5
- package/lib/libs/map.cjs.map +1 -1
- package/lib/libs/map.js +1 -5
- package/lib/libs/map.js.map +1 -1
- package/lib/themes/createBackgroundColor.cjs +3 -2
- package/lib/themes/createBackgroundColor.cjs.map +1 -1
- package/lib/themes/createBackgroundColor.js +2 -1
- package/lib/themes/createBackgroundColor.js.map +1 -1
- package/lib/themes/createBorder.cjs +3 -2
- package/lib/themes/createBorder.cjs.map +1 -1
- package/lib/themes/createBorder.js +2 -1
- package/lib/themes/createBorder.js.map +1 -1
- package/lib/themes/createBreakpoint.cjs +3 -2
- package/lib/themes/createBreakpoint.cjs.map +1 -1
- package/lib/themes/createBreakpoint.js +2 -1
- package/lib/themes/createBreakpoint.js.map +1 -1
- package/lib/themes/createColor.cjs +3 -2
- package/lib/themes/createColor.cjs.map +1 -1
- package/lib/themes/createColor.js +2 -1
- package/lib/themes/createColor.js.map +1 -1
- package/lib/themes/createFontSize.cjs +3 -2
- package/lib/themes/createFontSize.cjs.map +1 -1
- package/lib/themes/createFontSize.js +2 -1
- package/lib/themes/createFontSize.js.map +1 -1
- package/lib/themes/createInteraction.cjs +3 -2
- package/lib/themes/createInteraction.cjs.map +1 -1
- package/lib/themes/createInteraction.js +2 -1
- package/lib/themes/createInteraction.js.map +1 -1
- package/lib/themes/createLeading.cjs +3 -2
- package/lib/themes/createLeading.cjs.map +1 -1
- package/lib/themes/createLeading.js +2 -1
- package/lib/themes/createLeading.js.map +1 -1
- package/lib/themes/createRadius.cjs +3 -2
- package/lib/themes/createRadius.cjs.map +1 -1
- package/lib/themes/createRadius.js +2 -1
- package/lib/themes/createRadius.js.map +1 -1
- package/lib/themes/createShadow/createShadow.cjs +3 -2
- package/lib/themes/createShadow/createShadow.cjs.map +1 -1
- package/lib/themes/createShadow/createShadow.js +2 -1
- package/lib/themes/createShadow/createShadow.js.map +1 -1
- package/lib/themes/createTextColor.cjs +3 -2
- package/lib/themes/createTextColor.cjs.map +1 -1
- package/lib/themes/createTextColor.js +2 -1
- package/lib/themes/createTextColor.js.map +1 -1
- package/lib/themes/createZIndex.cjs +3 -2
- package/lib/themes/createZIndex.cjs.map +1 -1
- package/lib/themes/createZIndex.js +2 -1
- package/lib/themes/createZIndex.js.map +1 -1
- package/metadata.json +1 -1
- package/package.json +1 -1
- 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
|
|
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
|
|
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 = (
|
|
48
|
-
if (!
|
|
47
|
+
const calculateIdealRows = (node, maxRows, lineHeightNormal) => {
|
|
48
|
+
if (!node) {
|
|
49
49
|
return 0;
|
|
50
50
|
}
|
|
51
51
|
// 現在の入力値に応じた行数
|
|
52
|
-
const currentInputValueRows = Math.floor(
|
|
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,
|
|
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
|
|
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: (
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
callbackRef: (node) => {
|
|
130
|
+
// TODO: useMergeRefs, useOnceCallbackが実装されたら適用する
|
|
131
|
+
innerRef.current = node;
|
|
132
|
+
if (node) {
|
|
127
133
|
// autoFocus時に、フォーカスを当てる
|
|
128
134
|
if (latest.autoFocus) {
|
|
129
|
-
|
|
135
|
+
node.focus();
|
|
130
136
|
}
|
|
131
137
|
// autoResize時に、初期値での高さを指定
|
|
132
138
|
if (latest.autoResize) {
|
|
133
|
-
setInterimRows(calculateIdealRows(
|
|
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, () =>
|
|
150
|
-
return (jsx("textarea", { ...rest,
|
|
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
|
|
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;
|
|
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;
|
|
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;;"}
|
package/lib/hooks/useClick.cjs
CHANGED
|
@@ -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
|
-
}, [
|
|
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 }, [
|
|
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;;;;"}
|
package/lib/hooks/useClick.js
CHANGED
|
@@ -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
|
-
}, [
|
|
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 }, [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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 {
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useLatest } from './useLatest.js';
|
|
2
3
|
|
|
3
4
|
function useOuterClick(targets, callback) {
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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;;;;"}
|
package/lib/hooks/usePortal.cjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
60
|
-
|
|
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 {
|
|
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":";;;;;;;;AAUA,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,CAAC,UAAU,CAAC,GAAGA,cAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;AAChD,IAAA,MAAM,MAAM,GAAGC,gBAAU,CAAC,aAAa,CAAC;AAExC,IAAA,MAAM,cAAc,GAAGC,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;AAE7B,IAAA,MAAM,MAAM,GAAGC,yBAAS,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;AAEpE,IAAA,MAAM,SAAS,GAAGD,aAAO,CAAC,MAAK;AAC7B,QAAA,MAAM,oBAAoB,GAAgC,CAAC,EAAE,QAAQ,EAAE,KAAI;AACzE,YAAA,MAAM,KAAK,GAAuB;AAChC,gBAAA,IAAI,EAAE,MAAM,CAAC,cAAc,CAAC,UAAU;aACvC;YAED,OAAOE,cAAA,CAAC,aAAa,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,KAAK,EAAA,QAAA,EAAG,QAAQ,EAAA,CAA0B;AAClF,QAAA,CAAC;QAED,OAAO;YACL,oBAAoB;AACpB,YAAA,aAAa,EAAE,CAAC,OAA2B,KACzC,cAAc,CAAC,OAAO,EAAE,IAAI,MAAM,CAAC,CAAA,KAAA,EAAQ,MAAM,CAAC,UAAU,CAAA,KAAA,CAAO,CAAC,CAAC;AACvE,YAAA,YAAY,EAAE,CAAC,QAAmB,KAAI;AACpC,gBAAA,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,EAAE;AAC9B,oBAAA,OAAO,IAAI;gBACb;gBAEA,OAAOC,qBAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC;YAClD,CAAC;SACF;AACH,IAAA,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;IAEZC,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,OAAO;QACL,UAAU;QACV,aAAa,EAAE,SAAS,CAAC,aAAa;QACtC,YAAY,EAAE,SAAS,CAAC,YAAY;QACpC,oBAAoB,EAAE,SAAS,CAAC,oBAAoB;KACrD;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;;;;"}
|
package/lib/hooks/usePortal.d.ts
CHANGED
|
@@ -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
|
};
|