smarthr-ui 99.3.0 → 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/Checkbox/Checkbox.cjs +1 -1
- package/lib/components/Checkbox/Checkbox.cjs.map +1 -1
- package/lib/components/Checkbox/Checkbox.js +1 -1
- package/lib/components/Checkbox/Checkbox.js.map +1 -1
- 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 +2 -2
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.js +2 -2
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.js.map +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs +2 -2
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs.map +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.js +2 -2
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.js.map +1 -1
- package/lib/components/DatePicker/DatePicker.cjs +2 -2
- package/lib/components/DatePicker/DatePicker.cjs.map +1 -1
- package/lib/components/DatePicker/DatePicker.js +2 -2
- package/lib/components/DatePicker/DatePicker.js.map +1 -1
- package/lib/components/DatePicker/Portal.cjs +1 -1
- package/lib/components/DatePicker/Portal.cjs.map +1 -1
- package/lib/components/DatePicker/Portal.js +1 -1
- package/lib/components/DatePicker/Portal.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/DropZone/DropZone.cjs +1 -1
- package/lib/components/DropZone/DropZone.cjs.map +1 -1
- package/lib/components/DropZone/DropZone.js +1 -1
- package/lib/components/DropZone/DropZone.js.map +1 -1
- package/lib/components/Heading/PageHeading/PageHeading.cjs +1 -1
- package/lib/components/Heading/PageHeading/PageHeading.cjs.map +1 -1
- package/lib/components/Heading/PageHeading/PageHeading.js +1 -1
- package/lib/components/Heading/PageHeading/PageHeading.js.map +1 -1
- package/lib/components/Input/CurrencyInput/CurrencyInput.cjs +3 -3
- package/lib/components/Input/CurrencyInput/CurrencyInput.cjs.map +1 -1
- package/lib/components/Input/CurrencyInput/CurrencyInput.js +3 -3
- package/lib/components/Input/CurrencyInput/CurrencyInput.js.map +1 -1
- package/lib/components/Input/Input.cjs +15 -17
- package/lib/components/Input/Input.cjs.map +1 -1
- package/lib/components/Input/Input.js +15 -17
- 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/Scroller/Scroller.cjs +3 -1
- package/lib/components/Scroller/Scroller.cjs.map +1 -1
- package/lib/components/Scroller/Scroller.js +3 -1
- package/lib/components/Scroller/Scroller.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/TableReel.cjs +67 -3
- package/lib/components/Table/TableReel.cjs.map +1 -1
- package/lib/components/Table/TableReel.js +68 -4
- package/lib/components/Table/TableReel.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
- package/lib/components/Table/useReelCells.cjs +0 -83
- package/lib/components/Table/useReelCells.cjs.map +0 -1
- package/lib/components/Table/useReelCells.d.ts +0 -3
- package/lib/components/Table/useReelCells.js +0 -81
- package/lib/components/Table/useReelCells.js.map +0 -1
|
@@ -46,12 +46,12 @@ const classNameGenerator = index.tv({
|
|
|
46
46
|
'aria-[invalid]:shr-border-danger',
|
|
47
47
|
],
|
|
48
48
|
});
|
|
49
|
-
const calculateIdealRows = (
|
|
50
|
-
if (!
|
|
49
|
+
const calculateIdealRows = (node, maxRows, lineHeightNormal) => {
|
|
50
|
+
if (!node) {
|
|
51
51
|
return 0;
|
|
52
52
|
}
|
|
53
53
|
// 現在の入力値に応じた行数
|
|
54
|
-
const currentInputValueRows = Math.floor(
|
|
54
|
+
const currentInputValueRows = Math.floor(node.scrollHeight / (themes_createFontSize.defaultHtmlFontSize * lineHeightNormal));
|
|
55
55
|
return currentInputValueRows < maxRows ? currentInputValueRows : maxRows;
|
|
56
56
|
};
|
|
57
57
|
const Textarea = React.forwardRef(({ maxLetters, ...rest }, ref) => maxLetters ? (jsxRuntime.jsx(MaxLettersTextarea, { ...rest, externalRef: ref, maxLetters: maxLetters })) : (jsxRuntime.jsx(ActualTextarea, { ...rest, externalRef: ref })));
|
|
@@ -98,6 +98,10 @@ const MaxLettersTextarea = ({ maxLetters, error, value, defaultValue, onChange,
|
|
|
98
98
|
};
|
|
99
99
|
return {
|
|
100
100
|
updateCount,
|
|
101
|
+
cancelDebounce: () => {
|
|
102
|
+
updateSrMessage.cancel();
|
|
103
|
+
actualUpdateCount.cancel();
|
|
104
|
+
},
|
|
101
105
|
handleChange: (e) => {
|
|
102
106
|
updateCount(e.target.value);
|
|
103
107
|
latest.onChange?.(e);
|
|
@@ -106,12 +110,13 @@ const MaxLettersTextarea = ({ maxLetters, error, value, defaultValue, onChange,
|
|
|
106
110
|
}, [latest]);
|
|
107
111
|
React.useEffect(() => {
|
|
108
112
|
functions.updateCount(value ?? '');
|
|
113
|
+
return functions.cancelDebounce;
|
|
109
114
|
}, [value, functions]);
|
|
110
|
-
return (jsxRuntime.jsxs("span", { className: "shr-relative", children: [jsxRuntime.jsx(ActualTextarea, { ...rest, id: textareaId, value: value, defaultValue: defaultValue,
|
|
115
|
+
return (jsxRuntime.jsxs("span", { className: "shr-relative", children: [jsxRuntime.jsx(ActualTextarea, { ...rest, id: textareaId, value: value, defaultValue: defaultValue, "aria-describedby": `${maxLettersNoticeId} ${maxLettersId}`, error: error || countError, onChange: functions.handleChange }), jsxRuntime.jsx(components_VisuallyHiddenText_VisuallyHiddenText.VisuallyHiddenText, { id: maxLettersNoticeId, children: jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/Textarea/screenReaderMaxLettersDescription", defaultText: "\u6700\u5927{maxLetters}\u6587\u5B57\u5165\u529B\u3067\u304D\u307E\u3059", values: { maxLetters } }) }), jsxRuntime.jsx(components_VisuallyHiddenText_VisuallyHiddenText.VisuallyHiddenText, { as: "output", role: "status", htmlFor: textareaId, children: srCounterMessage }), jsxRuntime.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 ? (jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/Textarea/maxLettersExceeded", defaultText: "{exceededLetters}\u6587\u5B57\u30AA\u30FC\u30D0\u30FC", values: { exceededLetters: count - maxLetters } })) : (jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/Textarea/availableLetters", defaultText: "\u3042\u3068{availableLetters}\u6587\u5B57", values: { availableLetters: maxLetters - count } })) })] }));
|
|
111
116
|
};
|
|
112
117
|
const ActualTextarea = ({ autoFocus, width, className, autoResize = false, maxRows = Infinity, rows = 2, error, onChange, externalRef, ...rest }) => {
|
|
113
118
|
const theme = hooks_useTheme.useTheme();
|
|
114
|
-
const
|
|
119
|
+
const innerRef = React.useRef(null);
|
|
115
120
|
const [interimRows, setInterimRows] = React.useState(rows);
|
|
116
121
|
const actualClassName = React.useMemo(() => classNameGenerator({ className }), [className]);
|
|
117
122
|
const latest = hooks_useLatest.useLatest({
|
|
@@ -123,16 +128,17 @@ const ActualTextarea = ({ autoFocus, width, className, autoResize = false, maxRo
|
|
|
123
128
|
rows,
|
|
124
129
|
});
|
|
125
130
|
const functions = React.useMemo(() => ({
|
|
126
|
-
callbackRef: (
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
callbackRef: (node) => {
|
|
132
|
+
// TODO: useMergeRefs, useOnceCallbackが実装されたら適用する
|
|
133
|
+
innerRef.current = node;
|
|
134
|
+
if (node) {
|
|
129
135
|
// autoFocus時に、フォーカスを当てる
|
|
130
136
|
if (latest.autoFocus) {
|
|
131
|
-
|
|
137
|
+
node.focus();
|
|
132
138
|
}
|
|
133
139
|
// autoResize時に、初期値での高さを指定
|
|
134
140
|
if (latest.autoResize) {
|
|
135
|
-
setInterimRows(calculateIdealRows(
|
|
141
|
+
setInterimRows(calculateIdealRows(node, latest.maxRows, latest.theme.leading.NORMAL));
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
},
|
|
@@ -148,8 +154,8 @@ const ActualTextarea = ({ autoFocus, width, className, autoResize = false, maxRo
|
|
|
148
154
|
latest.onChange?.(e);
|
|
149
155
|
},
|
|
150
156
|
}), [latest]);
|
|
151
|
-
React.useImperativeHandle(externalRef, () =>
|
|
152
|
-
return (jsxRuntime.jsx("textarea", { ...rest,
|
|
157
|
+
React.useImperativeHandle(externalRef, () => innerRef.current, []);
|
|
158
|
+
return (jsxRuntime.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 } }));
|
|
153
159
|
};
|
|
154
160
|
|
|
155
161
|
exports.Textarea = Textarea;
|
|
@@ -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) {
|