smarthr-ui 98.0.0 → 98.1.0
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/components/Combobox/ItemButton.cjs +27 -26
- package/lib/components/Combobox/ItemButton.cjs.map +1 -1
- package/lib/components/Combobox/ItemButton.js +28 -27
- package/lib/components/Combobox/ItemButton.js.map +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs +35 -52
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.cjs.map +1 -1
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.js +35 -52
- package/lib/components/Combobox/SingleCombobox/SingleCombobox.js.map +1 -1
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.cjs +9 -3
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.cjs.map +1 -1
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.js +9 -3
- package/lib/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.js.map +1 -1
- package/lib/components/Disclosure/DisclosureTrigger.cjs +8 -4
- package/lib/components/Disclosure/DisclosureTrigger.cjs.map +1 -1
- package/lib/components/Disclosure/DisclosureTrigger.js +8 -4
- package/lib/components/Disclosure/DisclosureTrigger.js.map +1 -1
- package/lib/components/FileViewer/FileViewer.cjs +35 -2
- package/lib/components/FileViewer/FileViewer.cjs.map +1 -1
- package/lib/components/FileViewer/FileViewer.js +37 -4
- package/lib/components/FileViewer/FileViewer.js.map +1 -1
- package/lib/components/FileViewer/PDFViewer.cjs +59 -2
- package/lib/components/FileViewer/PDFViewer.cjs.map +1 -1
- package/lib/components/FileViewer/PDFViewer.d.ts +6 -1
- package/lib/components/FileViewer/PDFViewer.js +60 -3
- package/lib/components/FileViewer/PDFViewer.js.map +1 -1
- package/lib/components/FileViewer/SearchController.cjs +105 -0
- package/lib/components/FileViewer/SearchController.cjs.map +1 -0
- package/lib/components/FileViewer/SearchController.d.ts +7 -0
- package/lib/components/FileViewer/SearchController.js +103 -0
- package/lib/components/FileViewer/SearchController.js.map +1 -0
- package/lib/components/FileViewer/buildCustomTextRenderer.cjs +55 -0
- package/lib/components/FileViewer/buildCustomTextRenderer.cjs.map +1 -0
- package/lib/components/FileViewer/buildCustomTextRenderer.d.ts +9 -0
- package/lib/components/FileViewer/buildCustomTextRenderer.js +50 -0
- package/lib/components/FileViewer/buildCustomTextRenderer.js.map +1 -0
- package/lib/components/FileViewer/types.d.ts +7 -0
- package/lib/components/FileViewer/usePDFSearch.cjs +148 -0
- package/lib/components/FileViewer/usePDFSearch.cjs.map +1 -0
- package/lib/components/FileViewer/usePDFSearch.d.ts +19 -0
- package/lib/components/FileViewer/usePDFSearch.js +143 -0
- package/lib/components/FileViewer/usePDFSearch.js.map +1 -0
- package/lib/components/Icon/FaIcon.js +1 -1
- package/lib/hooks/useLatest.cjs +33 -0
- package/lib/hooks/useLatest.cjs.map +1 -0
- package/lib/hooks/useLatest.d.ts +19 -0
- package/lib/hooks/useLatest.js +31 -0
- package/lib/hooks/useLatest.js.map +1 -0
- package/lib/intl/locales/en_us.json.cjs +3 -0
- package/lib/intl/locales/en_us.json.cjs.map +1 -1
- package/lib/intl/locales/en_us.json.js +3 -0
- package/lib/intl/locales/en_us.json.js.map +1 -1
- package/lib/intl/locales/id_id.json.cjs +3 -0
- package/lib/intl/locales/id_id.json.cjs.map +1 -1
- package/lib/intl/locales/id_id.json.js +3 -0
- package/lib/intl/locales/id_id.json.js.map +1 -1
- package/lib/intl/locales/index.d.ts +27 -0
- package/lib/intl/locales/ja.cjs +3 -0
- package/lib/intl/locales/ja.cjs.map +1 -1
- package/lib/intl/locales/ja.d.ts +3 -0
- package/lib/intl/locales/ja.js +3 -0
- package/lib/intl/locales/ja.js.map +1 -1
- package/lib/intl/locales/ja.json.cjs +3 -0
- package/lib/intl/locales/ja.json.cjs.map +1 -1
- package/lib/intl/locales/ja.json.js +3 -0
- package/lib/intl/locales/ja.json.js.map +1 -1
- package/lib/intl/locales/ja_easy.json.cjs +3 -0
- package/lib/intl/locales/ja_easy.json.cjs.map +1 -1
- package/lib/intl/locales/ja_easy.json.js +3 -0
- package/lib/intl/locales/ja_easy.json.js.map +1 -1
- package/lib/intl/locales/ko_kr.json.cjs +3 -0
- package/lib/intl/locales/ko_kr.json.cjs.map +1 -1
- package/lib/intl/locales/ko_kr.json.js +3 -0
- package/lib/intl/locales/ko_kr.json.js.map +1 -1
- package/lib/intl/locales/pt_br.json.cjs +3 -0
- package/lib/intl/locales/pt_br.json.cjs.map +1 -1
- package/lib/intl/locales/pt_br.json.js +3 -0
- package/lib/intl/locales/pt_br.json.js.map +1 -1
- package/lib/intl/locales/vi_vn.json.cjs +3 -0
- package/lib/intl/locales/vi_vn.json.cjs.map +1 -1
- package/lib/intl/locales/vi_vn.json.js +3 -0
- package/lib/intl/locales/vi_vn.json.js.map +1 -1
- package/lib/intl/locales/zh_hans_cn.json.cjs +3 -0
- package/lib/intl/locales/zh_hans_cn.json.cjs.map +1 -1
- package/lib/intl/locales/zh_hans_cn.json.js +3 -0
- package/lib/intl/locales/zh_hans_cn.json.js.map +1 -1
- package/lib/intl/locales/zh_hant_tw.json.cjs +3 -0
- package/lib/intl/locales/zh_hant_tw.json.cjs.map +1 -1
- package/lib/intl/locales/zh_hant_tw.json.js +3 -0
- package/lib/intl/locales/zh_hant_tw.json.js.map +1 -1
- package/metadata.json +1 -1
- package/package.json +1 -1
- package/smarthr-ui.css +35 -4
|
@@ -4,6 +4,7 @@ import { memo, useRef, useState, useImperativeHandle, useCallback, useMemo, useE
|
|
|
4
4
|
import innerText from '../../../_virtual/index3.js';
|
|
5
5
|
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.17_@types_node@20.19.35_typescript@5.9.3__/vendor/tailwind-variants/dist/index.js';
|
|
6
6
|
import { useClick } from '../../../hooks/useClick.js';
|
|
7
|
+
import { useLatest } from '../../../hooks/useLatest.js';
|
|
7
8
|
import { useTheme } from '../../../hooks/useTheme.js';
|
|
8
9
|
import '../../../intl/IntlProvider.js';
|
|
9
10
|
import { useIntl } from '../../../intl/useIntl.js';
|
|
@@ -76,7 +77,7 @@ const ActualSingleCombobox = ({ items, selectedItem, defaultItem, name, disabled
|
|
|
76
77
|
const [inputValue, setInputValue] = useState('');
|
|
77
78
|
const [isComposing, setIsComposing] = useState(false);
|
|
78
79
|
const [isEditing, setIsEditing] = useState(false);
|
|
79
|
-
const
|
|
80
|
+
const latest = useLatest({
|
|
80
81
|
onChange,
|
|
81
82
|
onChangeInput,
|
|
82
83
|
onAdd,
|
|
@@ -94,24 +95,6 @@ const ActualSingleCombobox = ({ items, selectedItem, defaultItem, name, disabled
|
|
|
94
95
|
isComposing,
|
|
95
96
|
isEditing,
|
|
96
97
|
});
|
|
97
|
-
unstableRef.current = {
|
|
98
|
-
onChange,
|
|
99
|
-
onChangeInput,
|
|
100
|
-
onAdd,
|
|
101
|
-
onSelect,
|
|
102
|
-
onClear,
|
|
103
|
-
onClearClick,
|
|
104
|
-
onChangeSelected,
|
|
105
|
-
onFocus,
|
|
106
|
-
onBlur,
|
|
107
|
-
onKeyPress,
|
|
108
|
-
defaultItem,
|
|
109
|
-
selectedItem,
|
|
110
|
-
isFocused,
|
|
111
|
-
isExpanded,
|
|
112
|
-
isComposing,
|
|
113
|
-
isEditing,
|
|
114
|
-
};
|
|
115
98
|
useImperativeHandle(ref, () => inputRef.current);
|
|
116
99
|
const { options } = useSingleOptions({
|
|
117
100
|
items,
|
|
@@ -126,8 +109,8 @@ const ActualSingleCombobox = ({ items, selectedItem, defaultItem, name, disabled
|
|
|
126
109
|
dropdownWidth,
|
|
127
110
|
onAdd,
|
|
128
111
|
onSelect: useCallback((selected) => {
|
|
129
|
-
|
|
130
|
-
|
|
112
|
+
latest.onSelect?.(selected);
|
|
113
|
+
latest.onChangeSelected?.(selected);
|
|
131
114
|
// HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう
|
|
132
115
|
// requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする
|
|
133
116
|
requestAnimationFrame(() => {
|
|
@@ -138,46 +121,46 @@ const ActualSingleCombobox = ({ items, selectedItem, defaultItem, name, disabled
|
|
|
138
121
|
// - 先にclearしてしまうと意図せずこの要素のドロップダウンを閉じる前に他要素の再レンダリングを引き起こす可能性がある
|
|
139
122
|
// - 例えばFilterDropdownなどで当comboboxを使っている場合、レイアウト上comboboxのdropdown以下の要素がクリックされた扱いになってしまい
|
|
140
123
|
// FilterDropdownを意図せず閉じてしまうなどの挙動のバグを引き起こす可能性がある
|
|
141
|
-
|
|
124
|
+
latest.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT);
|
|
142
125
|
});
|
|
143
126
|
setIsEditing(false);
|
|
144
|
-
}, []),
|
|
127
|
+
}, [latest]),
|
|
145
128
|
isExpanded,
|
|
146
129
|
isLoading,
|
|
147
130
|
triggerRef: outerRef,
|
|
148
131
|
noResultText,
|
|
149
132
|
});
|
|
150
133
|
const selectDefaultItem = useCallback(() => {
|
|
151
|
-
if (
|
|
152
|
-
|
|
134
|
+
if (latest.onSelect && latest.defaultItem) {
|
|
135
|
+
latest.onSelect(latest.defaultItem);
|
|
153
136
|
}
|
|
154
|
-
}, []);
|
|
137
|
+
}, [latest]);
|
|
155
138
|
const focus = useCallback(() => {
|
|
156
|
-
|
|
139
|
+
latest.onFocus?.();
|
|
157
140
|
inputRef.current?.focus();
|
|
158
141
|
setIsFocused(true);
|
|
159
|
-
if (!
|
|
142
|
+
if (!latest.isFocused) {
|
|
160
143
|
setIsExpanded(true);
|
|
161
144
|
}
|
|
162
|
-
}, []);
|
|
145
|
+
}, [latest]);
|
|
163
146
|
const unfocus = useCallback(() => {
|
|
164
|
-
if (!
|
|
147
|
+
if (!latest.isFocused)
|
|
165
148
|
return;
|
|
166
|
-
|
|
149
|
+
latest.onBlur?.();
|
|
167
150
|
setIsFocused(false);
|
|
168
151
|
setIsExpanded(false);
|
|
169
152
|
setIsEditing(false);
|
|
170
|
-
if (
|
|
171
|
-
setInputValue(innerText(
|
|
153
|
+
if (latest.selectedItem) {
|
|
154
|
+
setInputValue(innerText(latest.selectedItem.label));
|
|
172
155
|
}
|
|
173
156
|
else {
|
|
174
157
|
selectDefaultItem();
|
|
175
158
|
}
|
|
176
|
-
}, [selectDefaultItem]);
|
|
159
|
+
}, [selectDefaultItem, latest]);
|
|
177
160
|
const onClickClear = useCallback((e) => {
|
|
178
161
|
e.stopPropagation();
|
|
179
162
|
let isExecutedPreventDefault = false;
|
|
180
|
-
|
|
163
|
+
latest.onClearClick?.({
|
|
181
164
|
...e,
|
|
182
165
|
preventDefault: () => {
|
|
183
166
|
e.preventDefault();
|
|
@@ -185,43 +168,43 @@ const ActualSingleCombobox = ({ items, selectedItem, defaultItem, name, disabled
|
|
|
185
168
|
},
|
|
186
169
|
});
|
|
187
170
|
if (!isExecutedPreventDefault) {
|
|
188
|
-
|
|
189
|
-
|
|
171
|
+
latest.onClear?.();
|
|
172
|
+
latest.onChangeSelected?.(null);
|
|
190
173
|
inputRef.current?.focus();
|
|
191
174
|
setIsFocused(true);
|
|
192
175
|
setIsExpanded(true);
|
|
193
176
|
}
|
|
194
|
-
}, []);
|
|
177
|
+
}, [latest]);
|
|
195
178
|
const onClickInput = useCallback((e) => {
|
|
196
179
|
if (disabled || readOnly) {
|
|
197
180
|
e.stopPropagation();
|
|
198
181
|
return;
|
|
199
182
|
}
|
|
200
183
|
inputRef.current?.focus();
|
|
201
|
-
if (!
|
|
184
|
+
if (!latest.isExpanded) {
|
|
202
185
|
setIsExpanded(true);
|
|
203
186
|
}
|
|
204
|
-
}, [disabled, readOnly]);
|
|
187
|
+
}, [disabled, readOnly, latest]);
|
|
205
188
|
const actualOnChangeInput = useCallback((e) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (!
|
|
189
|
+
latest.onChange?.(e);
|
|
190
|
+
latest.onChangeInput?.(e);
|
|
191
|
+
if (!latest.isEditing)
|
|
209
192
|
setIsEditing(true);
|
|
210
193
|
const { value } = e.currentTarget;
|
|
211
194
|
setInputValue(value);
|
|
212
195
|
if (value === '') {
|
|
213
|
-
|
|
214
|
-
|
|
196
|
+
latest.onClear?.();
|
|
197
|
+
latest.onChangeSelected?.(null);
|
|
215
198
|
}
|
|
216
|
-
}, []);
|
|
199
|
+
}, [latest]);
|
|
217
200
|
const onCompositionStart = useCallback(() => setIsComposing(true), []);
|
|
218
201
|
const onCompositionEnd = useCallback(() => setIsComposing(false), []);
|
|
219
202
|
const onKeyDownInput = useCallback((e) => {
|
|
220
|
-
if (
|
|
203
|
+
if (latest.isComposing) {
|
|
221
204
|
return;
|
|
222
205
|
}
|
|
223
206
|
if (ESCAPE_KEY_REGEX.test(e.key)) {
|
|
224
|
-
if (
|
|
207
|
+
if (latest.isExpanded) {
|
|
225
208
|
e.stopPropagation();
|
|
226
209
|
setIsExpanded(false);
|
|
227
210
|
}
|
|
@@ -234,20 +217,20 @@ const ActualSingleCombobox = ({ items, selectedItem, defaultItem, name, disabled
|
|
|
234
217
|
e.preventDefault();
|
|
235
218
|
}
|
|
236
219
|
inputRef.current?.focus();
|
|
237
|
-
if (!
|
|
220
|
+
if (!latest.isExpanded) {
|
|
238
221
|
setIsExpanded(true);
|
|
239
222
|
}
|
|
240
223
|
}
|
|
241
224
|
onKeyDownListBox(e);
|
|
242
|
-
}, [unfocus, onKeyDownListBox]);
|
|
225
|
+
}, [unfocus, onKeyDownListBox, latest]);
|
|
243
226
|
// HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で
|
|
244
227
|
// アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、
|
|
245
228
|
// submitイベントが発生し、formが送信される場合がある
|
|
246
229
|
const handleKeyPress = useCallback((e) => {
|
|
247
230
|
if (e.key === 'Enter')
|
|
248
231
|
e.preventDefault();
|
|
249
|
-
|
|
250
|
-
}, []);
|
|
232
|
+
latest.onKeyPress?.(e);
|
|
233
|
+
}, [latest]);
|
|
251
234
|
const caretIconColor = isFocused
|
|
252
235
|
? theme.textColor.black
|
|
253
236
|
: disabled || readOnly
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SingleCombobox.js","sources":["../../../../src/components/Combobox/SingleCombobox/SingleCombobox.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentPropsWithoutRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n memo,\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useClick } from '../../../hooks/useClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useIntl } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { UnstyledButton } from '../../Button'\nimport { FaCaretDownIcon, FaCircleXmarkIcon } from '../../Icon'\nimport { Input } from '../../Input'\nimport { useListbox } from '../useListbox'\nimport { useSingleOptions } from '../useOptions'\n\nimport type { ComboboxItem, AbstractProps as ComboboxProps } from '../types'\n\ntype AbstractProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテム\n */\n selectedItem: ComboboxItem<T> | null\n /**\n * デフォルトで選択されるアイテム\n */\n defaultItem?: ComboboxItem<T>\n /**\n * コンポーネント内の先頭に表示する内容\n */\n prefix?: ReactNode\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n */\n onClear?: () => void\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n * 指定している場合、クリア時にonClickを実行せずにonClearClickのみ実行する\n */\n onClearClick?: (e: MouseEvent) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItem: ComboboxItem<T> | null) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = AbstractProps<T> &\n Omit<ComponentPropsWithoutRef<'input'>, keyof AbstractProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_UP_DOWN_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n currentTarget: { value: '' },\n target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst classNameGenerator = tv({\n slots: {\n wrapper: 'smarthr-ui-SingleCombobox shr-inline-block',\n input: 'smarthr-ui-SingleCombobox-input shr-w-full',\n caretDownLayout: [\n 'shr-relative -shr-me-0.5 shr-p-0.5',\n 'before:shr-border-0',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n caretDownIcon: 'shr-block',\n clearButton: [\n 'smarthr-ui-SingleCombobox-clearButton',\n 'shr-group/clearButton',\n 'shr-me-0.5',\n 'focus-visible:shr-shadow-none',\n ],\n clearButtonIcon: [\n 'shr-block',\n 'group-focus-visible/clearButton:shr-focus-indicator group-focus-visible/clearButton:shr-rounded-full',\n ],\n },\n variants: {\n disabled: {\n true: {\n wrapper: 'shr-cursor-not-allowed',\n },\n },\n hidden: {\n true: {\n clearButton: 'shr-hidden',\n },\n },\n },\n})\n\ntype SuffixButtonsProps = {\n onClickClear: (e: MouseEvent) => void\n clearButtonRef: RefObject<HTMLButtonElement>\n onClickIcon: (e: MouseEvent) => void\n caretIconColor: string\n destroyButtonIconAlt: string\n classNames: {\n clearButton: string\n clearButtonIcon: string\n caretDownLayout: string\n caretDownIcon: string\n }\n}\n\nconst SuffixButtons = memo<SuffixButtonsProps>(\n ({\n onClickClear,\n clearButtonRef,\n onClickIcon: onDelegateClickIcon,\n caretIconColor,\n destroyButtonIconAlt,\n classNames,\n }) => (\n <>\n <UnstyledButton\n onClick={onClickClear}\n ref={clearButtonRef}\n className={classNames.clearButton}\n >\n <FaCircleXmarkIcon\n color=\"TEXT_BLACK\"\n alt={destroyButtonIconAlt}\n className={classNames.clearButtonIcon}\n />\n </UnstyledButton>\n <span\n role=\"presentation\"\n onClick={onDelegateClickIcon}\n className={classNames.caretDownLayout}\n >\n <FaCaretDownIcon color={caretIconColor} className={classNames.caretDownIcon} />\n </span>\n </>\n ),\n)\n\nconst ActualSingleCombobox = <T,>(\n {\n items,\n selectedItem,\n defaultItem,\n name,\n disabled,\n readOnly,\n required,\n prefix,\n error,\n creatable,\n placeholder,\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n width,\n dropdownWidth = 'auto',\n className,\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const theme = useTheme()\n const { localize } = useIntl()\n const outerRef = useRef<HTMLDivElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const clearButtonRef = useRef<HTMLButtonElement>(null)\n const [isFocused, setIsFocused] = useState(false)\n const [isExpanded, setIsExpanded] = useState(false)\n const [inputValue, setInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n const [isEditing, setIsEditing] = useState(false)\n\n const unstableRef = useRef({\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n defaultItem,\n selectedItem,\n isFocused,\n isExpanded,\n isComposing,\n isEditing,\n })\n unstableRef.current = {\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n defaultItem,\n selectedItem,\n isFocused,\n isExpanded,\n isComposing,\n isEditing,\n }\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n const { options } = useSingleOptions({\n items,\n selected: selectedItem,\n creatable,\n inputValue,\n isFilteringDisabled: !isEditing,\n })\n\n const { renderListBox, activeOption, onKeyDownListBox, listBoxId, listBoxRef } = useListbox<T>({\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n onSelect: useCallback((selected: ComboboxItem<T>) => {\n unstableRef.current.onSelect?.(selected)\n unstableRef.current.onChangeSelected?.(selected)\n\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n setIsExpanded(false)\n // HINT:\n // - 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n // - 対応するdropdownを閉じて以降にonChangeInputを発火する必要がある\n // - 先にclearしてしまうと意図せずこの要素のドロップダウンを閉じる前に他要素の再レンダリングを引き起こす可能性がある\n // - 例えばFilterDropdownなどで当comboboxを使っている場合、レイアウト上comboboxのdropdown以下の要素がクリックされた扱いになってしまい\n // FilterDropdownを意図せず閉じてしまうなどの挙動のバグを引き起こす可能性がある\n unstableRef.current.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n })\n\n setIsEditing(false)\n }, []),\n isExpanded,\n isLoading,\n triggerRef: outerRef,\n noResultText,\n })\n\n const selectDefaultItem = useCallback(() => {\n if (unstableRef.current.onSelect && unstableRef.current.defaultItem) {\n unstableRef.current.onSelect(unstableRef.current.defaultItem)\n }\n }, [])\n\n const focus = useCallback(() => {\n unstableRef.current.onFocus?.()\n inputRef.current?.focus()\n setIsFocused(true)\n\n if (!unstableRef.current.isFocused) {\n setIsExpanded(true)\n }\n }, [])\n const unfocus = useCallback(() => {\n if (!unstableRef.current.isFocused) return\n\n unstableRef.current.onBlur?.()\n\n setIsFocused(false)\n setIsExpanded(false)\n setIsEditing(false)\n\n if (unstableRef.current.selectedItem) {\n setInputValue(innerText(unstableRef.current.selectedItem.label))\n } else {\n selectDefaultItem()\n }\n }, [selectDefaultItem])\n const onClickClear = useCallback((e: MouseEvent) => {\n e.stopPropagation()\n\n let isExecutedPreventDefault = false\n\n unstableRef.current.onClearClick?.({\n ...e,\n preventDefault: () => {\n e.preventDefault()\n isExecutedPreventDefault = true\n },\n })\n\n if (!isExecutedPreventDefault) {\n unstableRef.current.onClear?.()\n unstableRef.current.onChangeSelected?.(null)\n\n inputRef.current?.focus()\n\n setIsFocused(true)\n setIsExpanded(true)\n }\n }, [])\n const onClickInput = useCallback(\n (e: MouseEvent) => {\n if (disabled || readOnly) {\n e.stopPropagation()\n\n return\n }\n\n inputRef.current?.focus()\n\n if (!unstableRef.current.isExpanded) {\n setIsExpanded(true)\n }\n },\n [disabled, readOnly],\n )\n const actualOnChangeInput = useCallback((e: ChangeEvent<HTMLInputElement>) => {\n unstableRef.current.onChange?.(e)\n unstableRef.current.onChangeInput?.(e)\n\n if (!unstableRef.current.isEditing) setIsEditing(true)\n\n const { value } = e.currentTarget\n\n setInputValue(value)\n\n if (value === '') {\n unstableRef.current.onClear?.()\n unstableRef.current.onChangeSelected?.(null)\n }\n }, [])\n const onCompositionStart = useCallback(() => setIsComposing(true), [])\n const onCompositionEnd = useCallback(() => setIsComposing(false), [])\n const onKeyDownInput = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (unstableRef.current.isComposing) {\n return\n }\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n if (unstableRef.current.isExpanded) {\n e.stopPropagation()\n setIsExpanded(false)\n }\n } else if (e.key === 'Tab') {\n unfocus()\n } else {\n if (ARROW_UP_DOWN_REGEX.test(e.key)) {\n e.preventDefault()\n }\n\n inputRef.current?.focus()\n\n if (!unstableRef.current.isExpanded) {\n setIsExpanded(true)\n }\n }\n onKeyDownListBox(e)\n },\n [unfocus, onKeyDownListBox],\n )\n\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n const handleKeyPress = useCallback((e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') e.preventDefault()\n\n unstableRef.current.onKeyPress?.(e)\n }, [])\n\n const caretIconColor = isFocused\n ? theme.textColor.black\n : disabled || readOnly\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n useClick(\n useMemo(() => [outerRef, listBoxRef, clearButtonRef], [listBoxRef]),\n isFocused || selectedItem ? NOOP : selectDefaultItem,\n unfocus,\n )\n\n // selectedItem.label はプリミティブ値でないデータ型の可能性があり、そのまま useEffect の依存配列に入れると意図せぬエフェクトの実行を引き起こしてしまう可能性があるので、プリミティブ値である string 型に変換したものを依存配列に入れています。\n const selectedItemLabelText = innerText(selectedItem?.label)\n useEffect(() => {\n setInputValue(selectedItemLabelText)\n }, [selectedItemLabelText])\n\n const wrapperStyle = useMemo(\n () => ({\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }),\n [style, width],\n )\n\n const hasSelectedItem = selectedItem !== null\n\n const classNames = useMemo(() => {\n const { wrapper, input, caretDownLayout, caretDownIcon, clearButton, clearButtonIcon } =\n classNameGenerator()\n\n return {\n wrapper: wrapper({ disabled, className }),\n input: input(),\n caretDownLayout: caretDownLayout(),\n caretDownIcon: caretDownIcon(),\n clearButton: clearButton({\n hidden: !hasSelectedItem || disabled || readOnly,\n }),\n clearButtonIcon: clearButtonIcon(),\n }\n }, [hasSelectedItem, disabled, readOnly, className])\n\n const destroyButtonIconAlt = useMemo(\n () =>\n localize({\n id: 'smarthr-ui/SingleCombobox/destroyButtonIconAlt',\n defaultText: 'クリア',\n }),\n [localize],\n )\n\n return (\n <div role=\"group\" className={classNames.wrapper} style={wrapperStyle} ref={outerRef}>\n <Input\n {...rest}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n name={name}\n value={inputValue}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n autoComplete={autoComplete ?? 'off'}\n aria-haspopup=\"listbox\"\n aria-controls={listBoxId}\n aria-expanded={isFocused}\n aria-activedescendant={activeOption?.id}\n aria-autocomplete=\"list\"\n /* eslint-disable-next-line smarthr/a11y-prohibit-input-placeholder */\n placeholder={placeholder}\n onClick={onClickInput}\n onChange={actualOnChangeInput}\n onFocus={isFocused ? undefined : focus}\n onCompositionStart={onCompositionStart}\n onCompositionEnd={onCompositionEnd}\n onKeyDown={onKeyDownInput}\n onKeyPress={handleKeyPress}\n error={error}\n prefix={prefix}\n suffix={\n <SuffixButtons\n onClickClear={onClickClear}\n clearButtonRef={clearButtonRef}\n onClickIcon={onClickInput}\n caretIconColor={caretIconColor}\n destroyButtonIconAlt={destroyButtonIconAlt}\n classNames={{\n clearButton: classNames.clearButton,\n clearButtonIcon: classNames.clearButtonIcon,\n caretDownLayout: classNames.caretDownLayout,\n caretDownIcon: classNames.caretDownIcon,\n }}\n />\n }\n className={classNames.input}\n data-smarthr-ui-input=\"true\"\n />\n {!readOnly && renderListBox()}\n </div>\n )\n}\n\nexport const SingleCombobox = genericsForwardRef(ActualSingleCombobox)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA2EA;AAEA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;AACA;AACA;;;;AAIC;AACD;AACA;;;;;AAKC;AACD;;;AAGC;AACF;AACD;AACE;AACE;AACE;AACD;AACF;AACD;AACE;AACE;AACD;AACF;AACF;AACF;AAgBD;AAgCA;AAmCE;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;AAwBC;;;;;;;;;;;;;;;;;;;;AAsBD;;AAEE;;;;AAID;AAED;;;;;AAKE;;;;;;;;;;;;;;AAeE;;;;;AAMF;;AAED;AAED;AACE;;;;AAKF;AACE;AACA;;AAGA;;;;AAIF;AACE;;AAEA;;;;AAMA;AACE;;;AAEA;;AAEJ;AACA;;;AAKE;AACE;;;;;AAKD;;AAGC;;AAGA;;;;;AAMJ;AAEI;;;;AAMA;AAEA;;;AAGF;AAGF;;;AAIE;;AAEA;;AAIA;AACE;;;;AAIJ;AACA;AACA;AAEI;;;;AAKE;;;;;AAIK;AACL;;;;;;AAMA;AAEA;;;;;AAKJ;;;;AAOF;AACE;;;;;AAMA;;AAEE;AACA;AAEJ;;;;;AAUA;AAEA;AAEI;AACA;AACD;AAIH;AAEA;AACE;;;;;;;AASI;;;;;;AASA;AACA;AACD;AAIL;;AAmBM;;;;;AAsBK;AASb;;;"}
|
|
1
|
+
{"version":3,"file":"SingleCombobox.js","sources":["../../../../src/components/Combobox/SingleCombobox/SingleCombobox.tsx"],"sourcesContent":["'use client'\n\nimport {\n type ChangeEvent,\n type ComponentPropsWithoutRef,\n type KeyboardEvent,\n type MouseEvent,\n type ReactNode,\n type Ref,\n type RefObject,\n memo,\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useClick } from '../../../hooks/useClick'\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useIntl } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { UnstyledButton } from '../../Button'\nimport { FaCaretDownIcon, FaCircleXmarkIcon } from '../../Icon'\nimport { Input } from '../../Input'\nimport { useListbox } from '../useListbox'\nimport { useSingleOptions } from '../useOptions'\n\nimport type { ComboboxItem, AbstractProps as ComboboxProps } from '../types'\n\ntype AbstractProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテム\n */\n selectedItem: ComboboxItem<T> | null\n /**\n * デフォルトで選択されるアイテム\n */\n defaultItem?: ComboboxItem<T>\n /**\n * コンポーネント内の先頭に表示する内容\n */\n prefix?: ReactNode\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n */\n onClear?: () => void\n /**\n * 選択されているアイテムがクリアされた時に発火するコールバック関数\n * 指定している場合、クリア時にonClickを実行せずにonClearClickのみ実行する\n */\n onClearClick?: (e: MouseEvent) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItem: ComboboxItem<T> | null) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = AbstractProps<T> &\n Omit<ComponentPropsWithoutRef<'input'>, keyof AbstractProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_UP_DOWN_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n currentTarget: { value: '' },\n target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst classNameGenerator = tv({\n slots: {\n wrapper: 'smarthr-ui-SingleCombobox shr-inline-block',\n input: 'smarthr-ui-SingleCombobox-input shr-w-full',\n caretDownLayout: [\n 'shr-relative -shr-me-0.5 shr-p-0.5',\n 'before:shr-border-0',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n caretDownIcon: 'shr-block',\n clearButton: [\n 'smarthr-ui-SingleCombobox-clearButton',\n 'shr-group/clearButton',\n 'shr-me-0.5',\n 'focus-visible:shr-shadow-none',\n ],\n clearButtonIcon: [\n 'shr-block',\n 'group-focus-visible/clearButton:shr-focus-indicator group-focus-visible/clearButton:shr-rounded-full',\n ],\n },\n variants: {\n disabled: {\n true: {\n wrapper: 'shr-cursor-not-allowed',\n },\n },\n hidden: {\n true: {\n clearButton: 'shr-hidden',\n },\n },\n },\n})\n\ntype SuffixButtonsProps = {\n onClickClear: (e: MouseEvent) => void\n clearButtonRef: RefObject<HTMLButtonElement>\n onClickIcon: (e: MouseEvent) => void\n caretIconColor: string\n destroyButtonIconAlt: string\n classNames: {\n clearButton: string\n clearButtonIcon: string\n caretDownLayout: string\n caretDownIcon: string\n }\n}\n\nconst SuffixButtons = memo<SuffixButtonsProps>(\n ({\n onClickClear,\n clearButtonRef,\n onClickIcon: onDelegateClickIcon,\n caretIconColor,\n destroyButtonIconAlt,\n classNames,\n }) => (\n <>\n <UnstyledButton\n onClick={onClickClear}\n ref={clearButtonRef}\n className={classNames.clearButton}\n >\n <FaCircleXmarkIcon\n color=\"TEXT_BLACK\"\n alt={destroyButtonIconAlt}\n className={classNames.clearButtonIcon}\n />\n </UnstyledButton>\n <span\n role=\"presentation\"\n onClick={onDelegateClickIcon}\n className={classNames.caretDownLayout}\n >\n <FaCaretDownIcon color={caretIconColor} className={classNames.caretDownIcon} />\n </span>\n </>\n ),\n)\n\nconst ActualSingleCombobox = <T,>(\n {\n items,\n selectedItem,\n defaultItem,\n name,\n disabled,\n readOnly,\n required,\n prefix,\n error,\n creatable,\n placeholder,\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n width,\n dropdownWidth = 'auto',\n className,\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const theme = useTheme()\n const { localize } = useIntl()\n const outerRef = useRef<HTMLDivElement>(null)\n const inputRef = useRef<HTMLInputElement>(null)\n const clearButtonRef = useRef<HTMLButtonElement>(null)\n const [isFocused, setIsFocused] = useState(false)\n const [isExpanded, setIsExpanded] = useState(false)\n const [inputValue, setInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n const [isEditing, setIsEditing] = useState(false)\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onAdd,\n onSelect,\n onClear,\n onClearClick,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n defaultItem,\n selectedItem,\n isFocused,\n isExpanded,\n isComposing,\n isEditing,\n })\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n const { options } = useSingleOptions({\n items,\n selected: selectedItem,\n creatable,\n inputValue,\n isFilteringDisabled: !isEditing,\n })\n\n const { renderListBox, activeOption, onKeyDownListBox, listBoxId, listBoxRef } = useListbox<T>({\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n onSelect: useCallback(\n (selected: ComboboxItem<T>) => {\n latest.onSelect?.(selected)\n latest.onChangeSelected?.(selected)\n\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n setIsExpanded(false)\n // HINT:\n // - 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n // - 対応するdropdownを閉じて以降にonChangeInputを発火する必要がある\n // - 先にclearしてしまうと意図せずこの要素のドロップダウンを閉じる前に他要素の再レンダリングを引き起こす可能性がある\n // - 例えばFilterDropdownなどで当comboboxを使っている場合、レイアウト上comboboxのdropdown以下の要素がクリックされた扱いになってしまい\n // FilterDropdownを意図せず閉じてしまうなどの挙動のバグを引き起こす可能性がある\n latest.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n })\n\n setIsEditing(false)\n },\n [latest],\n ),\n isExpanded,\n isLoading,\n triggerRef: outerRef,\n noResultText,\n })\n\n const selectDefaultItem = useCallback(() => {\n if (latest.onSelect && latest.defaultItem) {\n latest.onSelect(latest.defaultItem)\n }\n }, [latest])\n\n const focus = useCallback(() => {\n latest.onFocus?.()\n inputRef.current?.focus()\n setIsFocused(true)\n\n if (!latest.isFocused) {\n setIsExpanded(true)\n }\n }, [latest])\n const unfocus = useCallback(() => {\n if (!latest.isFocused) return\n\n latest.onBlur?.()\n\n setIsFocused(false)\n setIsExpanded(false)\n setIsEditing(false)\n\n if (latest.selectedItem) {\n setInputValue(innerText(latest.selectedItem.label))\n } else {\n selectDefaultItem()\n }\n }, [selectDefaultItem, latest])\n const onClickClear = useCallback(\n (e: MouseEvent) => {\n e.stopPropagation()\n\n let isExecutedPreventDefault = false\n\n latest.onClearClick?.({\n ...e,\n preventDefault: () => {\n e.preventDefault()\n isExecutedPreventDefault = true\n },\n })\n\n if (!isExecutedPreventDefault) {\n latest.onClear?.()\n latest.onChangeSelected?.(null)\n\n inputRef.current?.focus()\n\n setIsFocused(true)\n setIsExpanded(true)\n }\n },\n [latest],\n )\n const onClickInput = useCallback(\n (e: MouseEvent) => {\n if (disabled || readOnly) {\n e.stopPropagation()\n\n return\n }\n\n inputRef.current?.focus()\n\n if (!latest.isExpanded) {\n setIsExpanded(true)\n }\n },\n [disabled, readOnly, latest],\n )\n const actualOnChangeInput = useCallback(\n (e: ChangeEvent<HTMLInputElement>) => {\n latest.onChange?.(e)\n latest.onChangeInput?.(e)\n\n if (!latest.isEditing) setIsEditing(true)\n\n const { value } = e.currentTarget\n\n setInputValue(value)\n\n if (value === '') {\n latest.onClear?.()\n latest.onChangeSelected?.(null)\n }\n },\n [latest],\n )\n const onCompositionStart = useCallback(() => setIsComposing(true), [])\n const onCompositionEnd = useCallback(() => setIsComposing(false), [])\n const onKeyDownInput = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (latest.isComposing) {\n return\n }\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n if (latest.isExpanded) {\n e.stopPropagation()\n setIsExpanded(false)\n }\n } else if (e.key === 'Tab') {\n unfocus()\n } else {\n if (ARROW_UP_DOWN_REGEX.test(e.key)) {\n e.preventDefault()\n }\n\n inputRef.current?.focus()\n\n if (!latest.isExpanded) {\n setIsExpanded(true)\n }\n }\n onKeyDownListBox(e)\n },\n [unfocus, onKeyDownListBox, latest],\n )\n\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n const handleKeyPress = useCallback(\n (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') e.preventDefault()\n\n latest.onKeyPress?.(e)\n },\n [latest],\n )\n\n const caretIconColor = isFocused\n ? theme.textColor.black\n : disabled || readOnly\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n useClick(\n useMemo(() => [outerRef, listBoxRef, clearButtonRef], [listBoxRef]),\n isFocused || selectedItem ? NOOP : selectDefaultItem,\n unfocus,\n )\n\n // selectedItem.label はプリミティブ値でないデータ型の可能性があり、そのまま useEffect の依存配列に入れると意図せぬエフェクトの実行を引き起こしてしまう可能性があるので、プリミティブ値である string 型に変換したものを依存配列に入れています。\n const selectedItemLabelText = innerText(selectedItem?.label)\n useEffect(() => {\n setInputValue(selectedItemLabelText)\n }, [selectedItemLabelText])\n\n const wrapperStyle = useMemo(\n () => ({\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }),\n [style, width],\n )\n\n const hasSelectedItem = selectedItem !== null\n\n const classNames = useMemo(() => {\n const { wrapper, input, caretDownLayout, caretDownIcon, clearButton, clearButtonIcon } =\n classNameGenerator()\n\n return {\n wrapper: wrapper({ disabled, className }),\n input: input(),\n caretDownLayout: caretDownLayout(),\n caretDownIcon: caretDownIcon(),\n clearButton: clearButton({\n hidden: !hasSelectedItem || disabled || readOnly,\n }),\n clearButtonIcon: clearButtonIcon(),\n }\n }, [hasSelectedItem, disabled, readOnly, className])\n\n const destroyButtonIconAlt = useMemo(\n () =>\n localize({\n id: 'smarthr-ui/SingleCombobox/destroyButtonIconAlt',\n defaultText: 'クリア',\n }),\n [localize],\n )\n\n return (\n <div role=\"group\" className={classNames.wrapper} style={wrapperStyle} ref={outerRef}>\n <Input\n {...rest}\n ref={inputRef}\n type=\"text\"\n role=\"combobox\"\n name={name}\n value={inputValue}\n disabled={disabled}\n readOnly={readOnly}\n required={required}\n autoComplete={autoComplete ?? 'off'}\n aria-haspopup=\"listbox\"\n aria-controls={listBoxId}\n aria-expanded={isFocused}\n aria-activedescendant={activeOption?.id}\n aria-autocomplete=\"list\"\n /* eslint-disable-next-line smarthr/a11y-prohibit-input-placeholder */\n placeholder={placeholder}\n onClick={onClickInput}\n onChange={actualOnChangeInput}\n onFocus={isFocused ? undefined : focus}\n onCompositionStart={onCompositionStart}\n onCompositionEnd={onCompositionEnd}\n onKeyDown={onKeyDownInput}\n onKeyPress={handleKeyPress}\n error={error}\n prefix={prefix}\n suffix={\n <SuffixButtons\n onClickClear={onClickClear}\n clearButtonRef={clearButtonRef}\n onClickIcon={onClickInput}\n caretIconColor={caretIconColor}\n destroyButtonIconAlt={destroyButtonIconAlt}\n classNames={{\n clearButton: classNames.clearButton,\n clearButtonIcon: classNames.clearButtonIcon,\n caretDownLayout: classNames.caretDownLayout,\n caretDownIcon: classNames.caretDownIcon,\n }}\n />\n }\n className={classNames.input}\n data-smarthr-ui-input=\"true\"\n />\n {!readOnly && renderListBox()}\n </div>\n )\n}\n\nexport const SingleCombobox = genericsForwardRef(ActualSingleCombobox)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA;AAEA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;AACA;AACA;;;;AAIC;AACD;AACA;;;;;AAKC;AACD;;;AAGC;AACF;AACD;AACE;AACE;AACE;AACD;AACF;AACD;AACE;AACE;AACD;AACF;AACF;AACF;AAgBD;AAgCA;AAmCE;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;AAwBC;;AAID;;AAEE;;;;AAID;AAED;;;;;AAKE;AAEI;AACA;;;;;;;;;;;AAYE;AACF;;AAGF;;;AAKF;;AAED;AAED;;AAEI;;AAEJ;AAEA;AACE;AACA;;AAGA;;;AAGF;AACA;;;AAGE;;;;AAMA;;;;AAGE;;AAEJ;AACA;;;;AAOM;;;;;AAKD;;AAGC;AACA;AAEA;;;;AAKJ;AAGF;AAEI;;;;AAMA;AAEA;;;;AAMJ;AAEI;AACA;;;AAIA;;AAIA;AACE;AACA;;AAEJ;AAGF;AACA;AACA;AAEI;;;;AAKE;;;;;AAIK;AACL;;;;;;AAMA;AAEA;;;;;;;;;AAYN;AAEI;;AAEA;AACF;;AAKA;;AAEE;AACA;AAEJ;;;;;AAUA;AAEA;AAEI;AACA;AACD;AAIH;AAEA;AACE;;;;;;;AASI;;;;;;AASA;AACA;AACD;AAIL;;AAmBM;;;;;AAsBK;AASb;;;"}
|
|
@@ -37,9 +37,15 @@ const RemoteDialogTrigger = ({ targetId, children, onClick }) => {
|
|
|
37
37
|
// 現在の子要素に対して処理を実行
|
|
38
38
|
const setupElement = () => {
|
|
39
39
|
const element = getClickableElement();
|
|
40
|
-
if (element) {
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
if (!element) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
element.setAttribute('aria-haspopup', 'dialog');
|
|
44
|
+
element.setAttribute('aria-controls', targetId);
|
|
45
|
+
// Button は native disabled ではなく aria-disabled を使うため、
|
|
46
|
+
// 無効時はリスナーを貼らず Dialog が開かないようにする(DropdownTrigger と同じ)
|
|
47
|
+
if (!('disabled' in element && element.disabled) &&
|
|
48
|
+
element.getAttribute('aria-disabled') !== 'true') {
|
|
43
49
|
// HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する
|
|
44
50
|
element.addEventListener('click', actualOnClick, CAPTURE_OPTION);
|
|
45
51
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteDialogTrigger.cjs","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (element) {\n element.setAttribute('aria-haspopup', 'dialog')\n
|
|
1
|
+
{"version":3,"file":"RemoteDialogTrigger.cjs","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (!element) {\n return\n }\n\n element.setAttribute('aria-haspopup', 'dialog')\n element.setAttribute('aria-controls', targetId)\n\n // Button は native disabled ではなく aria-disabled を使うため、\n // 無効時はリスナーを貼らず Dialog が開かないようにする(DropdownTrigger と同じ)\n if (\n !('disabled' in element && element.disabled) &&\n element.getAttribute('aria-disabled') !== 'true'\n ) {\n // HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する\n element.addEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n const clearEventListener = () => {\n // 既存のイベントリスナーをクリーンアップ\n const element = getClickableElement()\n if (element) {\n element.removeEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n // 初回セットアップ\n setupElement()\n\n // MutationObserverでDOM変更を監視\n const observer = new MutationObserver(() => {\n clearEventListener()\n setupElement()\n })\n\n observer.observe(currentRef, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n observer.disconnect()\n clearEventListener()\n }\n }, [targetId, actualOnClick])\n\n return (\n <span className=\"smarthr-ui-RemoteDialogTrigger shr-contents\" ref={ref}>\n {children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAMA;AACE;;AAGF;AACE;AAEI;AACD;AAEL;AAEO;AAML;AACA;AACA;AAEA;;;;AAKE;AACE;;AAEA;;;;;AAOF;;;;;;;AAUE;;;;AAKA;AACA;;;;;;;;AAWF;;;AAIE;;;;AAIF;;AAGA;;AAGA;AACE;AACA;AACF;AAEA;AACE;AACA;;AAEA;AACA;AACD;AAED;;AAEE;AACF;AACF;AAEA;AAKF;;"}
|
|
@@ -35,9 +35,15 @@ const RemoteDialogTrigger = ({ targetId, children, onClick }) => {
|
|
|
35
35
|
// 現在の子要素に対して処理を実行
|
|
36
36
|
const setupElement = () => {
|
|
37
37
|
const element = getClickableElement();
|
|
38
|
-
if (element) {
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
if (!element) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
element.setAttribute('aria-haspopup', 'dialog');
|
|
42
|
+
element.setAttribute('aria-controls', targetId);
|
|
43
|
+
// Button は native disabled ではなく aria-disabled を使うため、
|
|
44
|
+
// 無効時はリスナーを貼らず Dialog が開かないようにする(DropdownTrigger と同じ)
|
|
45
|
+
if (!('disabled' in element && element.disabled) &&
|
|
46
|
+
element.getAttribute('aria-disabled') !== 'true') {
|
|
41
47
|
// HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する
|
|
42
48
|
element.addEventListener('click', actualOnClick, CAPTURE_OPTION);
|
|
43
49
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RemoteDialogTrigger.js","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (element) {\n element.setAttribute('aria-haspopup', 'dialog')\n
|
|
1
|
+
{"version":3,"file":"RemoteDialogTrigger.js","sources":["../../../../src/components/Dialog/RemoteDialogTrigger/RemoteDialogTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type PropsWithChildren, useCallback, useEffect, useRef } from 'react'\n\nimport { TRIGGER_EVENT } from '../useRemoteTrigger'\n\nconst CAPTURE_OPTION = {\n capture: true,\n}\n\nconst onClickRemoteDialogTrigger = (ariaControls: string) => {\n document.dispatchEvent(\n new CustomEvent(TRIGGER_EVENT, {\n detail: { id: ariaControls },\n }),\n )\n}\n\nexport const RemoteDialogTrigger: FC<\n PropsWithChildren<{\n targetId: string\n onClick?: (open: () => void) => void\n }>\n> = ({ targetId, children, onClick }) => {\n const ref = useRef<HTMLSpanElement | null>(null)\n const onClickRef = useRef(onClick)\n onClickRef.current = onClick\n\n const actualOnClick = useCallback((e: Event) => {\n // HINT: onClick内で非同期処理される場合、e.currentTargetがnullになってしまう可能性があるため\n // 先にariaControlsを取得しておく\n const ariaControls = (e.currentTarget as HTMLElement).getAttribute('aria-controls') as string\n\n if (onClickRef.current) {\n return onClickRef.current(() => {\n onClickRemoteDialogTrigger(ariaControls)\n })\n }\n\n onClickRemoteDialogTrigger(ariaControls)\n }, [])\n\n useEffect(() => {\n const currentRef = ref.current\n if (!currentRef) {\n return\n }\n\n const getClickableElement = () =>\n currentRef.querySelector<HTMLButtonElement | HTMLAnchorElement>('button, a')\n\n // 現在の子要素に対して処理を実行\n const setupElement = () => {\n const element = getClickableElement()\n if (!element) {\n return\n }\n\n element.setAttribute('aria-haspopup', 'dialog')\n element.setAttribute('aria-controls', targetId)\n\n // Button は native disabled ではなく aria-disabled を使うため、\n // 無効時はリスナーを貼らず Dialog が開かないようにする(DropdownTrigger と同じ)\n if (\n !('disabled' in element && element.disabled) &&\n element.getAttribute('aria-disabled') !== 'true'\n ) {\n // HINT: DropdownCloser のonClickより先に実行するため、キャプチャフェーズで処理する\n element.addEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n const clearEventListener = () => {\n // 既存のイベントリスナーをクリーンアップ\n const element = getClickableElement()\n if (element) {\n element.removeEventListener('click', actualOnClick, CAPTURE_OPTION)\n }\n }\n\n // 初回セットアップ\n setupElement()\n\n // MutationObserverでDOM変更を監視\n const observer = new MutationObserver(() => {\n clearEventListener()\n setupElement()\n })\n\n observer.observe(currentRef, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n observer.disconnect()\n clearEventListener()\n }\n }, [targetId, actualOnClick])\n\n return (\n <span className=\"smarthr-ui-RemoteDialogTrigger shr-contents\" ref={ref}>\n {children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;AAMA;AACE;;AAGF;AACE;AAEI;AACD;AAEL;AAEO;AAML;AACA;AACA;AAEA;;;;AAKE;AACE;;AAEA;;;;;AAOF;;;;;;;AAUE;;;;AAKA;AACA;;;;;;;;AAWF;;;AAIE;;;;AAIF;;AAGA;;AAGA;AACE;AACA;AACF;AAEA;AACE;AACA;;AAEA;AACA;AACD;AAED;;AAEE;AACF;AACF;AAEA;AAKF;;"}
|
|
@@ -36,10 +36,14 @@ const DisclosureTrigger = ({ targetId, children, onClick }) => {
|
|
|
36
36
|
}
|
|
37
37
|
button.setAttribute('aria-expanded', expanded.toString());
|
|
38
38
|
button.setAttribute('aria-controls', targetId);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// Button は native disabled ではなく aria-disabled を使うため、
|
|
40
|
+
// 無効時はリスナーを貼らず開閉しないようにする(DropdownTrigger と同じ)
|
|
41
|
+
if (!button.disabled && button.getAttribute('aria-disabled') !== 'true') {
|
|
42
|
+
button.addEventListener('click', actualOnClick);
|
|
43
|
+
currentCleanup = () => {
|
|
44
|
+
button.removeEventListener('click', actualOnClick);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
43
47
|
};
|
|
44
48
|
setupButton();
|
|
45
49
|
const observer = new MutationObserver(setupButton);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DisclosureTrigger.cjs","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useCallback, useEffect, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const unstableRef = useRef({ onClick, setExpanded })\n unstableRef.current = { onClick, setExpanded }\n\n const actualOnClick = useCallback((e: MouseEvent) => {\n const toggleExpanded = () => {\n unstableRef.current.setExpanded((current) => !current)\n }\n\n if (unstableRef.current.onClick) {\n unstableRef.current.onClick(toggleExpanded, e)\n } else {\n toggleExpanded()\n }\n }, [])\n\n useEffect(() => {\n const wrapper = ref.current\n if (!wrapper) {\n return\n }\n\n let currentCleanup: (() => void) | undefined\n\n const setupButton = () => {\n currentCleanup?.()\n currentCleanup = undefined\n\n const button = wrapper.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n button.addEventListener('click', actualOnClick)\n\n
|
|
1
|
+
{"version":3,"file":"DisclosureTrigger.cjs","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useCallback, useEffect, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const unstableRef = useRef({ onClick, setExpanded })\n unstableRef.current = { onClick, setExpanded }\n\n const actualOnClick = useCallback((e: MouseEvent) => {\n const toggleExpanded = () => {\n unstableRef.current.setExpanded((current) => !current)\n }\n\n if (unstableRef.current.onClick) {\n unstableRef.current.onClick(toggleExpanded, e)\n } else {\n toggleExpanded()\n }\n }, [])\n\n useEffect(() => {\n const wrapper = ref.current\n if (!wrapper) {\n return\n }\n\n let currentCleanup: (() => void) | undefined\n\n const setupButton = () => {\n currentCleanup?.()\n currentCleanup = undefined\n\n const button = wrapper.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n\n // Button は native disabled ではなく aria-disabled を使うため、\n // 無効時はリスナーを貼らず開閉しないようにする(DropdownTrigger と同じ)\n if (!button.disabled && button.getAttribute('aria-disabled') !== 'true') {\n button.addEventListener('click', actualOnClick)\n\n currentCleanup = () => {\n button.removeEventListener('click', actualOnClick)\n }\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(wrapper, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n currentCleanup?.()\n observer.disconnect()\n }\n }, [expanded, targetId, actualOnClick])\n\n // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。\n // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる\n // TODO: 将来的にspan -> Fragmentに変更する\n return (\n <span className=\"smarthr-ui-DisclosureTriggerWrapper shr-contents\" ref={ref}>\n {children instanceof Function ? children({ expanded }) : children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;;;AAoBO;;AAEL;;;AAKA;;AAEI;AACF;AAEA;;;;AAGE;;;;AAKF;;;;AAKA;;;;;;AASI;;;AAIF;;;AAIA;AACE;;AAGE;AACF;;AAEJ;AAEA;AAEA;AACA;AACE;AACA;;AAEA;AACA;AACD;AAED;;;AAGA;;;;;AAMF;AAKF;;"}
|
|
@@ -34,10 +34,14 @@ const DisclosureTrigger = ({ targetId, children, onClick }) => {
|
|
|
34
34
|
}
|
|
35
35
|
button.setAttribute('aria-expanded', expanded.toString());
|
|
36
36
|
button.setAttribute('aria-controls', targetId);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// Button は native disabled ではなく aria-disabled を使うため、
|
|
38
|
+
// 無効時はリスナーを貼らず開閉しないようにする(DropdownTrigger と同じ)
|
|
39
|
+
if (!button.disabled && button.getAttribute('aria-disabled') !== 'true') {
|
|
40
|
+
button.addEventListener('click', actualOnClick);
|
|
41
|
+
currentCleanup = () => {
|
|
42
|
+
button.removeEventListener('click', actualOnClick);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
41
45
|
};
|
|
42
46
|
setupButton();
|
|
43
47
|
const observer = new MutationObserver(setupButton);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DisclosureTrigger.js","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useCallback, useEffect, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const unstableRef = useRef({ onClick, setExpanded })\n unstableRef.current = { onClick, setExpanded }\n\n const actualOnClick = useCallback((e: MouseEvent) => {\n const toggleExpanded = () => {\n unstableRef.current.setExpanded((current) => !current)\n }\n\n if (unstableRef.current.onClick) {\n unstableRef.current.onClick(toggleExpanded, e)\n } else {\n toggleExpanded()\n }\n }, [])\n\n useEffect(() => {\n const wrapper = ref.current\n if (!wrapper) {\n return\n }\n\n let currentCleanup: (() => void) | undefined\n\n const setupButton = () => {\n currentCleanup?.()\n currentCleanup = undefined\n\n const button = wrapper.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n button.addEventListener('click', actualOnClick)\n\n
|
|
1
|
+
{"version":3,"file":"DisclosureTrigger.js","sources":["../../../src/components/Disclosure/DisclosureTrigger.tsx"],"sourcesContent":["'use client'\n\nimport { type FC, type ReactElement, useCallback, useEffect, useRef } from 'react'\n\nimport { useDisclosure } from './useDisclosure'\n\ntype DisclosureTriggerNodeChildren = Omit<\n ReactElement,\n 'onClick' | 'aria-expanded' | 'aria-controls'\n>\ntype DisclosureTriggerFuncChildren = (args: { expanded: boolean }) => DisclosureTriggerNodeChildren\n\ntype DisclosureTriggerProps = {\n /** DisclosureContentのidと紐づける文字列 */\n targetId: string\n /** 開閉時のハンドラー */\n onClick?: (open: () => void, e: MouseEvent) => void\n children: DisclosureTriggerNodeChildren | DisclosureTriggerFuncChildren\n}\n\nexport const DisclosureTrigger: FC<DisclosureTriggerProps> = ({ targetId, children, onClick }) => {\n const [expanded, setExpanded] = useDisclosure(targetId)\n const ref = useRef<HTMLSpanElement | null>(null)\n\n const unstableRef = useRef({ onClick, setExpanded })\n unstableRef.current = { onClick, setExpanded }\n\n const actualOnClick = useCallback((e: MouseEvent) => {\n const toggleExpanded = () => {\n unstableRef.current.setExpanded((current) => !current)\n }\n\n if (unstableRef.current.onClick) {\n unstableRef.current.onClick(toggleExpanded, e)\n } else {\n toggleExpanded()\n }\n }, [])\n\n useEffect(() => {\n const wrapper = ref.current\n if (!wrapper) {\n return\n }\n\n let currentCleanup: (() => void) | undefined\n\n const setupButton = () => {\n currentCleanup?.()\n currentCleanup = undefined\n\n const button = wrapper.querySelector('button')\n\n if (!button) {\n throw new Error('DisclosureTriggerのchildrenにbutton要素を設置してください')\n }\n\n button.setAttribute('aria-expanded', expanded.toString())\n button.setAttribute('aria-controls', targetId)\n\n // Button は native disabled ではなく aria-disabled を使うため、\n // 無効時はリスナーを貼らず開閉しないようにする(DropdownTrigger と同じ)\n if (!button.disabled && button.getAttribute('aria-disabled') !== 'true') {\n button.addEventListener('click', actualOnClick)\n\n currentCleanup = () => {\n button.removeEventListener('click', actualOnClick)\n }\n }\n }\n\n setupButton()\n\n const observer = new MutationObserver(setupButton)\n observer.observe(wrapper, {\n childList: true,\n subtree: true,\n // button要素の disabled / aria-disabled が動的に変化した場合も検知してリスナーを貼り直す\n attributes: true,\n attributeFilter: ['disabled', 'aria-disabled'],\n })\n\n return () => {\n currentCleanup?.()\n observer.disconnect()\n }\n }, [expanded, targetId, actualOnClick])\n\n // HINT: 念の為spanに対して外部からstyleを当てられるようにしておく。\n // Fragmentにrefが渡せるようになったタイミングでclassNameも不要になる\n // TODO: 将来的にspan -> Fragmentに変更する\n return (\n <span className=\"smarthr-ui-DisclosureTriggerWrapper shr-contents\" ref={ref}>\n {children instanceof Function ? children({ expanded }) : children}\n </span>\n )\n}\n"],"names":[],"mappings":";;;;;AAoBO;;AAEL;;;AAKA;;AAEI;AACF;AAEA;;;;AAGE;;;;AAKF;;;;AAKA;;;;;;AASI;;;AAIF;;;AAIA;AACE;;AAGE;AACF;;AAEJ;AAEA;AAEA;AACA;AACE;AACA;;AAEA;AACA;AACD;AAED;;;AAGA;;;;;AAMF;AAKF;;"}
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
var jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
var decimal = require('./../../vendor/.pnpm/decimal.js@10.6.0/vendor/decimal.js/decimal.cjs');
|
|
6
6
|
var React = require('react');
|
|
7
|
+
var index = require('./../../vendor/.pnpm/tailwind-variants@0.3.1_tailwindcss@3.4.19_ts-node@10.9.2_@swc_core@1.15.17_@types_node@20.19.35_typescript@5.9.3__/vendor/tailwind-variants/dist/index.cjs');
|
|
8
|
+
var hooks_useEnvironment_useEnvironment = require('../../hooks/useEnvironment/useEnvironment.cjs');
|
|
9
|
+
require('../../themes/createSpacing.cjs');
|
|
10
|
+
require('../../_virtual/index.cjs');
|
|
11
|
+
require('../../_virtual/index2.cjs');
|
|
12
|
+
require('../../themes/createColor.cjs');
|
|
13
|
+
require('../../themes/createTextColor.cjs');
|
|
14
|
+
require('styled-components');
|
|
15
|
+
require('../../themes/createShadow/defaultShadow.cjs');
|
|
16
|
+
require('../../themes/createWidth.cjs');
|
|
17
|
+
require('../../themes/createBackgroundColor.cjs');
|
|
18
|
+
require('../../themes/createBorder.cjs');
|
|
19
|
+
require('../../hooks/useTheme.cjs');
|
|
7
20
|
require('../../intl/IntlProvider.cjs');
|
|
8
21
|
require('react-intl');
|
|
9
22
|
var intl_Localizer = require('../../intl/Localizer.cjs');
|
|
@@ -34,6 +47,8 @@ var components_Scroller_Scroller = require('../Scroller/Scroller.cjs');
|
|
|
34
47
|
var components_VisuallyHiddenText_VisuallyHiddenText = require('../VisuallyHiddenText/VisuallyHiddenText.cjs');
|
|
35
48
|
var components_FileViewer_ImageViewer = require('./ImageViewer.cjs');
|
|
36
49
|
var components_FileViewer_PDFViewer = require('./PDFViewer.cjs');
|
|
50
|
+
var components_FileViewer_SearchController = require('./SearchController.cjs');
|
|
51
|
+
var components_FileViewer_usePDFSearch = require('./usePDFSearch.cjs');
|
|
37
52
|
|
|
38
53
|
const defaultScaleStep = new decimal.Decimal(0.2);
|
|
39
54
|
const defaultScaleSteps = [0.2, 0.6, 1, 1.6, 2, 3];
|
|
@@ -43,6 +58,8 @@ const FileViewer = ({ file, scaleStep, scaleSteps, width: fixedWidth, onPassword
|
|
|
43
58
|
const [loaded, setLoaded] = React.useState(false);
|
|
44
59
|
const [rotation, setRotation] = React.useState(undefined);
|
|
45
60
|
const [width, setWidth] = React.useState(fixedWidth ?? 0);
|
|
61
|
+
const isPDF = file.contentType === 'application/pdf';
|
|
62
|
+
const search = components_FileViewer_usePDFSearch.usePDFSearch(file.url);
|
|
46
63
|
const internalScaleStep = React.useMemo(() => (scaleStep ? new decimal.Decimal(scaleStep) : defaultScaleStep), [scaleStep]);
|
|
47
64
|
const scaleUp = React.useCallback(() => {
|
|
48
65
|
setScale((currentScale) => new decimal.Decimal(currentScale).add(internalScaleStep).toNumber());
|
|
@@ -74,9 +91,25 @@ const FileViewer = ({ file, scaleStep, scaleSteps, width: fixedWidth, onPassword
|
|
|
74
91
|
resizeObserver.disconnect();
|
|
75
92
|
};
|
|
76
93
|
}, [fixedWidth]);
|
|
77
|
-
return (jsxRuntime.jsxs(components_Scroller_Scroller.Scroller, { direction: "both", className: "shr-flex shr-h-full shr-w-full shr-flex-col shr-gap-2 shr-bg-scrim shr-bg-[radial-gradient(theme(textColor.black)_1px,_transparent_0)] shr-bg-[length:16px_16px]", ref: ref, children: [jsxRuntime.jsx("div", { className: "shr-sticky shr-start-0 shr-top-0 shr-z-[1] shr-flex shr-w-full shr-flex-shrink-0 shr-gap-0.5", children: jsxRuntime.jsx(Controller, { scale: scale, setScale: setScale, scaleSteps: scaleSteps || defaultScaleSteps, onClickScaleUpButton: scaleUp, onClickScaleDownButton: scaleDown, onClickRotateButton: rotate }) }), jsxRuntime.jsxs("div", { className: "shr-z-[0] shr-mx-auto shr-my-0 shr-box-border shr-flex shr-w-fit shr-flex-shrink-0 shr-grow shr-items-center shr-justify-center shr-px-2 shr-pb-2", children: [!loaded && (jsxRuntime.jsx("div", { className: "shr-pointer-events-none shr-fixed shr-inset-0 shr-flex shr-h-full shr-w-full shr-items-center shr-justify-center", children: jsxRuntime.jsx(components_Loader_Loader.Loader, { type: "light", size: "M" }) })), jsxRuntime.jsx("div", { className: !loaded ? 'shr-invisible' : '', children:
|
|
94
|
+
return (jsxRuntime.jsxs(components_Scroller_Scroller.Scroller, { direction: "both", className: "shr-flex shr-h-full shr-w-full shr-flex-col shr-gap-2 shr-bg-scrim shr-bg-[radial-gradient(theme(textColor.black)_1px,_transparent_0)] shr-bg-[length:16px_16px]", ref: ref, children: [jsxRuntime.jsx("div", { className: "shr-sticky shr-start-0 shr-top-0 shr-z-[1] shr-flex shr-w-full shr-flex-shrink-0 shr-gap-0.5", children: jsxRuntime.jsx(Controller, { scale: scale, setScale: setScale, scaleSteps: scaleSteps || defaultScaleSteps, onClickScaleUpButton: scaleUp, onClickScaleDownButton: scaleDown, onClickRotateButton: rotate, searchController: isPDF ? jsxRuntime.jsx(components_FileViewer_SearchController.SearchController, { search: search }) : undefined }) }), jsxRuntime.jsxs("div", { className: "shr-z-[0] shr-mx-auto shr-my-0 shr-box-border shr-flex shr-w-fit shr-flex-shrink-0 shr-grow shr-items-center shr-justify-center shr-px-2 shr-pb-2", children: [!loaded && (jsxRuntime.jsx("div", { className: "shr-pointer-events-none shr-fixed shr-inset-0 shr-flex shr-h-full shr-w-full shr-items-center shr-justify-center", children: jsxRuntime.jsx(components_Loader_Loader.Loader, { type: "light", size: "M" }) })), jsxRuntime.jsx("div", { className: !loaded ? 'shr-invisible' : '', children: isPDF ? (jsxRuntime.jsx(components_FileViewer_PDFViewer.PDFViewer, { scale: scale, rotation: rotation, file: file, width: width, onLoad: handleLoaded, onPDFLoaded: handlePDFLoaded, onPassword: onPassword, onLoadError: onLoadError, search: search })) : file.contentType.startsWith('image/') ? (jsxRuntime.jsx(components_FileViewer_ImageViewer.ImageViewer, { scale: scale, rotation: rotation, file: file, width: width, onLoad: handleLoaded, onLoadError: onLoadError })) : (jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/FileViewer/unsupportedFileText", defaultText: "\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u306A\u3044\u5F62\u5F0F\u306E\u30D5\u30A1\u30A4\u30EB\u3067\u3059\u3002" })) })] })] }));
|
|
78
95
|
};
|
|
79
|
-
const
|
|
96
|
+
const controllerClassNameGenerator = index.tv({
|
|
97
|
+
base: 'shr-sticky shr-grid shr-w-full shr-items-center shr-bg-scrim shr-py-0.5 shr-shadow-layer-1',
|
|
98
|
+
variants: {
|
|
99
|
+
mobile: {
|
|
100
|
+
true: 'shr-gap-0.5 shr-px-1',
|
|
101
|
+
false: 'shr-grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] shr-gap-1.5 shr-px-2',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const Controller = React.memo(({ scale, setScale, scaleSteps, onClickScaleUpButton, onClickScaleDownButton, onClickRotateButton, searchController, }) => {
|
|
106
|
+
const { mobile } = hooks_useEnvironment_useEnvironment.useEnvironment();
|
|
107
|
+
const className = React.useMemo(() => controllerClassNameGenerator({ mobile }), [mobile]);
|
|
108
|
+
const onClickScaleStep = React.useCallback((e) => setScale(Number(e.currentTarget.value)), [setScale]);
|
|
109
|
+
return (jsxRuntime.jsxs("div", { className: className, children: [!mobile && jsxRuntime.jsx("div", { role: "presentation", "aria-hidden": "true" }), jsxRuntime.jsxs(components_Layout_Cluster_Cluster.Cluster, { gap: 0.5, className: "shr-justify-self-center", children: [jsxRuntime.jsxs("div", { className: "shr-border-shorthand shr-flex shr-divide-x shr-divide-solid shr-overflow-hidden shr-rounded-m", children: [jsxRuntime.jsx(components_Button_Button.Button, { onClick: onClickScaleDownButton, disabled: scale <= scaleSteps[0], className: "shr-rounded-r-none shr-border-none", children: jsxRuntime.jsx(components_Icon_FaIcon.FaMagnifyingGlassMinusIcon, { alt: jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/FileViewer/scaleDownAlt", defaultText: "\u7E2E\u5C0F" }) }) }), jsxRuntime.jsx(components_Dropdown_DropdownMenuButton_DropdownMenuButton.DropdownMenuButton, { trigger: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(components_VisuallyHiddenText_VisuallyHiddenText.VisuallyHiddenText, { children: jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/FileViewer/scaleRateLabel", defaultText: "\u62E1\u5927\u7387" }) }), `${(scale * 100).toFixed(0)}%`] }), className: "shr-border-y-0 shr-border-[theme(borderColor.default)] [&_.smarthr-ui-Button]:shr-rounded-none [&_.smarthr-ui-Button]:shr-border-[transparent]", children: scaleSteps.map((step) => (jsxRuntime.jsx(components_Button_Button.Button, { value: step, onClick: onClickScaleStep, className: "shr-rounded-none shr-border-0", children: `${(step * 100).toFixed(0)}%` }, step.toString()))) }), jsxRuntime.jsx(components_Button_Button.Button, { onClick: onClickScaleUpButton, className: "shr-rounded-l-none shr-border-0", children: jsxRuntime.jsx(components_Icon_FaIcon.FaMagnifyingGlassPlusIcon, { alt: jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/FileViewer/scaleUpAlt", defaultText: "\u62E1\u5927" }) }) })] }), jsxRuntime.jsx(components_Button_Button.Button, { onClick: onClickRotateButton, className: "shr-p-0.75", children: jsxRuntime.jsx(components_Icon_FaIcon.FaArrowRotateLeftIcon, { alt: jsxRuntime.jsx(intl_Localizer.Localizer, { id: "smarthr-ui/FileViewer/rotateAlt", defaultText: "\u5DE6\u56DE\u8EE2" }) }) })] }), searchController ? (jsxRuntime.jsx("div", { className: "shr-min-w-0 shr-justify-self-stretch", children: searchController })) : (
|
|
110
|
+
/* PC 表示時のときに中央の操作ボタンたちを中央へ寄せるための空のスペーサー */
|
|
111
|
+
!mobile && jsxRuntime.jsx("div", { role: "presentation", "aria-hidden": "true" }))] }));
|
|
112
|
+
});
|
|
80
113
|
|
|
81
114
|
exports.FileViewer = FileViewer;
|
|
82
115
|
//# sourceMappingURL=FileViewer.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileViewer.cjs","sources":["../../../src/components/FileViewer/FileViewer.tsx"],"sourcesContent":["'use client'\n\nimport Decimal from 'decimal.js'\nimport {\n type ComponentProps,\n type FC,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\n\nimport { Localizer } from '../../intl'\nimport { Button } from '../Button'\nimport { DropdownMenuButton } from '../Dropdown'\nimport {\n FaArrowRotateLeftIcon,\n FaMagnifyingGlassMinusIcon,\n FaMagnifyingGlassPlusIcon,\n} from '../Icon'\nimport { Cluster } from '../Layout'\nimport { Loader } from '../Loader'\nimport { Scroller } from '../Scroller'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\nimport { ImageViewer } from './ImageViewer'\nimport { PDFViewer } from './PDFViewer'\n\nimport type { FileForViewer } from './types'\n\nconst defaultScaleStep = new Decimal(0.2)\nconst defaultScaleSteps = [0.2, 0.6, 1, 1.6, 2, 3]\n\ntype Props = {\n file: FileForViewer\n width?: number\n\n /*\n * 拡大縮小率のステップを、100%を1とした配列で指定します。\n * */\n scaleSteps?: number[]\n\n scaleStep?: number\n onPassword?: ComponentProps<typeof PDFViewer>['onPassword']\n onLoadError?: () => void\n}\n\nexport const FileViewer: FC<Props> = ({\n file,\n scaleStep,\n scaleSteps,\n width: fixedWidth,\n onPassword,\n onLoadError,\n}) => {\n const ref = useRef<HTMLDivElement>(null)\n const [scale, setScale] = useState(1)\n const [loaded, setLoaded] = useState(false)\n const [rotation, setRotation] = useState<number | undefined>(undefined)\n const [width, setWidth] = useState(fixedWidth ?? 0)\n\n const internalScaleStep = useMemo(\n () => (scaleStep ? new Decimal(scaleStep) : defaultScaleStep),\n [scaleStep],\n )\n\n const scaleUp = useCallback(() => {\n setScale((currentScale) => new Decimal(currentScale).add(internalScaleStep).toNumber())\n }, [internalScaleStep])\n\n const scaleDown = useCallback(() => {\n setScale((currentScale) => new Decimal(currentScale).sub(internalScaleStep).toNumber())\n }, [internalScaleStep])\n\n const rotate = useCallback(() => {\n // HINT: react-pdf側のAnnotationLayer.cssではマイナスの回転に対応しておらず、また0, 90, 180, 270度のみ対応しているため、-90度の場合は+270度として扱う\n const currentRotation = rotation ?? 0\n const newRotation = currentRotation === 0 ? 270 : currentRotation - 90\n setRotation(newRotation)\n }, [rotation])\n\n const handleLoaded = useCallback(() => {\n setLoaded(true)\n }, [])\n\n const handlePDFLoaded = useCallback((defaultRotation: number) => {\n setRotation(defaultRotation)\n }, [])\n\n useEffect(() => {\n if (!ref.current || fixedWidth !== undefined) {\n return\n }\n\n const resizeObserver = new ResizeObserver(() => {\n setWidth((ref.current?.clientWidth ?? 0) - 64)\n })\n\n resizeObserver.observe(ref.current)\n\n return () => {\n resizeObserver.disconnect()\n }\n }, [fixedWidth])\n\n return (\n <Scroller\n direction=\"both\"\n className=\"shr-flex shr-h-full shr-w-full shr-flex-col shr-gap-2 shr-bg-scrim shr-bg-[radial-gradient(theme(textColor.black)_1px,_transparent_0)] shr-bg-[length:16px_16px]\"\n ref={ref}\n >\n <div className=\"shr-sticky shr-start-0 shr-top-0 shr-z-[1] shr-flex shr-w-full shr-flex-shrink-0 shr-gap-0.5\">\n <Controller\n scale={scale}\n setScale={setScale}\n scaleSteps={scaleSteps || defaultScaleSteps}\n onClickScaleUpButton={scaleUp}\n onClickScaleDownButton={scaleDown}\n onClickRotateButton={rotate}\n />\n </div>\n <div className=\"shr-z-[0] shr-mx-auto shr-my-0 shr-box-border shr-flex shr-w-fit shr-flex-shrink-0 shr-grow shr-items-center shr-justify-center shr-px-2 shr-pb-2\">\n {!loaded && (\n <div className=\"shr-pointer-events-none shr-fixed shr-inset-0 shr-flex shr-h-full shr-w-full shr-items-center shr-justify-center\">\n <Loader type=\"light\" size=\"M\" />\n </div>\n )}\n <div className={!loaded ? 'shr-invisible' : ''}>\n {
|
|
1
|
+
{"version":3,"file":"FileViewer.cjs","sources":["../../../src/components/FileViewer/FileViewer.tsx"],"sourcesContent":["'use client'\n\nimport Decimal from 'decimal.js'\nimport {\n type ComponentProps,\n type FC,\n type MouseEvent,\n type ReactNode,\n memo,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport { tv } from 'tailwind-variants'\n\nimport { useEnvironment } from '../../hooks/useEnvironment'\nimport { Localizer } from '../../intl'\nimport { Button } from '../Button'\nimport { DropdownMenuButton } from '../Dropdown'\nimport {\n FaArrowRotateLeftIcon,\n FaMagnifyingGlassMinusIcon,\n FaMagnifyingGlassPlusIcon,\n} from '../Icon'\nimport { Cluster } from '../Layout'\nimport { Loader } from '../Loader'\nimport { Scroller } from '../Scroller'\nimport { VisuallyHiddenText } from '../VisuallyHiddenText'\n\nimport { ImageViewer } from './ImageViewer'\nimport { PDFViewer } from './PDFViewer'\nimport { SearchController } from './SearchController'\nimport { usePDFSearch } from './usePDFSearch'\n\nimport type { FileForViewer } from './types'\n\nconst defaultScaleStep = new Decimal(0.2)\nconst defaultScaleSteps = [0.2, 0.6, 1, 1.6, 2, 3]\n\ntype Props = {\n file: FileForViewer\n width?: number\n\n /*\n * 拡大縮小率のステップを、100%を1とした配列で指定します。\n * */\n scaleSteps?: number[]\n\n scaleStep?: number\n onPassword?: ComponentProps<typeof PDFViewer>['onPassword']\n onLoadError?: () => void\n}\n\nexport const FileViewer: FC<Props> = ({\n file,\n scaleStep,\n scaleSteps,\n width: fixedWidth,\n onPassword,\n onLoadError,\n}) => {\n const ref = useRef<HTMLDivElement>(null)\n const [scale, setScale] = useState(1)\n const [loaded, setLoaded] = useState(false)\n const [rotation, setRotation] = useState<number | undefined>(undefined)\n const [width, setWidth] = useState(fixedWidth ?? 0)\n const isPDF = file.contentType === 'application/pdf'\n\n const search = usePDFSearch(file.url)\n\n const internalScaleStep = useMemo(\n () => (scaleStep ? new Decimal(scaleStep) : defaultScaleStep),\n [scaleStep],\n )\n\n const scaleUp = useCallback(() => {\n setScale((currentScale) => new Decimal(currentScale).add(internalScaleStep).toNumber())\n }, [internalScaleStep])\n\n const scaleDown = useCallback(() => {\n setScale((currentScale) => new Decimal(currentScale).sub(internalScaleStep).toNumber())\n }, [internalScaleStep])\n\n const rotate = useCallback(() => {\n // HINT: react-pdf側のAnnotationLayer.cssではマイナスの回転に対応しておらず、また0, 90, 180, 270度のみ対応しているため、-90度の場合は+270度として扱う\n const currentRotation = rotation ?? 0\n const newRotation = currentRotation === 0 ? 270 : currentRotation - 90\n setRotation(newRotation)\n }, [rotation])\n\n const handleLoaded = useCallback(() => {\n setLoaded(true)\n }, [])\n\n const handlePDFLoaded = useCallback((defaultRotation: number) => {\n setRotation(defaultRotation)\n }, [])\n\n useEffect(() => {\n if (!ref.current || fixedWidth !== undefined) {\n return\n }\n\n const resizeObserver = new ResizeObserver(() => {\n setWidth((ref.current?.clientWidth ?? 0) - 64)\n })\n\n resizeObserver.observe(ref.current)\n\n return () => {\n resizeObserver.disconnect()\n }\n }, [fixedWidth])\n\n return (\n <Scroller\n direction=\"both\"\n className=\"shr-flex shr-h-full shr-w-full shr-flex-col shr-gap-2 shr-bg-scrim shr-bg-[radial-gradient(theme(textColor.black)_1px,_transparent_0)] shr-bg-[length:16px_16px]\"\n ref={ref}\n >\n <div className=\"shr-sticky shr-start-0 shr-top-0 shr-z-[1] shr-flex shr-w-full shr-flex-shrink-0 shr-gap-0.5\">\n <Controller\n scale={scale}\n setScale={setScale}\n scaleSteps={scaleSteps || defaultScaleSteps}\n onClickScaleUpButton={scaleUp}\n onClickScaleDownButton={scaleDown}\n onClickRotateButton={rotate}\n searchController={isPDF ? <SearchController search={search} /> : undefined}\n />\n </div>\n <div className=\"shr-z-[0] shr-mx-auto shr-my-0 shr-box-border shr-flex shr-w-fit shr-flex-shrink-0 shr-grow shr-items-center shr-justify-center shr-px-2 shr-pb-2\">\n {!loaded && (\n <div className=\"shr-pointer-events-none shr-fixed shr-inset-0 shr-flex shr-h-full shr-w-full shr-items-center shr-justify-center\">\n <Loader type=\"light\" size=\"M\" />\n </div>\n )}\n <div className={!loaded ? 'shr-invisible' : ''}>\n {isPDF ? (\n <PDFViewer\n scale={scale}\n rotation={rotation}\n file={file}\n width={width}\n onLoad={handleLoaded}\n onPDFLoaded={handlePDFLoaded}\n onPassword={onPassword}\n onLoadError={onLoadError}\n search={search}\n />\n ) : file.contentType.startsWith('image/') ? (\n <ImageViewer\n scale={scale}\n rotation={rotation}\n file={file}\n width={width}\n onLoad={handleLoaded}\n onLoadError={onLoadError}\n />\n ) : (\n <Localizer\n id=\"smarthr-ui/FileViewer/unsupportedFileText\"\n defaultText=\"サポートされていない形式のファイルです。\"\n />\n )}\n </div>\n </div>\n </Scroller>\n )\n}\n\ntype ControllerProps = {\n scale: number\n setScale: (scale: number) => void\n scaleSteps: number[]\n onClickScaleUpButton: () => void\n onClickScaleDownButton: () => void\n onClickRotateButton: () => void\n searchController?: ReactNode\n}\n\nconst controllerClassNameGenerator = tv({\n base: 'shr-sticky shr-grid shr-w-full shr-items-center shr-bg-scrim shr-py-0.5 shr-shadow-layer-1',\n variants: {\n mobile: {\n true: 'shr-gap-0.5 shr-px-1',\n false: 'shr-grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] shr-gap-1.5 shr-px-2',\n },\n },\n})\n\nconst Controller: FC<ControllerProps> = memo(\n ({\n scale,\n setScale,\n scaleSteps,\n onClickScaleUpButton,\n onClickScaleDownButton,\n onClickRotateButton,\n searchController,\n }) => {\n const { mobile } = useEnvironment()\n const className = useMemo(() => controllerClassNameGenerator({ mobile }), [mobile])\n\n const onClickScaleStep = useCallback(\n (e: MouseEvent<HTMLButtonElement>) => setScale(Number(e.currentTarget.value)),\n [setScale],\n )\n\n return (\n <div className={className}>\n {/* PC 表示時のときに中央の操作ボタンたちを中央へ寄せるための空のスペーサー */}\n {!mobile && <div role=\"presentation\" aria-hidden=\"true\" />}\n <Cluster gap={0.5} className=\"shr-justify-self-center\">\n <div className=\"shr-border-shorthand shr-flex shr-divide-x shr-divide-solid shr-overflow-hidden shr-rounded-m\">\n <Button\n onClick={onClickScaleDownButton}\n disabled={scale <= scaleSteps[0]}\n className=\"shr-rounded-r-none shr-border-none\"\n >\n <FaMagnifyingGlassMinusIcon\n alt={<Localizer id=\"smarthr-ui/FileViewer/scaleDownAlt\" defaultText=\"縮小\" />}\n />\n </Button>\n <DropdownMenuButton\n trigger={\n <>\n <VisuallyHiddenText>\n <Localizer id=\"smarthr-ui/FileViewer/scaleRateLabel\" defaultText=\"拡大率\" />\n </VisuallyHiddenText>\n {`${(scale * 100).toFixed(0)}%`}\n </>\n }\n className=\"shr-border-y-0 shr-border-[theme(borderColor.default)] [&_.smarthr-ui-Button]:shr-rounded-none [&_.smarthr-ui-Button]:shr-border-[transparent]\"\n >\n {scaleSteps.map((step) => (\n <Button\n key={step.toString()}\n value={step}\n onClick={onClickScaleStep}\n className=\"shr-rounded-none shr-border-0\"\n >\n {`${(step * 100).toFixed(0)}%`}\n </Button>\n ))}\n </DropdownMenuButton>\n <Button onClick={onClickScaleUpButton} className=\"shr-rounded-l-none shr-border-0\">\n <FaMagnifyingGlassPlusIcon\n alt={<Localizer id=\"smarthr-ui/FileViewer/scaleUpAlt\" defaultText=\"拡大\" />}\n />\n </Button>\n </div>\n <Button onClick={onClickRotateButton} className=\"shr-p-0.75\">\n <FaArrowRotateLeftIcon\n alt={<Localizer id=\"smarthr-ui/FileViewer/rotateAlt\" defaultText=\"左回転\" />}\n />\n </Button>\n </Cluster>\n {searchController ? (\n <div className=\"shr-min-w-0 shr-justify-self-stretch\">{searchController}</div>\n ) : (\n /* PC 表示時のときに中央の操作ボタンたちを中央へ寄せるための空のスペーサー */\n !mobile && <div role=\"presentation\" aria-hidden=\"true\" />\n )}\n </div>\n )\n },\n)\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA;AACA;;AAwBE;;;;AAIA;AACA;;;AASA;;AAEA;AAEA;;AAEA;AAEA;;AAEE;AACA;;AAEF;AAEA;;;AAIA;;;;;;;AASE;AACE;AACF;AAEA;AAEA;;AAEA;AACF;;AAyDF;AAYA;AACE;AACA;AACE;AACE;AACA;AACD;AACF;AACF;AAED;AAUI;AACA;;;;AA4DM;AAIR;;"}
|