smarthr-ui 99.2.0 → 99.3.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/AppHeader/AppHeader.cjs +2 -2
- package/lib/components/AppHeader/AppHeader.cjs.map +1 -1
- package/lib/components/AppHeader/AppHeader.js +2 -2
- package/lib/components/AppHeader/AppHeader.js.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs +31 -37
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.cjs.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.js +33 -39
- package/lib/components/Combobox/MultiCombobox/MultiCombobox.js.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiSelectedItem.cjs +8 -6
- package/lib/components/Combobox/MultiCombobox/MultiSelectedItem.cjs.map +1 -1
- package/lib/components/Combobox/MultiCombobox/MultiSelectedItem.d.ts +2 -3
- package/lib/components/Combobox/MultiCombobox/MultiSelectedItem.js +8 -7
- package/lib/components/Combobox/MultiCombobox/MultiSelectedItem.js.map +1 -1
- package/lib/components/Combobox/useListbox.cjs +24 -3
- package/lib/components/Combobox/useListbox.cjs.map +1 -1
- package/lib/components/Combobox/useListbox.js +24 -3
- package/lib/components/Combobox/useListbox.js.map +1 -1
- package/lib/components/Disclosure/useDisclosure.cjs +24 -19
- package/lib/components/Disclosure/useDisclosure.cjs.map +1 -1
- package/lib/components/Disclosure/useDisclosure.js +25 -20
- package/lib/components/Disclosure/useDisclosure.js.map +1 -1
- package/lib/components/FileViewer/FileViewer.cjs +35 -12
- package/lib/components/FileViewer/FileViewer.cjs.map +1 -1
- package/lib/components/FileViewer/FileViewer.d.ts +3 -1
- package/lib/components/FileViewer/FileViewer.js +36 -13
- package/lib/components/FileViewer/FileViewer.js.map +1 -1
- package/lib/components/FileViewer/ImageViewer.cjs +2 -2
- package/lib/components/FileViewer/ImageViewer.cjs.map +1 -1
- package/lib/components/FileViewer/ImageViewer.js +2 -2
- package/lib/components/FileViewer/ImageViewer.js.map +1 -1
- package/lib/components/FileViewer/PDFViewer.cjs +23 -15
- package/lib/components/FileViewer/PDFViewer.cjs.map +1 -1
- package/lib/components/FileViewer/PDFViewer.js +24 -16
- package/lib/components/FileViewer/PDFViewer.js.map +1 -1
- package/lib/components/FileViewer/types.d.ts +2 -2
- package/lib/components/InputFile/FilePreviewDialog.cjs +2 -2
- package/lib/components/InputFile/FilePreviewDialog.cjs.map +1 -1
- package/lib/components/InputFile/FilePreviewDialog.d.ts +1 -0
- package/lib/components/InputFile/FilePreviewDialog.js +2 -2
- package/lib/components/InputFile/FilePreviewDialog.js.map +1 -1
- package/lib/components/InputFile/InputFile.cjs +6 -3
- package/lib/components/InputFile/InputFile.cjs.map +1 -1
- package/lib/components/InputFile/InputFile.js +6 -3
- package/lib/components/InputFile/InputFile.js.map +1 -1
- package/lib/components/InputFile/InputFileMultiplyAppendable.cjs +2 -2
- package/lib/components/InputFile/InputFileMultiplyAppendable.cjs.map +1 -1
- package/lib/components/InputFile/InputFileMultiplyAppendable.d.ts +2 -2
- package/lib/components/InputFile/InputFileMultiplyAppendable.js +2 -2
- package/lib/components/InputFile/InputFileMultiplyAppendable.js.map +1 -1
- package/lib/components/InputFile/InputFileNative.cjs +2 -2
- package/lib/components/InputFile/InputFileNative.cjs.map +1 -1
- package/lib/components/InputFile/InputFileNative.d.ts +2 -2
- package/lib/components/InputFile/InputFileNative.js +2 -2
- package/lib/components/InputFile/InputFileNative.js.map +1 -1
- package/lib/components/InputFile/types.d.ts +8 -1
- package/lib/components/Stepper/HorizontalStepItem.cjs +4 -4
- package/lib/components/Stepper/HorizontalStepItem.cjs.map +1 -1
- package/lib/components/Stepper/HorizontalStepItem.d.ts +3 -1
- package/lib/components/Stepper/HorizontalStepItem.js +4 -4
- package/lib/components/Stepper/HorizontalStepItem.js.map +1 -1
- package/lib/components/Stepper/StepCounter.cjs +4 -4
- package/lib/components/Stepper/StepCounter.cjs.map +1 -1
- package/lib/components/Stepper/StepCounter.d.ts +3 -2
- package/lib/components/Stepper/StepCounter.js +4 -4
- package/lib/components/Stepper/StepCounter.js.map +1 -1
- package/lib/components/Stepper/StepStatusIcon.cjs +3 -5
- package/lib/components/Stepper/StepStatusIcon.cjs.map +1 -1
- package/lib/components/Stepper/StepStatusIcon.d.ts +4 -5
- package/lib/components/Stepper/StepStatusIcon.js +3 -5
- package/lib/components/Stepper/StepStatusIcon.js.map +1 -1
- package/lib/components/Stepper/Stepper.cjs +7 -2
- package/lib/components/Stepper/Stepper.cjs.map +1 -1
- package/lib/components/Stepper/Stepper.js +7 -2
- package/lib/components/Stepper/Stepper.js.map +1 -1
- package/lib/components/Stepper/VerticalStepItem.cjs +4 -4
- package/lib/components/Stepper/VerticalStepItem.cjs.map +1 -1
- package/lib/components/Stepper/VerticalStepItem.d.ts +3 -1
- package/lib/components/Stepper/VerticalStepItem.js +4 -4
- package/lib/components/Stepper/VerticalStepItem.js.map +1 -1
- package/lib/components/Textarea/Textarea.cjs +14 -37
- package/lib/components/Textarea/Textarea.cjs.map +1 -1
- package/lib/components/Textarea/Textarea.js +14 -37
- package/lib/components/Textarea/Textarea.js.map +1 -1
- package/lib/components/Timeline/TimelineItem.cjs +11 -15
- package/lib/components/Timeline/TimelineItem.cjs.map +1 -1
- package/lib/components/Timeline/TimelineItem.d.ts +3 -3
- package/lib/components/Timeline/TimelineItem.js +11 -15
- package/lib/components/Timeline/TimelineItem.js.map +1 -1
- package/metadata.json +1 -1
- package/package.json +1 -1
- package/smarthr-ui.css +47 -29
|
@@ -6,14 +6,14 @@ var components_AppHeader_components_desktop_DesktopHeader = require('./component
|
|
|
6
6
|
var components_AppHeader_components_mobile_MobileHeader = require('./components/mobile/MobileHeader.cjs');
|
|
7
7
|
var components_AppHeader_hooks_useMediaQuery = require('./hooks/useMediaQuery.cjs');
|
|
8
8
|
|
|
9
|
-
const AppHeader = ({ children, ...rest }) => {
|
|
9
|
+
const AppHeader = ({ children, desktopAdditionalContent, desktopNavigationAdditionalContent, mobileAdditionalContent, ...rest }) => {
|
|
10
10
|
// NOTE: ヘッダーの出し分けは CSS によって行われているので、useMediaQuery による children の出し分けは本来不要ですが、
|
|
11
11
|
// wovn の言語切替カスタム UI の挿入対象となる DOM ("wovn-embedded-widget-anchor" クラスを持った div) が複数描画されていると、
|
|
12
12
|
// wovn のスクリプトの仕様上1つ目の DOM にしか UI が挿入されないため、やむを得ず children のみ React のレンダリングレベルでの出し分けをしています。
|
|
13
13
|
const isDesktop = components_AppHeader_hooks_useMediaQuery.useMediaQuery(components_AppHeader_hooks_useMediaQuery.mediaQuery.desktop);
|
|
14
14
|
// HINT: Desktop,Mobileの両ヘッダーは常にHTML上に存在し、cssでvisibleを切り替えることでSSR環境でのレイアウトシフトが発生しないようにしています
|
|
15
15
|
// 表示切替は画面幅によって決まり、SSR環境では判定出来ないためです
|
|
16
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(components_AppHeader_components_desktop_DesktopHeader.DesktopHeader, { ...rest, children: isDesktop ? children : undefined }), jsxRuntime.jsx(components_AppHeader_components_mobile_MobileHeader.MobileHeader, { ...rest, children: isDesktop ? undefined : children })] }));
|
|
16
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(components_AppHeader_components_desktop_DesktopHeader.DesktopHeader, { ...rest, desktopAdditionalContent: desktopAdditionalContent, desktopNavigationAdditionalContent: desktopNavigationAdditionalContent, children: isDesktop ? children : undefined }), jsxRuntime.jsx(components_AppHeader_components_mobile_MobileHeader.MobileHeader, { ...rest, mobileAdditionalContent: mobileAdditionalContent, children: isDesktop ? undefined : children })] }));
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
exports.AppHeader = AppHeader;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppHeader.cjs","sources":["../../../src/components/AppHeader/AppHeader.tsx"],"sourcesContent":["'use client'\n\nimport { DesktopHeader } from './components/desktop/DesktopHeader'\nimport { MobileHeader } from './components/mobile/MobileHeader'\nimport { mediaQuery, useMediaQuery } from './hooks/useMediaQuery'\n\nimport type { HeaderProps } from './types'\nimport type { FC } from 'react'\n\nexport const AppHeader: FC<HeaderProps> = ({
|
|
1
|
+
{"version":3,"file":"AppHeader.cjs","sources":["../../../src/components/AppHeader/AppHeader.tsx"],"sourcesContent":["'use client'\n\nimport { DesktopHeader } from './components/desktop/DesktopHeader'\nimport { MobileHeader } from './components/mobile/MobileHeader'\nimport { mediaQuery, useMediaQuery } from './hooks/useMediaQuery'\n\nimport type { HeaderProps } from './types'\nimport type { FC } from 'react'\n\nexport const AppHeader: FC<HeaderProps> = ({\n children,\n desktopAdditionalContent,\n desktopNavigationAdditionalContent,\n mobileAdditionalContent,\n ...rest\n}) => {\n // NOTE: ヘッダーの出し分けは CSS によって行われているので、useMediaQuery による children の出し分けは本来不要ですが、\n // wovn の言語切替カスタム UI の挿入対象となる DOM (\"wovn-embedded-widget-anchor\" クラスを持った div) が複数描画されていると、\n // wovn のスクリプトの仕様上1つ目の DOM にしか UI が挿入されないため、やむを得ず children のみ React のレンダリングレベルでの出し分けをしています。\n const isDesktop = useMediaQuery(mediaQuery.desktop)\n\n // HINT: Desktop,Mobileの両ヘッダーは常にHTML上に存在し、cssでvisibleを切り替えることでSSR環境でのレイアウトシフトが発生しないようにしています\n // 表示切替は画面幅によって決まり、SSR環境では判定出来ないためです\n return (\n <>\n <DesktopHeader\n {...rest}\n desktopAdditionalContent={desktopAdditionalContent}\n desktopNavigationAdditionalContent={desktopNavigationAdditionalContent}\n >\n {isDesktop ? children : undefined}\n </DesktopHeader>\n <MobileHeader {...rest} mobileAdditionalContent={mobileAdditionalContent}>\n {isDesktop ? undefined : children}\n </MobileHeader>\n </>\n )\n}\n"],"names":[],"mappings":";;;;;;;;AASO;;;;;;;;AA4BP;;"}
|
|
@@ -4,14 +4,14 @@ import { DesktopHeader } from './components/desktop/DesktopHeader.js';
|
|
|
4
4
|
import { MobileHeader } from './components/mobile/MobileHeader.js';
|
|
5
5
|
import { useMediaQuery, mediaQuery } from './hooks/useMediaQuery.js';
|
|
6
6
|
|
|
7
|
-
const AppHeader = ({ children, ...rest }) => {
|
|
7
|
+
const AppHeader = ({ children, desktopAdditionalContent, desktopNavigationAdditionalContent, mobileAdditionalContent, ...rest }) => {
|
|
8
8
|
// NOTE: ヘッダーの出し分けは CSS によって行われているので、useMediaQuery による children の出し分けは本来不要ですが、
|
|
9
9
|
// wovn の言語切替カスタム UI の挿入対象となる DOM ("wovn-embedded-widget-anchor" クラスを持った div) が複数描画されていると、
|
|
10
10
|
// wovn のスクリプトの仕様上1つ目の DOM にしか UI が挿入されないため、やむを得ず children のみ React のレンダリングレベルでの出し分けをしています。
|
|
11
11
|
const isDesktop = useMediaQuery(mediaQuery.desktop);
|
|
12
12
|
// HINT: Desktop,Mobileの両ヘッダーは常にHTML上に存在し、cssでvisibleを切り替えることでSSR環境でのレイアウトシフトが発生しないようにしています
|
|
13
13
|
// 表示切替は画面幅によって決まり、SSR環境では判定出来ないためです
|
|
14
|
-
return (jsxs(Fragment, { children: [jsx(DesktopHeader, { ...rest, children: isDesktop ? children : undefined }), jsx(MobileHeader, { ...rest, children: isDesktop ? undefined : children })] }));
|
|
14
|
+
return (jsxs(Fragment, { children: [jsx(DesktopHeader, { ...rest, desktopAdditionalContent: desktopAdditionalContent, desktopNavigationAdditionalContent: desktopNavigationAdditionalContent, children: isDesktop ? children : undefined }), jsx(MobileHeader, { ...rest, mobileAdditionalContent: mobileAdditionalContent, children: isDesktop ? undefined : children })] }));
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export { AppHeader };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppHeader.js","sources":["../../../src/components/AppHeader/AppHeader.tsx"],"sourcesContent":["'use client'\n\nimport { DesktopHeader } from './components/desktop/DesktopHeader'\nimport { MobileHeader } from './components/mobile/MobileHeader'\nimport { mediaQuery, useMediaQuery } from './hooks/useMediaQuery'\n\nimport type { HeaderProps } from './types'\nimport type { FC } from 'react'\n\nexport const AppHeader: FC<HeaderProps> = ({
|
|
1
|
+
{"version":3,"file":"AppHeader.js","sources":["../../../src/components/AppHeader/AppHeader.tsx"],"sourcesContent":["'use client'\n\nimport { DesktopHeader } from './components/desktop/DesktopHeader'\nimport { MobileHeader } from './components/mobile/MobileHeader'\nimport { mediaQuery, useMediaQuery } from './hooks/useMediaQuery'\n\nimport type { HeaderProps } from './types'\nimport type { FC } from 'react'\n\nexport const AppHeader: FC<HeaderProps> = ({\n children,\n desktopAdditionalContent,\n desktopNavigationAdditionalContent,\n mobileAdditionalContent,\n ...rest\n}) => {\n // NOTE: ヘッダーの出し分けは CSS によって行われているので、useMediaQuery による children の出し分けは本来不要ですが、\n // wovn の言語切替カスタム UI の挿入対象となる DOM (\"wovn-embedded-widget-anchor\" クラスを持った div) が複数描画されていると、\n // wovn のスクリプトの仕様上1つ目の DOM にしか UI が挿入されないため、やむを得ず children のみ React のレンダリングレベルでの出し分けをしています。\n const isDesktop = useMediaQuery(mediaQuery.desktop)\n\n // HINT: Desktop,Mobileの両ヘッダーは常にHTML上に存在し、cssでvisibleを切り替えることでSSR環境でのレイアウトシフトが発生しないようにしています\n // 表示切替は画面幅によって決まり、SSR環境では判定出来ないためです\n return (\n <>\n <DesktopHeader\n {...rest}\n desktopAdditionalContent={desktopAdditionalContent}\n desktopNavigationAdditionalContent={desktopNavigationAdditionalContent}\n >\n {isDesktop ? children : undefined}\n </DesktopHeader>\n <MobileHeader {...rest} mobileAdditionalContent={mobileAdditionalContent}>\n {isDesktop ? undefined : children}\n </MobileHeader>\n </>\n )\n}\n"],"names":[],"mappings":";;;;;;AASO;;;;;;;;AA4BP;;"}
|
|
@@ -14,6 +14,7 @@ require('react-intl');
|
|
|
14
14
|
require('../../../intl/Localizer.cjs');
|
|
15
15
|
require('../../../_virtual/dayjs.min.cjs');
|
|
16
16
|
require('../../../intl/TimeFormatter.cjs');
|
|
17
|
+
var libs_delegate = require('../../../libs/delegate.cjs');
|
|
17
18
|
var libs_util = require('../../../libs/util.cjs');
|
|
18
19
|
var components_Icon_FaIcon = require('../../Icon/FaIcon.cjs');
|
|
19
20
|
require('../../Icon/generateIcon.cjs');
|
|
@@ -38,6 +39,7 @@ const EMPTY_INPUT_CHANGE_EVENT = {
|
|
|
38
39
|
currentTarget: { value: '' },
|
|
39
40
|
target: { value: '' },
|
|
40
41
|
};
|
|
42
|
+
const DELETE_BUTTON_CLASSNAME = `.${components_Combobox_MultiCombobox_MultiSelectedItem.DELETE_BUTTON_SELECTOR}`;
|
|
41
43
|
const classNameGenerator = index.tv({
|
|
42
44
|
slots: {
|
|
43
45
|
wrapper: [
|
|
@@ -92,17 +94,7 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
92
94
|
inputValue,
|
|
93
95
|
isItemSelected,
|
|
94
96
|
});
|
|
95
|
-
const selectedItemLength = selectedItems.length;
|
|
96
|
-
// TODO: 完全にcreateRefを作り直すのではなく、差分更新させたい
|
|
97
|
-
const deletionButtonRefs = React.useMemo(() => {
|
|
98
|
-
const refs = [];
|
|
99
|
-
for (let i = 0; i < selectedItemLength; i++) {
|
|
100
|
-
refs[i] = React.createRef();
|
|
101
|
-
}
|
|
102
|
-
return refs;
|
|
103
|
-
}, [selectedItemLength]);
|
|
104
97
|
const inputRef = React.useRef(null);
|
|
105
|
-
const [focusedIndex, setFocusedIndex] = React.useState(null);
|
|
106
98
|
// eslint-disable-next-line local-rules/best-practice-for-use-latest
|
|
107
99
|
const latestForListBox = hooks_useLatest.useLatest({
|
|
108
100
|
onDelete,
|
|
@@ -171,43 +163,48 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
171
163
|
isComposing,
|
|
172
164
|
isInputEmpty,
|
|
173
165
|
selectedItems,
|
|
174
|
-
deletionButtonRefs,
|
|
175
|
-
focusedIndex,
|
|
176
|
-
selectedItemLength,
|
|
177
166
|
setInputValueIfUncontrolled,
|
|
178
167
|
handleKeyDownListBox,
|
|
179
168
|
});
|
|
180
169
|
const functions = React.useMemo(() => {
|
|
181
|
-
const resetDeletionButtonFocus = () => {
|
|
182
|
-
setFocusedIndex(null);
|
|
183
|
-
};
|
|
184
170
|
const handleDelete = listBoxFunctions.handleDelete;
|
|
171
|
+
const getDeletionButtons = () => {
|
|
172
|
+
if (triggerRef.current) {
|
|
173
|
+
const buttons = triggerRef.current.querySelectorAll(DELETE_BUTTON_CLASSNAME);
|
|
174
|
+
if (buttons.length > 0) {
|
|
175
|
+
const actualButtons = Array.from(buttons);
|
|
176
|
+
return {
|
|
177
|
+
buttons: actualButtons,
|
|
178
|
+
currentIndex: actualButtons.indexOf(document.activeElement),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
};
|
|
185
184
|
const focusPrevDeletionButton = () => {
|
|
186
|
-
|
|
185
|
+
const result = getDeletionButtons();
|
|
186
|
+
if (!result)
|
|
187
187
|
return;
|
|
188
|
-
}
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
latest.deletionButtonRefs[nextIndex].current?.focus();
|
|
192
|
-
setFocusedIndex(nextIndex);
|
|
188
|
+
const { buttons, currentIndex } = result;
|
|
189
|
+
if (currentIndex !== -1) {
|
|
190
|
+
buttons[Math.max(currentIndex - 1, 0)].focus();
|
|
193
191
|
}
|
|
194
192
|
else if (inputRef.current?.selectionStart === 0) {
|
|
195
|
-
|
|
196
|
-
latest.deletionButtonRefs[nextIndex].current?.focus();
|
|
197
|
-
setFocusedIndex(nextIndex);
|
|
193
|
+
buttons[buttons.length - 1].focus();
|
|
198
194
|
}
|
|
199
195
|
};
|
|
200
196
|
const focusNextDeletionButton = () => {
|
|
201
|
-
|
|
197
|
+
const result = getDeletionButtons();
|
|
198
|
+
if (!result)
|
|
202
199
|
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
200
|
+
const { buttons, currentIndex } = result;
|
|
201
|
+
if (currentIndex === -1)
|
|
202
|
+
return;
|
|
203
|
+
const nextIndex = currentIndex + 1;
|
|
204
|
+
if (nextIndex < buttons.length) {
|
|
205
|
+
buttons[nextIndex].focus();
|
|
208
206
|
}
|
|
209
207
|
else {
|
|
210
|
-
setFocusedIndex(null);
|
|
211
208
|
// キー入力が input に影響しないようにフォーカスタイミングを遅らせる
|
|
212
209
|
setTimeout(() => {
|
|
213
210
|
inputRef.current?.focus();
|
|
@@ -222,7 +219,6 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
222
219
|
if (latest.isExpanded) {
|
|
223
220
|
latest.onBlur?.();
|
|
224
221
|
setIsExpanded(false);
|
|
225
|
-
resetDeletionButtonFocus();
|
|
226
222
|
}
|
|
227
223
|
};
|
|
228
224
|
return {
|
|
@@ -264,14 +260,13 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
264
260
|
else {
|
|
265
261
|
e.stopPropagation();
|
|
266
262
|
inputRef.current?.focus();
|
|
267
|
-
resetDeletionButtonFocus();
|
|
268
263
|
}
|
|
269
264
|
latest.handleKeyDownListBox(e);
|
|
270
265
|
},
|
|
271
266
|
handleDelegateClick: (e) => {
|
|
272
267
|
if (!latest.disabled &&
|
|
273
268
|
!latest.isExpanded &&
|
|
274
|
-
!
|
|
269
|
+
!libs_delegate.findDelegateTarget(e, DELETE_BUTTON_CLASSNAME)) {
|
|
275
270
|
focus();
|
|
276
271
|
}
|
|
277
272
|
},
|
|
@@ -281,7 +276,6 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
281
276
|
latest.setInputValueIfUncontrolled(e.currentTarget.value);
|
|
282
277
|
},
|
|
283
278
|
handleFocusInput: () => {
|
|
284
|
-
resetDeletionButtonFocus();
|
|
285
279
|
if (!latest.isExpanded) {
|
|
286
280
|
focus();
|
|
287
281
|
}
|
|
@@ -343,7 +337,7 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
343
337
|
jsxRuntime.jsxs("div", { ref: triggerRef, role: "group", onClick: functions.handleDelegateClick, onKeyDown: functions.handleDelegateKeyDown, onKeyPress: functions.handleDelegateKeyPress, className: classNames.wrapper, style: {
|
|
344
338
|
...style,
|
|
345
339
|
width: typeof width === 'number' ? `${width}px` : width,
|
|
346
|
-
}, children: [jsxRuntime.jsxs(components_Scroller_Scroller.Scroller, { className: classNames.inputArea, children: [jsxRuntime.jsx("ul", { id: selectedListId, "aria-label": localized.selectedListAriaLabel, className: classNames.selectedList, children: selectedItems.map((selectedItem
|
|
340
|
+
}, children: [jsxRuntime.jsxs(components_Scroller_Scroller.Scroller, { className: classNames.inputArea, children: [jsxRuntime.jsx("ul", { id: selectedListId, "aria-label": localized.selectedListAriaLabel, className: classNames.selectedList, children: selectedItems.map((selectedItem) => (jsxRuntime.jsx("li", { children: jsxRuntime.jsx(components_Combobox_MultiCombobox_MultiSelectedItem.MultiSelectedItem, { item: selectedItem, disabled: disabled, handleDelete: functions.handleDelete, enableEllipsis: selectedItemEllipsis }) }, `${selectedItem.label}-${index$1(selectedItem.value)}`))) }), jsxRuntime.jsx("div", { className: classNames.inputWrapper, children: jsxRuntime.jsx("input", { ...rest, "data-smarthr-ui-input": "true", type: "text", name: name, value: inputValue, disabled: disabled, required: required && selectedItems.length === 0, ref: inputRef, onChange: functions.handleChangeInput, onFocus: functions.handleFocusInput, onCompositionStart: functions.handleCompositionStart, onCompositionEnd: functions.handleCompositionEnd, onKeyDown: functions.handleKeyDownInput, autoComplete: autoComplete ?? 'off', tabIndex: 0, role: "combobox", "aria-activedescendant": activeOption?.id, "aria-controls": `${listBoxId} ${selectedListId}`, "aria-haspopup": "listbox", "aria-expanded": isExpanded, "aria-invalid": error || undefined, "aria-disabled": disabled, "aria-autocomplete": "list", className: classNames.input }) }), selectedItems.length === 0 && placeholder && !isExpanded && (jsxRuntime.jsx("p", { className: classNames.placeholder, children: placeholder }))] }), jsxRuntime.jsx(MemoizedCaretDown, { disabled: disabled, isExpanded: isExpanded, classNames: classNames }), jsxRuntime.jsx(components_Combobox_useListbox.ListBox, { ...listBoxProps })] }));
|
|
347
341
|
};
|
|
348
342
|
const MultiCombobox = libs_util.genericsForwardRef(ActualMultiCombobox);
|
|
349
343
|
const MemoizedCaretDown = React.memo(({ disabled, isExpanded, classNames }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiCombobox.cjs","sources":["../../../../src/components/Combobox/MultiCombobox/MultiCombobox.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 createRef,\n memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useOuterClick } from '../../../hooks/useOuterClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useLocalize } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { FaCaretDownIcon } from '../../Icon'\nimport { Scroller } from '../../Scroller'\nimport { areItemsEqual } from '../helper'\nimport { ListBox, useListbox } from '../useListbox'\nimport { useMultiOptions } from '../useOptions'\n\nimport { MultiSelectedItem } from './MultiSelectedItem'\n\nimport type { ComboboxItem, BaseProps as ComboboxProps } from '../types'\n\ntype BaseProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテムのリスト\n */\n selectedItems: Array<ComboboxItem<T> & { deletable?: boolean }>\n /**\n * 選択されているアイテムのラベルを省略表示するかどうか\n */\n selectedItemEllipsis?: boolean\n /**\n * テキストボックスの `value` 属性の値。\n * `onChangeInput` と併せて設定することで、テキストボックスの挙動が制御可能になる。\n */\n inputValue?: string\n /**\n * 選択されているアイテムの削除ボタンがクリックされた時に発火するコールバック関数\n */\n onDelete?: (item: ComboboxItem<T>) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItems: Array<ComboboxItem<T>>) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * アイテムが選択されたときに選択済みかどうかを判定するコールバック関数/\n */\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = BaseProps<T> & Omit<ComponentPropsWithoutRef<'input'>, keyof BaseProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst preventDefaultWithPressEnter = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n }\n}\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_LEFT_KEY_REGEX = /^(Arrow)?Left$/\nconst ARROW_RIGHT_KEY_REGEX = /^(Arrow)?Right/\nconst ARROW_UP_AND_DOWN_KEY_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: [\n 'smarthr-ui-MultiCombobox',\n 'shr-box-border shr-inline-flex shr-min-w-[15em] shr-rounded-m shr-border shr-border-solid shr-px-0.5 shr-py-0.25 shr-align-bottom',\n 'contrast-more:shr-border-high-contrast',\n 'has-[[role=combobox][aria-expanded=true]]:shr-focus-indicator',\n 'shr-cursor-text shr-border-default shr-bg-white',\n 'has-[[role=combobox]:disabled]:shr-cursor-not-allowed',\n 'has-[[role=combobox]:disabled]:shr-border-default/50',\n 'has-[[role=combobox]:disabled]:shr-bg-white-darken',\n 'has-[[role=combobox]:disabled]:shr-text-disabled',\n // HINT: disabled と詳細度が同じ [0,2,0] のため、CSS ソース順でこちらを後に置くことで error 時の border 色を優先させる\n 'has-[[role=combobox][aria-invalid]]:shr-border-danger',\n ],\n inputArea: 'shr-flex shr-flex-1 shr-flex-wrap shr-gap-0.5',\n selectedList:\n 'smarthr-ui-MultiCombobox-selectedList shr-contents shr-list-none [&_li]:shr-min-w-0',\n inputWrapper: [\n 'shr-flex shr-flex-1 shr-items-center',\n 'has-[[role=combobox][aria-expanded=false]]:shr-pointer-events-none',\n 'has-[[role=combobox][aria-expanded=false]]:shr-absolute',\n 'has-[[role=combobox][aria-expanded=false]]:shr-opacity-0',\n ],\n input: [\n 'smarthr-ui-MultiCombobox-input',\n 'shr-w-full shr-min-w-[5em] shr-border-none shr-text-base shr-text-black shr-outline-none shr-outline-0',\n 'disabled:shr-hidden',\n ],\n placeholderEl: 'smarthr-ui-MultiCombobox-placeholder shr-my-0 shr-self-center',\n suffixWrapper: [\n 'shr-relative -shr-me-0.5 shr-ms-0.5 shr-p-0.5',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n suffixIcon: 'shr-block',\n },\n})\n\nconst ActualMultiCombobox = <T,>(\n {\n items,\n selectedItems,\n name,\n disabled = false,\n required = false,\n error = false,\n creatable = false,\n placeholder = '',\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n selectedItemEllipsis,\n width,\n dropdownWidth = 'auto',\n inputValue: controlledInputValue,\n className,\n onChange,\n onChangeInput,\n onAdd,\n onDelete,\n onSelect,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n isItemSelected,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const triggerRef = useRef<HTMLDivElement>(null)\n const [isExpanded, setIsExpanded] = useState(false)\n const [highlighted, setHighlighted] = useState(false)\n const [uncontrolledInputValue, setUncontrolledInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n\n const selectedListId = useId()\n\n const isInputControlled = controlledInputValue !== undefined\n const inputValue = isInputControlled ? controlledInputValue : uncontrolledInputValue\n const setInputValueIfUncontrolled = isInputControlled ? NOOP : setUncontrolledInputValue\n const isInputEmpty = !inputValue\n\n const { options } = useMultiOptions({\n items,\n selected: selectedItems,\n creatable,\n inputValue,\n isItemSelected,\n })\n const selectedItemLength = selectedItems.length\n\n // TODO: 完全にcreateRefを作り直すのではなく、差分更新させたい\n const deletionButtonRefs = useMemo(() => {\n const refs: Array<ReturnType<typeof createRef<HTMLButtonElement>>> = []\n\n for (let i = 0; i < selectedItemLength; i++) {\n refs[i] = createRef<HTMLButtonElement>()\n }\n\n return refs\n }, [selectedItemLength])\n const inputRef = useRef<HTMLInputElement>(null)\n\n const [focusedIndex, setFocusedIndex] = useState<number | null>(null)\n\n // eslint-disable-next-line local-rules/best-practice-for-use-latest\n const latestForListBox = useLatest({\n onDelete,\n onChangeSelected,\n onSelect,\n onChangeInput,\n selectedItems,\n })\n\n const listBoxFunctions = useMemo(() => {\n const handleDelete = (item: ComboboxItem<T>) => {\n const handlers: Array<(deletingItem: ComboboxItem<T>) => void> = []\n\n if (latestForListBox.onDelete) {\n handlers.push((deletingItem: ComboboxItem<T>) => latestForListBox.onDelete!(deletingItem))\n }\n if (latestForListBox.onChangeSelected) {\n handlers.push((deletingItem: ComboboxItem<T>) =>\n latestForListBox.onChangeSelected!(\n latestForListBox.selectedItems.filter(\n (selected) => !areItemsEqual(selected, deletingItem),\n ),\n ),\n )\n }\n\n if (handlers.length > 0) {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n handlers.forEach((h) => h(item))\n })\n }\n }\n\n return {\n handleDelete,\n handleSelect: (selected: ComboboxItem<T>) => {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n const matchedSelectedItem = latestForListBox.selectedItems.find((item) =>\n areItemsEqual(item, selected),\n )\n\n if (matchedSelectedItem === undefined) {\n latestForListBox.onSelect?.(selected)\n latestForListBox.onChangeSelected?.(latestForListBox.selectedItems.concat(selected))\n\n // 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n latestForListBox.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n } else if (matchedSelectedItem.deletable !== false) {\n handleDelete(selected)\n }\n })\n },\n }\n }, [latestForListBox])\n\n const { listBoxProps, activeOption, handleKeyDownListBox, listBoxId, listBoxRef } = useListbox({\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n onSelect: listBoxFunctions.handleSelect,\n isExpanded,\n isLoading,\n triggerRef,\n noResultText,\n })\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onFocus,\n onBlur,\n onKeyPress,\n disabled,\n isExpanded,\n highlighted,\n isComposing,\n isInputEmpty,\n selectedItems,\n deletionButtonRefs,\n focusedIndex,\n selectedItemLength,\n setInputValueIfUncontrolled,\n handleKeyDownListBox,\n })\n\n const functions = useMemo(() => {\n const resetDeletionButtonFocus = () => {\n setFocusedIndex(null)\n }\n\n const handleDelete = listBoxFunctions.handleDelete\n\n const focusPrevDeletionButton = () => {\n if (latest.selectedItemLength === 0) {\n return\n }\n\n if (latest.focusedIndex !== null) {\n const nextIndex = Math.max(latest.focusedIndex - 1, 0)\n\n latest.deletionButtonRefs[nextIndex].current?.focus()\n setFocusedIndex(nextIndex)\n } else if (inputRef.current?.selectionStart === 0) {\n const nextIndex = latest.deletionButtonRefs.length - 1\n\n latest.deletionButtonRefs[nextIndex].current?.focus()\n setFocusedIndex(nextIndex)\n }\n }\n\n const focusNextDeletionButton = () => {\n if (latest.deletionButtonRefs.length === 0 || latest.focusedIndex === null) {\n return\n }\n\n const nextIndex = latest.focusedIndex + 1\n\n if (nextIndex < latest.deletionButtonRefs.length) {\n latest.deletionButtonRefs[nextIndex].current?.focus()\n setFocusedIndex(nextIndex)\n } else {\n setFocusedIndex(null)\n // キー入力が input に影響しないようにフォーカスタイミングを遅らせる\n setTimeout(() => {\n inputRef.current?.focus()\n })\n }\n }\n\n const focus = () => {\n latest.onFocus?.()\n setIsExpanded(true)\n }\n\n const blur = () => {\n if (latest.isExpanded) {\n latest.onBlur?.()\n setIsExpanded(false)\n resetDeletionButtonFocus()\n }\n }\n\n return {\n handleDelete,\n blur,\n handleDelegateKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {\n if (latest.isComposing) return\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n blur()\n } else if (e.key === 'Tab') {\n if (latest.isExpanded) {\n // フォーカスがコンポーネントを抜けるように先に input をフォーカスしておく\n inputRef.current?.focus()\n }\n\n blur()\n } else if (ARROW_LEFT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusPrevDeletionButton()\n } else if (ARROW_RIGHT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusNextDeletionButton()\n } else if (\n e.key === 'Backspace' &&\n latest.isInputEmpty &&\n latest.selectedItems.length > 0 &&\n latest.selectedItems[latest.selectedItems.length - 1].deletable !== false\n ) {\n e.preventDefault()\n e.stopPropagation()\n\n const lastItem = latest.selectedItems[latest.selectedItems.length - 1]\n\n handleDelete(lastItem)\n setHighlighted(true)\n latest.setInputValueIfUncontrolled(innerText(lastItem.label))\n } else {\n e.stopPropagation()\n inputRef.current?.focus()\n resetDeletionButtonFocus()\n }\n\n latest.handleKeyDownListBox(e)\n },\n handleDelegateClick: (e: MouseEvent<HTMLElement>) => {\n if (\n !latest.disabled &&\n !latest.isExpanded &&\n !(e.target as HTMLElement).closest('.smarthr-ui-MultiCombobox-deleteButton')\n ) {\n focus()\n }\n },\n handleChangeInput: (e: ChangeEvent<HTMLInputElement>) => {\n const handlers = [latest.onChange, latest.onChangeInput].filter((h) => !!h)\n\n handlers.forEach((h) => h(e))\n latest.setInputValueIfUncontrolled(e.currentTarget.value)\n },\n handleFocusInput: () => {\n resetDeletionButtonFocus()\n\n if (!latest.isExpanded) {\n focus()\n }\n },\n handleCompositionStart: () => setIsComposing(true),\n handleCompositionEnd: () => setIsComposing(false),\n handleKeyDownInput: (e: KeyboardEvent<HTMLInputElement>) => {\n if (ARROW_UP_AND_DOWN_KEY_REGEX.test(e.key)) {\n // 上下キー入力はリストボックスの activeDescendant の移動に用いるため、input 内では作用させない\n e.preventDefault()\n }\n },\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n handleDelegateKeyPress: (e: KeyboardEvent<HTMLInputElement>) => {\n preventDefaultWithPressEnter(e)\n latest.onKeyPress?.(e)\n },\n }\n }, [listBoxFunctions, latest])\n\n useOuterClick(\n useMemo(() => [triggerRef, listBoxRef], [listBoxRef]),\n functions.blur,\n )\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n useEffect(() => {\n if (latest.highlighted) {\n setHighlighted(false)\n inputRef.current?.select()\n } else {\n setInputValueIfUncontrolled('')\n }\n }, [selectedItems, setInputValueIfUncontrolled, inputRef, latest])\n\n useEffect(() => {\n if (isExpanded) {\n inputRef.current?.focus()\n }\n }, [isExpanded, selectedItems, isInputControlled, inputRef])\n\n const classNames = useMemo(() => {\n const {\n wrapper,\n inputArea,\n selectedList,\n inputWrapper,\n input,\n placeholderEl,\n suffixWrapper,\n suffixIcon,\n } = classNameGenerator()\n\n return {\n wrapper: wrapper({ className }),\n inputArea: inputArea(),\n selectedList: selectedList(),\n inputWrapper: inputWrapper(),\n input: input(),\n placeholder: placeholderEl(),\n suffixWrapper: suffixWrapper(),\n suffixIcon: suffixIcon(),\n }\n }, [className])\n\n const localized = useLocalize({\n selectedListAriaLabel: {\n id: 'smarthr-ui/MultiCombobox/selectedListAriaLabel',\n defaultText: '選択済みアイテム',\n },\n })\n\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n ref={triggerRef}\n role=\"group\"\n onClick={functions.handleDelegateClick}\n onKeyDown={functions.handleDelegateKeyDown}\n onKeyPress={functions.handleDelegateKeyPress}\n className={classNames.wrapper}\n style={{\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <Scroller className={classNames.inputArea}>\n <ul\n id={selectedListId}\n aria-label={localized.selectedListAriaLabel}\n className={classNames.selectedList}\n >\n {selectedItems.map((selectedItem, i) => (\n <li key={`${selectedItem.label}-${innerText(selectedItem.value)}`}>\n <MultiSelectedItem\n item={selectedItem}\n disabled={disabled}\n handleDelete={functions.handleDelete}\n enableEllipsis={selectedItemEllipsis}\n buttonRef={deletionButtonRefs[i]}\n />\n </li>\n ))}\n </ul>\n\n <div className={classNames.inputWrapper}>\n <input\n {...rest}\n data-smarthr-ui-input=\"true\"\n type=\"text\"\n name={name}\n value={inputValue}\n disabled={disabled}\n required={required && selectedItems.length === 0}\n ref={inputRef}\n onChange={functions.handleChangeInput}\n onFocus={functions.handleFocusInput}\n onCompositionStart={functions.handleCompositionStart}\n onCompositionEnd={functions.handleCompositionEnd}\n onKeyDown={functions.handleKeyDownInput}\n autoComplete={autoComplete ?? 'off'}\n tabIndex={0}\n role=\"combobox\"\n aria-activedescendant={activeOption?.id}\n aria-controls={`${listBoxId} ${selectedListId}`}\n aria-haspopup=\"listbox\"\n aria-expanded={isExpanded}\n aria-invalid={error || undefined}\n aria-disabled={disabled}\n aria-autocomplete=\"list\"\n className={classNames.input}\n />\n </div>\n\n {selectedItems.length === 0 && placeholder && !isExpanded && (\n <p className={classNames.placeholder}>{placeholder}</p>\n )}\n </Scroller>\n\n <MemoizedCaretDown disabled={disabled} isExpanded={isExpanded} classNames={classNames} />\n\n <ListBox {...listBoxProps} />\n </div>\n )\n}\n\nexport const MultiCombobox = genericsForwardRef(ActualMultiCombobox)\n\nconst MemoizedCaretDown = memo<{\n disabled: boolean\n isExpanded: boolean\n classNames: {\n suffixWrapper: string\n suffixIcon: string\n }\n}>(({ disabled, isExpanded, classNames }) => {\n const theme = useTheme()\n const caretIconColor = isExpanded\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n <div className={classNames.suffixWrapper}>\n <FaCaretDownIcon color={caretIconColor} className={classNames.suffixIcon} />\n </div>\n )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AAEA;AACE;;;AAGF;AAEA;AACA;AACA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;;;;;;;;;;;;AAYC;AACD;AACA;AAEA;;;;;AAKC;AACD;;;;AAIC;AACD;AACA;;;AAGC;AACD;AACD;AACF;AAED;AAkCE;;;;;AAMA;AAEA;;;AAGA;AAEA;;AAEE;;;;AAID;AACD;;AAGA;;AAGE;AACE;;AAGF;AACF;AACA;;;;;;;;;AAWC;AAED;AACE;;AAGE;AACE;;AAEF;AACE;;AASF;;;;AAII;AACF;;AAEJ;;;AAIE;;;;;AAQI;AACE;AACA;;AAGA;;AACK;;;AAGT;;;AAGN;AAEA;;;;;;;;;;AAUC;;;;;;;;;;;;;;;;;;AAmBA;AAED;;;AAGE;AAEA;;AAGE;;;AAIA;AACE;;;;;;;;;AAUJ;;AAGE;;;AAIA;;;;;;;;;AASI;AACF;;AAEJ;;AAGE;;AAEF;;AAGE;AACE;;AAEA;;AAEJ;;;;AAKE;;;;;AAKI;;AACK;AACL;;AAEE;;AAGF;;;;AAGA;;;;AAGA;;AACK;AAEL;AACA;AACA;;;AAKA;;;;;;;AAOA;AACA;;AAGF;;AAEF;;;;AAMI;;;AAGJ;;AAGE;;;;AAIA;AAEA;AACE;;;AAGJ;AACA;AACA;;;;;;;;;AASA;;AAEE;;;AAGN;;;;AAUE;;AAEE;;;;;;;;AAQA;;;AAIJ;;;AAaI;;;;;;;;;AASJ;;AAGE;AACE;AACA;AACD;AACF;;;AAIC;AAQI;AACA;;AA6DR;;AAIA;AAQE;;AAEE;AACA;AACE;AACA;;AAON;;"}
|
|
1
|
+
{"version":3,"file":"MultiCombobox.cjs","sources":["../../../../src/components/Combobox/MultiCombobox/MultiCombobox.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 memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useOuterClick } from '../../../hooks/useOuterClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useLocalize } from '../../../intl'\nimport { findDelegateTarget } from '../../../libs/delegate'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { FaCaretDownIcon } from '../../Icon'\nimport { Scroller } from '../../Scroller'\nimport { areItemsEqual } from '../helper'\nimport { ListBox, useListbox } from '../useListbox'\nimport { useMultiOptions } from '../useOptions'\n\nimport { DELETE_BUTTON_SELECTOR, MultiSelectedItem } from './MultiSelectedItem'\n\nimport type { ComboboxItem, BaseProps as ComboboxProps } from '../types'\n\ntype BaseProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテムのリスト\n */\n selectedItems: Array<ComboboxItem<T> & { deletable?: boolean }>\n /**\n * 選択されているアイテムのラベルを省略表示するかどうか\n */\n selectedItemEllipsis?: boolean\n /**\n * テキストボックスの `value` 属性の値。\n * `onChangeInput` と併せて設定することで、テキストボックスの挙動が制御可能になる。\n */\n inputValue?: string\n /**\n * 選択されているアイテムの削除ボタンがクリックされた時に発火するコールバック関数\n */\n onDelete?: (item: ComboboxItem<T>) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItems: Array<ComboboxItem<T>>) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * アイテムが選択されたときに選択済みかどうかを判定するコールバック関数/\n */\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = BaseProps<T> & Omit<ComponentPropsWithoutRef<'input'>, keyof BaseProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst preventDefaultWithPressEnter = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n }\n}\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_LEFT_KEY_REGEX = /^(Arrow)?Left$/\nconst ARROW_RIGHT_KEY_REGEX = /^(Arrow)?Right/\nconst ARROW_UP_AND_DOWN_KEY_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n currentTarget: { value: '' },\n target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst DELETE_BUTTON_CLASSNAME = `.${DELETE_BUTTON_SELECTOR}`\n\nconst classNameGenerator = tv({\n slots: {\n wrapper: [\n 'smarthr-ui-MultiCombobox',\n 'shr-box-border shr-inline-flex shr-min-w-[15em] shr-rounded-m shr-border shr-border-solid shr-px-0.5 shr-py-0.25 shr-align-bottom',\n 'contrast-more:shr-border-high-contrast',\n 'has-[[role=combobox][aria-expanded=true]]:shr-focus-indicator',\n 'shr-cursor-text shr-border-default shr-bg-white',\n 'has-[[role=combobox]:disabled]:shr-cursor-not-allowed',\n 'has-[[role=combobox]:disabled]:shr-border-default/50',\n 'has-[[role=combobox]:disabled]:shr-bg-white-darken',\n 'has-[[role=combobox]:disabled]:shr-text-disabled',\n // HINT: disabled と詳細度が同じ [0,2,0] のため、CSS ソース順でこちらを後に置くことで error 時の border 色を優先させる\n 'has-[[role=combobox][aria-invalid]]:shr-border-danger',\n ],\n inputArea: 'shr-flex shr-flex-1 shr-flex-wrap shr-gap-0.5',\n selectedList:\n 'smarthr-ui-MultiCombobox-selectedList shr-contents shr-list-none [&_li]:shr-min-w-0',\n inputWrapper: [\n 'shr-flex shr-flex-1 shr-items-center',\n 'has-[[role=combobox][aria-expanded=false]]:shr-pointer-events-none',\n 'has-[[role=combobox][aria-expanded=false]]:shr-absolute',\n 'has-[[role=combobox][aria-expanded=false]]:shr-opacity-0',\n ],\n input: [\n 'smarthr-ui-MultiCombobox-input',\n 'shr-w-full shr-min-w-[5em] shr-border-none shr-text-base shr-text-black shr-outline-none shr-outline-0',\n 'disabled:shr-hidden',\n ],\n placeholderEl: 'smarthr-ui-MultiCombobox-placeholder shr-my-0 shr-self-center',\n suffixWrapper: [\n 'shr-relative -shr-me-0.5 shr-ms-0.5 shr-p-0.5',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n suffixIcon: 'shr-block',\n },\n})\n\nconst ActualMultiCombobox = <T,>(\n {\n items,\n selectedItems,\n name,\n disabled = false,\n required = false,\n error = false,\n creatable = false,\n placeholder = '',\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n selectedItemEllipsis,\n width,\n dropdownWidth = 'auto',\n inputValue: controlledInputValue,\n className,\n onChange,\n onChangeInput,\n onAdd,\n onDelete,\n onSelect,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n isItemSelected,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const triggerRef = useRef<HTMLDivElement>(null)\n const [isExpanded, setIsExpanded] = useState(false)\n const [highlighted, setHighlighted] = useState(false)\n const [uncontrolledInputValue, setUncontrolledInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n\n const selectedListId = useId()\n\n const isInputControlled = controlledInputValue !== undefined\n const inputValue = isInputControlled ? controlledInputValue : uncontrolledInputValue\n const setInputValueIfUncontrolled = isInputControlled ? NOOP : setUncontrolledInputValue\n const isInputEmpty = !inputValue\n\n const { options } = useMultiOptions({\n items,\n selected: selectedItems,\n creatable,\n inputValue,\n isItemSelected,\n })\n const inputRef = useRef<HTMLInputElement>(null)\n\n // eslint-disable-next-line local-rules/best-practice-for-use-latest\n const latestForListBox = useLatest({\n onDelete,\n onChangeSelected,\n onSelect,\n onChangeInput,\n selectedItems,\n })\n\n const listBoxFunctions = useMemo(() => {\n const handleDelete = (item: ComboboxItem<T>) => {\n const handlers: Array<(deletingItem: ComboboxItem<T>) => void> = []\n\n if (latestForListBox.onDelete) {\n handlers.push((deletingItem: ComboboxItem<T>) => latestForListBox.onDelete!(deletingItem))\n }\n if (latestForListBox.onChangeSelected) {\n handlers.push((deletingItem: ComboboxItem<T>) =>\n latestForListBox.onChangeSelected!(\n latestForListBox.selectedItems.filter(\n (selected) => !areItemsEqual(selected, deletingItem),\n ),\n ),\n )\n }\n\n if (handlers.length > 0) {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n handlers.forEach((h) => h(item))\n })\n }\n }\n\n return {\n handleDelete,\n handleSelect: (selected: ComboboxItem<T>) => {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n const matchedSelectedItem = latestForListBox.selectedItems.find((item) =>\n areItemsEqual(item, selected),\n )\n\n if (matchedSelectedItem === undefined) {\n latestForListBox.onSelect?.(selected)\n latestForListBox.onChangeSelected?.(latestForListBox.selectedItems.concat(selected))\n\n // 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n latestForListBox.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n } else if (matchedSelectedItem.deletable !== false) {\n handleDelete(selected)\n }\n })\n },\n }\n }, [latestForListBox])\n\n const { listBoxProps, activeOption, handleKeyDownListBox, listBoxId, listBoxRef } = useListbox({\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n onSelect: listBoxFunctions.handleSelect,\n isExpanded,\n isLoading,\n triggerRef,\n noResultText,\n })\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onFocus,\n onBlur,\n onKeyPress,\n disabled,\n isExpanded,\n highlighted,\n isComposing,\n isInputEmpty,\n selectedItems,\n setInputValueIfUncontrolled,\n handleKeyDownListBox,\n })\n\n const functions = useMemo(() => {\n const handleDelete = listBoxFunctions.handleDelete\n\n const getDeletionButtons = () => {\n if (triggerRef.current) {\n const buttons =\n triggerRef.current.querySelectorAll<HTMLButtonElement>(DELETE_BUTTON_CLASSNAME)\n\n if (buttons.length > 0) {\n const actualButtons = Array.from(buttons)\n\n return {\n buttons: actualButtons,\n currentIndex: actualButtons.indexOf(document.activeElement as HTMLButtonElement),\n }\n }\n }\n\n return null\n }\n\n const focusPrevDeletionButton = () => {\n const result = getDeletionButtons()\n\n if (!result) return\n\n const { buttons, currentIndex } = result\n\n if (currentIndex !== -1) {\n buttons[Math.max(currentIndex - 1, 0)].focus()\n } else if (inputRef.current?.selectionStart === 0) {\n buttons[buttons.length - 1].focus()\n }\n }\n\n const focusNextDeletionButton = () => {\n const result = getDeletionButtons()\n\n if (!result) return\n\n const { buttons, currentIndex } = result\n\n if (currentIndex === -1) return\n\n const nextIndex = currentIndex + 1\n\n if (nextIndex < buttons.length) {\n buttons[nextIndex].focus()\n } else {\n // キー入力が input に影響しないようにフォーカスタイミングを遅らせる\n setTimeout(() => {\n inputRef.current?.focus()\n })\n }\n }\n\n const focus = () => {\n latest.onFocus?.()\n setIsExpanded(true)\n }\n\n const blur = () => {\n if (latest.isExpanded) {\n latest.onBlur?.()\n setIsExpanded(false)\n }\n }\n\n return {\n handleDelete,\n blur,\n handleDelegateKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {\n if (latest.isComposing) return\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n blur()\n } else if (e.key === 'Tab') {\n if (latest.isExpanded) {\n // フォーカスがコンポーネントを抜けるように先に input をフォーカスしておく\n inputRef.current?.focus()\n }\n\n blur()\n } else if (ARROW_LEFT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusPrevDeletionButton()\n } else if (ARROW_RIGHT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusNextDeletionButton()\n } else if (\n e.key === 'Backspace' &&\n latest.isInputEmpty &&\n latest.selectedItems.length > 0 &&\n latest.selectedItems[latest.selectedItems.length - 1].deletable !== false\n ) {\n e.preventDefault()\n e.stopPropagation()\n\n const lastItem = latest.selectedItems[latest.selectedItems.length - 1]\n\n handleDelete(lastItem)\n setHighlighted(true)\n latest.setInputValueIfUncontrolled(innerText(lastItem.label))\n } else {\n e.stopPropagation()\n inputRef.current?.focus()\n }\n\n latest.handleKeyDownListBox(e)\n },\n handleDelegateClick: (e: MouseEvent<HTMLElement>) => {\n if (\n !latest.disabled &&\n !latest.isExpanded &&\n !findDelegateTarget(e, DELETE_BUTTON_CLASSNAME)\n ) {\n focus()\n }\n },\n handleChangeInput: (e: ChangeEvent<HTMLInputElement>) => {\n const handlers = [latest.onChange, latest.onChangeInput].filter((h) => !!h)\n\n handlers.forEach((h) => h(e))\n latest.setInputValueIfUncontrolled(e.currentTarget.value)\n },\n handleFocusInput: () => {\n if (!latest.isExpanded) {\n focus()\n }\n },\n handleCompositionStart: () => setIsComposing(true),\n handleCompositionEnd: () => setIsComposing(false),\n handleKeyDownInput: (e: KeyboardEvent<HTMLInputElement>) => {\n if (ARROW_UP_AND_DOWN_KEY_REGEX.test(e.key)) {\n // 上下キー入力はリストボックスの activeDescendant の移動に用いるため、input 内では作用させない\n e.preventDefault()\n }\n },\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n handleDelegateKeyPress: (e: KeyboardEvent<HTMLInputElement>) => {\n preventDefaultWithPressEnter(e)\n latest.onKeyPress?.(e)\n },\n }\n }, [listBoxFunctions, latest])\n\n useOuterClick(\n useMemo(() => [triggerRef, listBoxRef], [listBoxRef]),\n functions.blur,\n )\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n useEffect(() => {\n if (latest.highlighted) {\n setHighlighted(false)\n inputRef.current?.select()\n } else {\n setInputValueIfUncontrolled('')\n }\n }, [selectedItems, setInputValueIfUncontrolled, inputRef, latest])\n\n useEffect(() => {\n if (isExpanded) {\n inputRef.current?.focus()\n }\n }, [isExpanded, selectedItems, isInputControlled, inputRef])\n\n const classNames = useMemo(() => {\n const {\n wrapper,\n inputArea,\n selectedList,\n inputWrapper,\n input,\n placeholderEl,\n suffixWrapper,\n suffixIcon,\n } = classNameGenerator()\n\n return {\n wrapper: wrapper({ className }),\n inputArea: inputArea(),\n selectedList: selectedList(),\n inputWrapper: inputWrapper(),\n input: input(),\n placeholder: placeholderEl(),\n suffixWrapper: suffixWrapper(),\n suffixIcon: suffixIcon(),\n }\n }, [className])\n\n const localized = useLocalize({\n selectedListAriaLabel: {\n id: 'smarthr-ui/MultiCombobox/selectedListAriaLabel',\n defaultText: '選択済みアイテム',\n },\n })\n\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n ref={triggerRef}\n role=\"group\"\n onClick={functions.handleDelegateClick}\n onKeyDown={functions.handleDelegateKeyDown}\n onKeyPress={functions.handleDelegateKeyPress}\n className={classNames.wrapper}\n style={{\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <Scroller className={classNames.inputArea}>\n <ul\n id={selectedListId}\n aria-label={localized.selectedListAriaLabel}\n className={classNames.selectedList}\n >\n {selectedItems.map((selectedItem) => (\n <li key={`${selectedItem.label}-${innerText(selectedItem.value)}`}>\n <MultiSelectedItem\n item={selectedItem}\n disabled={disabled}\n handleDelete={functions.handleDelete}\n enableEllipsis={selectedItemEllipsis}\n />\n </li>\n ))}\n </ul>\n\n <div className={classNames.inputWrapper}>\n <input\n {...rest}\n data-smarthr-ui-input=\"true\"\n type=\"text\"\n name={name}\n value={inputValue}\n disabled={disabled}\n required={required && selectedItems.length === 0}\n ref={inputRef}\n onChange={functions.handleChangeInput}\n onFocus={functions.handleFocusInput}\n onCompositionStart={functions.handleCompositionStart}\n onCompositionEnd={functions.handleCompositionEnd}\n onKeyDown={functions.handleKeyDownInput}\n autoComplete={autoComplete ?? 'off'}\n tabIndex={0}\n role=\"combobox\"\n aria-activedescendant={activeOption?.id}\n aria-controls={`${listBoxId} ${selectedListId}`}\n aria-haspopup=\"listbox\"\n aria-expanded={isExpanded}\n aria-invalid={error || undefined}\n aria-disabled={disabled}\n aria-autocomplete=\"list\"\n className={classNames.input}\n />\n </div>\n\n {selectedItems.length === 0 && placeholder && !isExpanded && (\n <p className={classNames.placeholder}>{placeholder}</p>\n )}\n </Scroller>\n\n <MemoizedCaretDown disabled={disabled} isExpanded={isExpanded} classNames={classNames} />\n\n <ListBox {...listBoxProps} />\n </div>\n )\n}\n\nexport const MultiCombobox = genericsForwardRef(ActualMultiCombobox)\n\nconst MemoizedCaretDown = memo<{\n disabled: boolean\n isExpanded: boolean\n classNames: {\n suffixWrapper: string\n suffixIcon: string\n }\n}>(({ disabled, isExpanded, classNames }) => {\n const theme = useTheme()\n const caretIconColor = isExpanded\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n <div className={classNames.suffixWrapper}>\n <FaCaretDownIcon color={caretIconColor} className={classNames.suffixIcon} />\n </div>\n )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AAEA;AACE;;;AAGF;AAEA;AACA;AACA;AACA;AAEA;AACE;AACA;;AAGF;AAEA;AACE;AACE;;;;;;;;;;;;AAYC;AACD;AACA;AAEA;;;;;AAKC;AACD;;;;AAIC;AACD;AACA;;;AAGC;AACD;AACD;AACF;AAED;AAkCE;;;;;AAMA;AAEA;;;AAGA;AAEA;;AAEE;;;;AAID;AACD;;;;;;;;AASC;AAED;AACE;;AAGE;AACE;;AAEF;AACE;;AASF;;;;AAII;AACF;;AAEJ;;;AAIE;;;;;AAQI;AACE;AACA;;AAGA;;AACK;;;AAGT;;;AAGN;AAEA;;;;;;;;;;AAUC;;;;;;;;;;;;;;;AAgBA;AAED;AACE;;AAGE;;AAIE;;;AAII;;;;;AAMN;AACF;;AAGE;AAEA;;AAEA;AAEA;AACE;;;;;AAIJ;;AAGE;AAEA;;AAEA;;;AAIA;AAEA;AACE;;;;;AAIE;AACF;;AAEJ;;AAGE;;AAEF;;AAGE;AACE;;;AAGJ;;;;AAKE;;;;;AAKI;;AACK;AACL;;AAEE;;AAGF;;;;AAGA;;;;AAGA;;AACK;AAEL;AACA;AACA;;;AAKA;;;;;;;AAOA;;AAGF;;AAEF;;;AAII;AAEA;;;AAGJ;;AAGE;;;;AAIA;AACE;;;AAGJ;AACA;AACA;;;;;;;;;AASA;;AAEE;;;AAGN;;;;AAUE;;AAEE;;;;;;;;AAQA;;;AAIJ;;;AAaI;;;;;;;;;AASJ;;AAGE;AACE;AACA;AACD;AACF;;;AAIC;AAQI;AACA;;AA4DR;;AAIA;AAQE;;AAEE;AACA;AACE;AACA;;AAON;;"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { memo, useRef, useState, useId, useMemo,
|
|
3
|
+
import { memo, useRef, useState, useId, useMemo, useImperativeHandle, useEffect } from 'react';
|
|
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.46_@types_node@20.19.43_typescript@5.9.3__/vendor/tailwind-variants/dist/index.js';
|
|
6
6
|
import { useLatest } from '../../../hooks/useLatest.js';
|
|
@@ -12,6 +12,7 @@ import 'react-intl';
|
|
|
12
12
|
import '../../../intl/Localizer.js';
|
|
13
13
|
import '../../../_virtual/dayjs.min.js';
|
|
14
14
|
import '../../../intl/TimeFormatter.js';
|
|
15
|
+
import { findDelegateTarget } from '../../../libs/delegate.js';
|
|
15
16
|
import { genericsForwardRef } from '../../../libs/util.js';
|
|
16
17
|
import { FaCaretDownIcon } from '../../Icon/FaIcon.js';
|
|
17
18
|
import '../../Icon/generateIcon.js';
|
|
@@ -20,7 +21,7 @@ import { Scroller } from '../../Scroller/Scroller.js';
|
|
|
20
21
|
import { areItemsEqual } from '../helper.js';
|
|
21
22
|
import { useListbox, ListBox } from '../useListbox.js';
|
|
22
23
|
import { useMultiOptions } from '../useOptions.js';
|
|
23
|
-
import { MultiSelectedItem } from './MultiSelectedItem.js';
|
|
24
|
+
import { DELETE_BUTTON_SELECTOR, MultiSelectedItem } from './MultiSelectedItem.js';
|
|
24
25
|
|
|
25
26
|
const NOOP = () => undefined;
|
|
26
27
|
const preventDefaultWithPressEnter = (e) => {
|
|
@@ -36,6 +37,7 @@ const EMPTY_INPUT_CHANGE_EVENT = {
|
|
|
36
37
|
currentTarget: { value: '' },
|
|
37
38
|
target: { value: '' },
|
|
38
39
|
};
|
|
40
|
+
const DELETE_BUTTON_CLASSNAME = `.${DELETE_BUTTON_SELECTOR}`;
|
|
39
41
|
const classNameGenerator = ce({
|
|
40
42
|
slots: {
|
|
41
43
|
wrapper: [
|
|
@@ -90,17 +92,7 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
90
92
|
inputValue,
|
|
91
93
|
isItemSelected,
|
|
92
94
|
});
|
|
93
|
-
const selectedItemLength = selectedItems.length;
|
|
94
|
-
// TODO: 完全にcreateRefを作り直すのではなく、差分更新させたい
|
|
95
|
-
const deletionButtonRefs = useMemo(() => {
|
|
96
|
-
const refs = [];
|
|
97
|
-
for (let i = 0; i < selectedItemLength; i++) {
|
|
98
|
-
refs[i] = createRef();
|
|
99
|
-
}
|
|
100
|
-
return refs;
|
|
101
|
-
}, [selectedItemLength]);
|
|
102
95
|
const inputRef = useRef(null);
|
|
103
|
-
const [focusedIndex, setFocusedIndex] = useState(null);
|
|
104
96
|
// eslint-disable-next-line local-rules/best-practice-for-use-latest
|
|
105
97
|
const latestForListBox = useLatest({
|
|
106
98
|
onDelete,
|
|
@@ -169,43 +161,48 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
169
161
|
isComposing,
|
|
170
162
|
isInputEmpty,
|
|
171
163
|
selectedItems,
|
|
172
|
-
deletionButtonRefs,
|
|
173
|
-
focusedIndex,
|
|
174
|
-
selectedItemLength,
|
|
175
164
|
setInputValueIfUncontrolled,
|
|
176
165
|
handleKeyDownListBox,
|
|
177
166
|
});
|
|
178
167
|
const functions = useMemo(() => {
|
|
179
|
-
const resetDeletionButtonFocus = () => {
|
|
180
|
-
setFocusedIndex(null);
|
|
181
|
-
};
|
|
182
168
|
const handleDelete = listBoxFunctions.handleDelete;
|
|
169
|
+
const getDeletionButtons = () => {
|
|
170
|
+
if (triggerRef.current) {
|
|
171
|
+
const buttons = triggerRef.current.querySelectorAll(DELETE_BUTTON_CLASSNAME);
|
|
172
|
+
if (buttons.length > 0) {
|
|
173
|
+
const actualButtons = Array.from(buttons);
|
|
174
|
+
return {
|
|
175
|
+
buttons: actualButtons,
|
|
176
|
+
currentIndex: actualButtons.indexOf(document.activeElement),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
183
182
|
const focusPrevDeletionButton = () => {
|
|
184
|
-
|
|
183
|
+
const result = getDeletionButtons();
|
|
184
|
+
if (!result)
|
|
185
185
|
return;
|
|
186
|
-
}
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
latest.deletionButtonRefs[nextIndex].current?.focus();
|
|
190
|
-
setFocusedIndex(nextIndex);
|
|
186
|
+
const { buttons, currentIndex } = result;
|
|
187
|
+
if (currentIndex !== -1) {
|
|
188
|
+
buttons[Math.max(currentIndex - 1, 0)].focus();
|
|
191
189
|
}
|
|
192
190
|
else if (inputRef.current?.selectionStart === 0) {
|
|
193
|
-
|
|
194
|
-
latest.deletionButtonRefs[nextIndex].current?.focus();
|
|
195
|
-
setFocusedIndex(nextIndex);
|
|
191
|
+
buttons[buttons.length - 1].focus();
|
|
196
192
|
}
|
|
197
193
|
};
|
|
198
194
|
const focusNextDeletionButton = () => {
|
|
199
|
-
|
|
195
|
+
const result = getDeletionButtons();
|
|
196
|
+
if (!result)
|
|
200
197
|
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
198
|
+
const { buttons, currentIndex } = result;
|
|
199
|
+
if (currentIndex === -1)
|
|
200
|
+
return;
|
|
201
|
+
const nextIndex = currentIndex + 1;
|
|
202
|
+
if (nextIndex < buttons.length) {
|
|
203
|
+
buttons[nextIndex].focus();
|
|
206
204
|
}
|
|
207
205
|
else {
|
|
208
|
-
setFocusedIndex(null);
|
|
209
206
|
// キー入力が input に影響しないようにフォーカスタイミングを遅らせる
|
|
210
207
|
setTimeout(() => {
|
|
211
208
|
inputRef.current?.focus();
|
|
@@ -220,7 +217,6 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
220
217
|
if (latest.isExpanded) {
|
|
221
218
|
latest.onBlur?.();
|
|
222
219
|
setIsExpanded(false);
|
|
223
|
-
resetDeletionButtonFocus();
|
|
224
220
|
}
|
|
225
221
|
};
|
|
226
222
|
return {
|
|
@@ -262,14 +258,13 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
262
258
|
else {
|
|
263
259
|
e.stopPropagation();
|
|
264
260
|
inputRef.current?.focus();
|
|
265
|
-
resetDeletionButtonFocus();
|
|
266
261
|
}
|
|
267
262
|
latest.handleKeyDownListBox(e);
|
|
268
263
|
},
|
|
269
264
|
handleDelegateClick: (e) => {
|
|
270
265
|
if (!latest.disabled &&
|
|
271
266
|
!latest.isExpanded &&
|
|
272
|
-
!e
|
|
267
|
+
!findDelegateTarget(e, DELETE_BUTTON_CLASSNAME)) {
|
|
273
268
|
focus();
|
|
274
269
|
}
|
|
275
270
|
},
|
|
@@ -279,7 +274,6 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
279
274
|
latest.setInputValueIfUncontrolled(e.currentTarget.value);
|
|
280
275
|
},
|
|
281
276
|
handleFocusInput: () => {
|
|
282
|
-
resetDeletionButtonFocus();
|
|
283
277
|
if (!latest.isExpanded) {
|
|
284
278
|
focus();
|
|
285
279
|
}
|
|
@@ -341,7 +335,7 @@ const ActualMultiCombobox = ({ items, selectedItems, name, disabled = false, req
|
|
|
341
335
|
jsxs("div", { ref: triggerRef, role: "group", onClick: functions.handleDelegateClick, onKeyDown: functions.handleDelegateKeyDown, onKeyPress: functions.handleDelegateKeyPress, className: classNames.wrapper, style: {
|
|
342
336
|
...style,
|
|
343
337
|
width: typeof width === 'number' ? `${width}px` : width,
|
|
344
|
-
}, children: [jsxs(Scroller, { className: classNames.inputArea, children: [jsx("ul", { id: selectedListId, "aria-label": localized.selectedListAriaLabel, className: classNames.selectedList, children: selectedItems.map((selectedItem
|
|
338
|
+
}, children: [jsxs(Scroller, { className: classNames.inputArea, children: [jsx("ul", { id: selectedListId, "aria-label": localized.selectedListAriaLabel, className: classNames.selectedList, children: selectedItems.map((selectedItem) => (jsx("li", { children: jsx(MultiSelectedItem, { item: selectedItem, disabled: disabled, handleDelete: functions.handleDelete, enableEllipsis: selectedItemEllipsis }) }, `${selectedItem.label}-${innerText(selectedItem.value)}`))) }), jsx("div", { className: classNames.inputWrapper, children: jsx("input", { ...rest, "data-smarthr-ui-input": "true", type: "text", name: name, value: inputValue, disabled: disabled, required: required && selectedItems.length === 0, ref: inputRef, onChange: functions.handleChangeInput, onFocus: functions.handleFocusInput, onCompositionStart: functions.handleCompositionStart, onCompositionEnd: functions.handleCompositionEnd, onKeyDown: functions.handleKeyDownInput, autoComplete: autoComplete ?? 'off', tabIndex: 0, role: "combobox", "aria-activedescendant": activeOption?.id, "aria-controls": `${listBoxId} ${selectedListId}`, "aria-haspopup": "listbox", "aria-expanded": isExpanded, "aria-invalid": error || undefined, "aria-disabled": disabled, "aria-autocomplete": "list", className: classNames.input }) }), selectedItems.length === 0 && placeholder && !isExpanded && (jsx("p", { className: classNames.placeholder, children: placeholder }))] }), jsx(MemoizedCaretDown, { disabled: disabled, isExpanded: isExpanded, classNames: classNames }), jsx(ListBox, { ...listBoxProps })] }));
|
|
345
339
|
};
|
|
346
340
|
const MultiCombobox = genericsForwardRef(ActualMultiCombobox);
|
|
347
341
|
const MemoizedCaretDown = memo(({ disabled, isExpanded, classNames }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MultiCombobox.js","sources":["../../../../src/components/Combobox/MultiCombobox/MultiCombobox.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 createRef,\n memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useOuterClick } from '../../../hooks/useOuterClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useLocalize } from '../../../intl'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { FaCaretDownIcon } from '../../Icon'\nimport { Scroller } from '../../Scroller'\nimport { areItemsEqual } from '../helper'\nimport { ListBox, useListbox } from '../useListbox'\nimport { useMultiOptions } from '../useOptions'\n\nimport { MultiSelectedItem } from './MultiSelectedItem'\n\nimport type { ComboboxItem, BaseProps as ComboboxProps } from '../types'\n\ntype BaseProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテムのリスト\n */\n selectedItems: Array<ComboboxItem<T> & { deletable?: boolean }>\n /**\n * 選択されているアイテムのラベルを省略表示するかどうか\n */\n selectedItemEllipsis?: boolean\n /**\n * テキストボックスの `value` 属性の値。\n * `onChangeInput` と併せて設定することで、テキストボックスの挙動が制御可能になる。\n */\n inputValue?: string\n /**\n * 選択されているアイテムの削除ボタンがクリックされた時に発火するコールバック関数\n */\n onDelete?: (item: ComboboxItem<T>) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItems: Array<ComboboxItem<T>>) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * アイテムが選択されたときに選択済みかどうかを判定するコールバック関数/\n */\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = BaseProps<T> & Omit<ComponentPropsWithoutRef<'input'>, keyof BaseProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst preventDefaultWithPressEnter = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n }\n}\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_LEFT_KEY_REGEX = /^(Arrow)?Left$/\nconst ARROW_RIGHT_KEY_REGEX = /^(Arrow)?Right/\nconst ARROW_UP_AND_DOWN_KEY_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: [\n 'smarthr-ui-MultiCombobox',\n 'shr-box-border shr-inline-flex shr-min-w-[15em] shr-rounded-m shr-border shr-border-solid shr-px-0.5 shr-py-0.25 shr-align-bottom',\n 'contrast-more:shr-border-high-contrast',\n 'has-[[role=combobox][aria-expanded=true]]:shr-focus-indicator',\n 'shr-cursor-text shr-border-default shr-bg-white',\n 'has-[[role=combobox]:disabled]:shr-cursor-not-allowed',\n 'has-[[role=combobox]:disabled]:shr-border-default/50',\n 'has-[[role=combobox]:disabled]:shr-bg-white-darken',\n 'has-[[role=combobox]:disabled]:shr-text-disabled',\n // HINT: disabled と詳細度が同じ [0,2,0] のため、CSS ソース順でこちらを後に置くことで error 時の border 色を優先させる\n 'has-[[role=combobox][aria-invalid]]:shr-border-danger',\n ],\n inputArea: 'shr-flex shr-flex-1 shr-flex-wrap shr-gap-0.5',\n selectedList:\n 'smarthr-ui-MultiCombobox-selectedList shr-contents shr-list-none [&_li]:shr-min-w-0',\n inputWrapper: [\n 'shr-flex shr-flex-1 shr-items-center',\n 'has-[[role=combobox][aria-expanded=false]]:shr-pointer-events-none',\n 'has-[[role=combobox][aria-expanded=false]]:shr-absolute',\n 'has-[[role=combobox][aria-expanded=false]]:shr-opacity-0',\n ],\n input: [\n 'smarthr-ui-MultiCombobox-input',\n 'shr-w-full shr-min-w-[5em] shr-border-none shr-text-base shr-text-black shr-outline-none shr-outline-0',\n 'disabled:shr-hidden',\n ],\n placeholderEl: 'smarthr-ui-MultiCombobox-placeholder shr-my-0 shr-self-center',\n suffixWrapper: [\n 'shr-relative -shr-me-0.5 shr-ms-0.5 shr-p-0.5',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n suffixIcon: 'shr-block',\n },\n})\n\nconst ActualMultiCombobox = <T,>(\n {\n items,\n selectedItems,\n name,\n disabled = false,\n required = false,\n error = false,\n creatable = false,\n placeholder = '',\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n selectedItemEllipsis,\n width,\n dropdownWidth = 'auto',\n inputValue: controlledInputValue,\n className,\n onChange,\n onChangeInput,\n onAdd,\n onDelete,\n onSelect,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n isItemSelected,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const triggerRef = useRef<HTMLDivElement>(null)\n const [isExpanded, setIsExpanded] = useState(false)\n const [highlighted, setHighlighted] = useState(false)\n const [uncontrolledInputValue, setUncontrolledInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n\n const selectedListId = useId()\n\n const isInputControlled = controlledInputValue !== undefined\n const inputValue = isInputControlled ? controlledInputValue : uncontrolledInputValue\n const setInputValueIfUncontrolled = isInputControlled ? NOOP : setUncontrolledInputValue\n const isInputEmpty = !inputValue\n\n const { options } = useMultiOptions({\n items,\n selected: selectedItems,\n creatable,\n inputValue,\n isItemSelected,\n })\n const selectedItemLength = selectedItems.length\n\n // TODO: 完全にcreateRefを作り直すのではなく、差分更新させたい\n const deletionButtonRefs = useMemo(() => {\n const refs: Array<ReturnType<typeof createRef<HTMLButtonElement>>> = []\n\n for (let i = 0; i < selectedItemLength; i++) {\n refs[i] = createRef<HTMLButtonElement>()\n }\n\n return refs\n }, [selectedItemLength])\n const inputRef = useRef<HTMLInputElement>(null)\n\n const [focusedIndex, setFocusedIndex] = useState<number | null>(null)\n\n // eslint-disable-next-line local-rules/best-practice-for-use-latest\n const latestForListBox = useLatest({\n onDelete,\n onChangeSelected,\n onSelect,\n onChangeInput,\n selectedItems,\n })\n\n const listBoxFunctions = useMemo(() => {\n const handleDelete = (item: ComboboxItem<T>) => {\n const handlers: Array<(deletingItem: ComboboxItem<T>) => void> = []\n\n if (latestForListBox.onDelete) {\n handlers.push((deletingItem: ComboboxItem<T>) => latestForListBox.onDelete!(deletingItem))\n }\n if (latestForListBox.onChangeSelected) {\n handlers.push((deletingItem: ComboboxItem<T>) =>\n latestForListBox.onChangeSelected!(\n latestForListBox.selectedItems.filter(\n (selected) => !areItemsEqual(selected, deletingItem),\n ),\n ),\n )\n }\n\n if (handlers.length > 0) {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n handlers.forEach((h) => h(item))\n })\n }\n }\n\n return {\n handleDelete,\n handleSelect: (selected: ComboboxItem<T>) => {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n const matchedSelectedItem = latestForListBox.selectedItems.find((item) =>\n areItemsEqual(item, selected),\n )\n\n if (matchedSelectedItem === undefined) {\n latestForListBox.onSelect?.(selected)\n latestForListBox.onChangeSelected?.(latestForListBox.selectedItems.concat(selected))\n\n // 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n latestForListBox.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n } else if (matchedSelectedItem.deletable !== false) {\n handleDelete(selected)\n }\n })\n },\n }\n }, [latestForListBox])\n\n const { listBoxProps, activeOption, handleKeyDownListBox, listBoxId, listBoxRef } = useListbox({\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n onSelect: listBoxFunctions.handleSelect,\n isExpanded,\n isLoading,\n triggerRef,\n noResultText,\n })\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onFocus,\n onBlur,\n onKeyPress,\n disabled,\n isExpanded,\n highlighted,\n isComposing,\n isInputEmpty,\n selectedItems,\n deletionButtonRefs,\n focusedIndex,\n selectedItemLength,\n setInputValueIfUncontrolled,\n handleKeyDownListBox,\n })\n\n const functions = useMemo(() => {\n const resetDeletionButtonFocus = () => {\n setFocusedIndex(null)\n }\n\n const handleDelete = listBoxFunctions.handleDelete\n\n const focusPrevDeletionButton = () => {\n if (latest.selectedItemLength === 0) {\n return\n }\n\n if (latest.focusedIndex !== null) {\n const nextIndex = Math.max(latest.focusedIndex - 1, 0)\n\n latest.deletionButtonRefs[nextIndex].current?.focus()\n setFocusedIndex(nextIndex)\n } else if (inputRef.current?.selectionStart === 0) {\n const nextIndex = latest.deletionButtonRefs.length - 1\n\n latest.deletionButtonRefs[nextIndex].current?.focus()\n setFocusedIndex(nextIndex)\n }\n }\n\n const focusNextDeletionButton = () => {\n if (latest.deletionButtonRefs.length === 0 || latest.focusedIndex === null) {\n return\n }\n\n const nextIndex = latest.focusedIndex + 1\n\n if (nextIndex < latest.deletionButtonRefs.length) {\n latest.deletionButtonRefs[nextIndex].current?.focus()\n setFocusedIndex(nextIndex)\n } else {\n setFocusedIndex(null)\n // キー入力が input に影響しないようにフォーカスタイミングを遅らせる\n setTimeout(() => {\n inputRef.current?.focus()\n })\n }\n }\n\n const focus = () => {\n latest.onFocus?.()\n setIsExpanded(true)\n }\n\n const blur = () => {\n if (latest.isExpanded) {\n latest.onBlur?.()\n setIsExpanded(false)\n resetDeletionButtonFocus()\n }\n }\n\n return {\n handleDelete,\n blur,\n handleDelegateKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {\n if (latest.isComposing) return\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n blur()\n } else if (e.key === 'Tab') {\n if (latest.isExpanded) {\n // フォーカスがコンポーネントを抜けるように先に input をフォーカスしておく\n inputRef.current?.focus()\n }\n\n blur()\n } else if (ARROW_LEFT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusPrevDeletionButton()\n } else if (ARROW_RIGHT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusNextDeletionButton()\n } else if (\n e.key === 'Backspace' &&\n latest.isInputEmpty &&\n latest.selectedItems.length > 0 &&\n latest.selectedItems[latest.selectedItems.length - 1].deletable !== false\n ) {\n e.preventDefault()\n e.stopPropagation()\n\n const lastItem = latest.selectedItems[latest.selectedItems.length - 1]\n\n handleDelete(lastItem)\n setHighlighted(true)\n latest.setInputValueIfUncontrolled(innerText(lastItem.label))\n } else {\n e.stopPropagation()\n inputRef.current?.focus()\n resetDeletionButtonFocus()\n }\n\n latest.handleKeyDownListBox(e)\n },\n handleDelegateClick: (e: MouseEvent<HTMLElement>) => {\n if (\n !latest.disabled &&\n !latest.isExpanded &&\n !(e.target as HTMLElement).closest('.smarthr-ui-MultiCombobox-deleteButton')\n ) {\n focus()\n }\n },\n handleChangeInput: (e: ChangeEvent<HTMLInputElement>) => {\n const handlers = [latest.onChange, latest.onChangeInput].filter((h) => !!h)\n\n handlers.forEach((h) => h(e))\n latest.setInputValueIfUncontrolled(e.currentTarget.value)\n },\n handleFocusInput: () => {\n resetDeletionButtonFocus()\n\n if (!latest.isExpanded) {\n focus()\n }\n },\n handleCompositionStart: () => setIsComposing(true),\n handleCompositionEnd: () => setIsComposing(false),\n handleKeyDownInput: (e: KeyboardEvent<HTMLInputElement>) => {\n if (ARROW_UP_AND_DOWN_KEY_REGEX.test(e.key)) {\n // 上下キー入力はリストボックスの activeDescendant の移動に用いるため、input 内では作用させない\n e.preventDefault()\n }\n },\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n handleDelegateKeyPress: (e: KeyboardEvent<HTMLInputElement>) => {\n preventDefaultWithPressEnter(e)\n latest.onKeyPress?.(e)\n },\n }\n }, [listBoxFunctions, latest])\n\n useOuterClick(\n useMemo(() => [triggerRef, listBoxRef], [listBoxRef]),\n functions.blur,\n )\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n useEffect(() => {\n if (latest.highlighted) {\n setHighlighted(false)\n inputRef.current?.select()\n } else {\n setInputValueIfUncontrolled('')\n }\n }, [selectedItems, setInputValueIfUncontrolled, inputRef, latest])\n\n useEffect(() => {\n if (isExpanded) {\n inputRef.current?.focus()\n }\n }, [isExpanded, selectedItems, isInputControlled, inputRef])\n\n const classNames = useMemo(() => {\n const {\n wrapper,\n inputArea,\n selectedList,\n inputWrapper,\n input,\n placeholderEl,\n suffixWrapper,\n suffixIcon,\n } = classNameGenerator()\n\n return {\n wrapper: wrapper({ className }),\n inputArea: inputArea(),\n selectedList: selectedList(),\n inputWrapper: inputWrapper(),\n input: input(),\n placeholder: placeholderEl(),\n suffixWrapper: suffixWrapper(),\n suffixIcon: suffixIcon(),\n }\n }, [className])\n\n const localized = useLocalize({\n selectedListAriaLabel: {\n id: 'smarthr-ui/MultiCombobox/selectedListAriaLabel',\n defaultText: '選択済みアイテム',\n },\n })\n\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n ref={triggerRef}\n role=\"group\"\n onClick={functions.handleDelegateClick}\n onKeyDown={functions.handleDelegateKeyDown}\n onKeyPress={functions.handleDelegateKeyPress}\n className={classNames.wrapper}\n style={{\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <Scroller className={classNames.inputArea}>\n <ul\n id={selectedListId}\n aria-label={localized.selectedListAriaLabel}\n className={classNames.selectedList}\n >\n {selectedItems.map((selectedItem, i) => (\n <li key={`${selectedItem.label}-${innerText(selectedItem.value)}`}>\n <MultiSelectedItem\n item={selectedItem}\n disabled={disabled}\n handleDelete={functions.handleDelete}\n enableEllipsis={selectedItemEllipsis}\n buttonRef={deletionButtonRefs[i]}\n />\n </li>\n ))}\n </ul>\n\n <div className={classNames.inputWrapper}>\n <input\n {...rest}\n data-smarthr-ui-input=\"true\"\n type=\"text\"\n name={name}\n value={inputValue}\n disabled={disabled}\n required={required && selectedItems.length === 0}\n ref={inputRef}\n onChange={functions.handleChangeInput}\n onFocus={functions.handleFocusInput}\n onCompositionStart={functions.handleCompositionStart}\n onCompositionEnd={functions.handleCompositionEnd}\n onKeyDown={functions.handleKeyDownInput}\n autoComplete={autoComplete ?? 'off'}\n tabIndex={0}\n role=\"combobox\"\n aria-activedescendant={activeOption?.id}\n aria-controls={`${listBoxId} ${selectedListId}`}\n aria-haspopup=\"listbox\"\n aria-expanded={isExpanded}\n aria-invalid={error || undefined}\n aria-disabled={disabled}\n aria-autocomplete=\"list\"\n className={classNames.input}\n />\n </div>\n\n {selectedItems.length === 0 && placeholder && !isExpanded && (\n <p className={classNames.placeholder}>{placeholder}</p>\n )}\n </Scroller>\n\n <MemoizedCaretDown disabled={disabled} isExpanded={isExpanded} classNames={classNames} />\n\n <ListBox {...listBoxProps} />\n </div>\n )\n}\n\nexport const MultiCombobox = genericsForwardRef(ActualMultiCombobox)\n\nconst MemoizedCaretDown = memo<{\n disabled: boolean\n isExpanded: boolean\n classNames: {\n suffixWrapper: string\n suffixIcon: string\n }\n}>(({ disabled, isExpanded, classNames }) => {\n const theme = useTheme()\n const caretIconColor = isExpanded\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n <div className={classNames.suffixWrapper}>\n <FaCaretDownIcon color={caretIconColor} className={classNames.suffixIcon} />\n </div>\n )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AAEA;AACE;;;AAGF;AAEA;AACA;AACA;AACA;AAEA;AACE;AACA;;AAGF;AACE;AACE;;;;;;;;;;;;AAYC;AACD;AACA;AAEA;;;;;AAKC;AACD;;;;AAIC;AACD;AACA;;;AAGC;AACD;AACD;AACF;AAED;AAkCE;;;;;AAMA;AAEA;;;AAGA;AAEA;;AAEE;;;;AAID;AACD;;AAGA;;AAGE;AACE;;AAGF;AACF;AACA;;;;;;;;;AAWC;AAED;AACE;;AAGE;AACE;;AAEF;AACE;;AASF;;;;AAII;AACF;;AAEJ;;;AAIE;;;;;AAQI;AACE;AACA;;AAGA;;AACK;;;AAGT;;;AAGN;AAEA;;;;;;;;;;AAUC;;;;;;;;;;;;;;;;;;AAmBA;AAED;;;AAGE;AAEA;;AAGE;;;AAIA;AACE;;;;;;;;;AAUJ;;AAGE;;;AAIA;;;;;;;;;AASI;AACF;;AAEJ;;AAGE;;AAEF;;AAGE;AACE;;AAEA;;AAEJ;;;;AAKE;;;;;AAKI;;AACK;AACL;;AAEE;;AAGF;;;;AAGA;;;;AAGA;;AACK;AAEL;AACA;AACA;;;AAKA;;;;;;;AAOA;AACA;;AAGF;;AAEF;;;;AAMI;;;AAGJ;;AAGE;;;;AAIA;AAEA;AACE;;;AAGJ;AACA;AACA;;;;;;;;;AASA;;AAEE;;;AAGN;;;;AAUE;;AAEE;;;;;;;;AAQA;;;AAIJ;;;AAaI;;;;;;;;;AASJ;;AAGE;AACE;AACA;AACD;AACF;;;AAIC;AAQI;AACA;;AA6DR;;AAIA;AAQE;;AAEE;AACA;AACE;AACA;;AAON;;"}
|
|
1
|
+
{"version":3,"file":"MultiCombobox.js","sources":["../../../../src/components/Combobox/MultiCombobox/MultiCombobox.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 memo,\n useEffect,\n useId,\n useImperativeHandle,\n useMemo,\n useRef,\n useState,\n} from 'react'\nimport innerText from 'react-innertext'\nimport { tv } from 'tailwind-variants'\n\nimport { useLatest } from '../../../hooks/useLatest'\nimport { useOuterClick } from '../../../hooks/useOuterClick'\nimport { useTheme } from '../../../hooks/useTheme'\nimport { useLocalize } from '../../../intl'\nimport { findDelegateTarget } from '../../../libs/delegate'\nimport { genericsForwardRef } from '../../../libs/util'\nimport { FaCaretDownIcon } from '../../Icon'\nimport { Scroller } from '../../Scroller'\nimport { areItemsEqual } from '../helper'\nimport { ListBox, useListbox } from '../useListbox'\nimport { useMultiOptions } from '../useOptions'\n\nimport { DELETE_BUTTON_SELECTOR, MultiSelectedItem } from './MultiSelectedItem'\n\nimport type { ComboboxItem, BaseProps as ComboboxProps } from '../types'\n\ntype BaseProps<T> = ComboboxProps<T> & {\n /**\n * 選択されているアイテムのリスト\n */\n selectedItems: Array<ComboboxItem<T> & { deletable?: boolean }>\n /**\n * 選択されているアイテムのラベルを省略表示するかどうか\n */\n selectedItemEllipsis?: boolean\n /**\n * テキストボックスの `value` 属性の値。\n * `onChangeInput` と併せて設定することで、テキストボックスの挙動が制御可能になる。\n */\n inputValue?: string\n /**\n * 選択されているアイテムの削除ボタンがクリックされた時に発火するコールバック関数\n */\n onDelete?: (item: ComboboxItem<T>) => void\n /**\n * 選択されているアイテムのリストが変わった時に発火するコールバック関数\n */\n onChangeSelected?: (selectedItems: Array<ComboboxItem<T>>) => void\n /**\n * コンポーネントがフォーカスされたときに発火するコールバック関数\n */\n onFocus?: () => void\n /**\n * コンポーネントからフォーカスが外れた時に発火するコールバック関数\n */\n onBlur?: () => void\n /**\n * アイテムが選択されたときに選択済みかどうかを判定するコールバック関数/\n */\n isItemSelected?: (targetItem: ComboboxItem<T>, selectedItems: Array<ComboboxItem<T>>) => boolean\n /**\n * 検索結果が0件の時に表示するコンテンツ\n */\n noResultText?: ReactNode\n}\ntype Props<T> = BaseProps<T> & Omit<ComponentPropsWithoutRef<'input'>, keyof BaseProps<unknown>>\n\nconst NOOP = () => undefined\n\nconst preventDefaultWithPressEnter = (e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === 'Enter') {\n e.preventDefault()\n }\n}\n\nconst ESCAPE_KEY_REGEX = /^Esc(ape)?$/\nconst ARROW_LEFT_KEY_REGEX = /^(Arrow)?Left$/\nconst ARROW_RIGHT_KEY_REGEX = /^(Arrow)?Right/\nconst ARROW_UP_AND_DOWN_KEY_REGEX = /^(Arrow)?(Up|Down)$/\n\nconst EMPTY_INPUT_CHANGE_EVENT = {\n currentTarget: { value: '' },\n target: { value: '' },\n} as ChangeEvent<HTMLInputElement>\n\nconst DELETE_BUTTON_CLASSNAME = `.${DELETE_BUTTON_SELECTOR}`\n\nconst classNameGenerator = tv({\n slots: {\n wrapper: [\n 'smarthr-ui-MultiCombobox',\n 'shr-box-border shr-inline-flex shr-min-w-[15em] shr-rounded-m shr-border shr-border-solid shr-px-0.5 shr-py-0.25 shr-align-bottom',\n 'contrast-more:shr-border-high-contrast',\n 'has-[[role=combobox][aria-expanded=true]]:shr-focus-indicator',\n 'shr-cursor-text shr-border-default shr-bg-white',\n 'has-[[role=combobox]:disabled]:shr-cursor-not-allowed',\n 'has-[[role=combobox]:disabled]:shr-border-default/50',\n 'has-[[role=combobox]:disabled]:shr-bg-white-darken',\n 'has-[[role=combobox]:disabled]:shr-text-disabled',\n // HINT: disabled と詳細度が同じ [0,2,0] のため、CSS ソース順でこちらを後に置くことで error 時の border 色を優先させる\n 'has-[[role=combobox][aria-invalid]]:shr-border-danger',\n ],\n inputArea: 'shr-flex shr-flex-1 shr-flex-wrap shr-gap-0.5',\n selectedList:\n 'smarthr-ui-MultiCombobox-selectedList shr-contents shr-list-none [&_li]:shr-min-w-0',\n inputWrapper: [\n 'shr-flex shr-flex-1 shr-items-center',\n 'has-[[role=combobox][aria-expanded=false]]:shr-pointer-events-none',\n 'has-[[role=combobox][aria-expanded=false]]:shr-absolute',\n 'has-[[role=combobox][aria-expanded=false]]:shr-opacity-0',\n ],\n input: [\n 'smarthr-ui-MultiCombobox-input',\n 'shr-w-full shr-min-w-[5em] shr-border-none shr-text-base shr-text-black shr-outline-none shr-outline-0',\n 'disabled:shr-hidden',\n ],\n placeholderEl: 'smarthr-ui-MultiCombobox-placeholder shr-my-0 shr-self-center',\n suffixWrapper: [\n 'shr-relative -shr-me-0.5 shr-ms-0.5 shr-p-0.5',\n 'before:shr-absolute before:shr-inset-x-0 before:shr-inset-y-0.25 before:shr-w-0 before:shr-border-0 before:shr-border-l before:shr-border-solid before:shr-border-default before:shr-content-[\"\"]',\n ],\n suffixIcon: 'shr-block',\n },\n})\n\nconst ActualMultiCombobox = <T,>(\n {\n items,\n selectedItems,\n name,\n disabled = false,\n required = false,\n error = false,\n creatable = false,\n placeholder = '',\n autoComplete,\n dropdownHelpMessage,\n isLoading,\n selectedItemEllipsis,\n width,\n dropdownWidth = 'auto',\n inputValue: controlledInputValue,\n className,\n onChange,\n onChangeInput,\n onAdd,\n onDelete,\n onSelect,\n onChangeSelected,\n onFocus,\n onBlur,\n onKeyPress,\n isItemSelected,\n noResultText,\n style,\n ...rest\n }: Props<T>,\n ref: Ref<HTMLInputElement>,\n) => {\n const triggerRef = useRef<HTMLDivElement>(null)\n const [isExpanded, setIsExpanded] = useState(false)\n const [highlighted, setHighlighted] = useState(false)\n const [uncontrolledInputValue, setUncontrolledInputValue] = useState('')\n const [isComposing, setIsComposing] = useState(false)\n\n const selectedListId = useId()\n\n const isInputControlled = controlledInputValue !== undefined\n const inputValue = isInputControlled ? controlledInputValue : uncontrolledInputValue\n const setInputValueIfUncontrolled = isInputControlled ? NOOP : setUncontrolledInputValue\n const isInputEmpty = !inputValue\n\n const { options } = useMultiOptions({\n items,\n selected: selectedItems,\n creatable,\n inputValue,\n isItemSelected,\n })\n const inputRef = useRef<HTMLInputElement>(null)\n\n // eslint-disable-next-line local-rules/best-practice-for-use-latest\n const latestForListBox = useLatest({\n onDelete,\n onChangeSelected,\n onSelect,\n onChangeInput,\n selectedItems,\n })\n\n const listBoxFunctions = useMemo(() => {\n const handleDelete = (item: ComboboxItem<T>) => {\n const handlers: Array<(deletingItem: ComboboxItem<T>) => void> = []\n\n if (latestForListBox.onDelete) {\n handlers.push((deletingItem: ComboboxItem<T>) => latestForListBox.onDelete!(deletingItem))\n }\n if (latestForListBox.onChangeSelected) {\n handlers.push((deletingItem: ComboboxItem<T>) =>\n latestForListBox.onChangeSelected!(\n latestForListBox.selectedItems.filter(\n (selected) => !areItemsEqual(selected, deletingItem),\n ),\n ),\n )\n }\n\n if (handlers.length > 0) {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n handlers.forEach((h) => h(item))\n })\n }\n }\n\n return {\n handleDelete,\n handleSelect: (selected: ComboboxItem<T>) => {\n // HINT: Dropdown系コンポーネント内でComboboxを使うと、選択肢がportalで表現されている関係上Dropdownが閉じてしまう\n // requestAnimationFrameを追加、処理を遅延させることで正常に閉じる/閉じないの判定を行えるようにする\n requestAnimationFrame(() => {\n const matchedSelectedItem = latestForListBox.selectedItems.find((item) =>\n areItemsEqual(item, selected),\n )\n\n if (matchedSelectedItem === undefined) {\n latestForListBox.onSelect?.(selected)\n latestForListBox.onChangeSelected?.(latestForListBox.selectedItems.concat(selected))\n\n // 制御コンポーネントの場合に親側でinputValueを更新できるように、選択時にonChangeInputを空文字で発火する\n latestForListBox.onChangeInput?.(EMPTY_INPUT_CHANGE_EVENT)\n } else if (matchedSelectedItem.deletable !== false) {\n handleDelete(selected)\n }\n })\n },\n }\n }, [latestForListBox])\n\n const { listBoxProps, activeOption, handleKeyDownListBox, listBoxId, listBoxRef } = useListbox({\n options,\n dropdownHelpMessage,\n dropdownWidth,\n onAdd,\n onSelect: listBoxFunctions.handleSelect,\n isExpanded,\n isLoading,\n triggerRef,\n noResultText,\n })\n\n const latest = useLatest({\n onChange,\n onChangeInput,\n onFocus,\n onBlur,\n onKeyPress,\n disabled,\n isExpanded,\n highlighted,\n isComposing,\n isInputEmpty,\n selectedItems,\n setInputValueIfUncontrolled,\n handleKeyDownListBox,\n })\n\n const functions = useMemo(() => {\n const handleDelete = listBoxFunctions.handleDelete\n\n const getDeletionButtons = () => {\n if (triggerRef.current) {\n const buttons =\n triggerRef.current.querySelectorAll<HTMLButtonElement>(DELETE_BUTTON_CLASSNAME)\n\n if (buttons.length > 0) {\n const actualButtons = Array.from(buttons)\n\n return {\n buttons: actualButtons,\n currentIndex: actualButtons.indexOf(document.activeElement as HTMLButtonElement),\n }\n }\n }\n\n return null\n }\n\n const focusPrevDeletionButton = () => {\n const result = getDeletionButtons()\n\n if (!result) return\n\n const { buttons, currentIndex } = result\n\n if (currentIndex !== -1) {\n buttons[Math.max(currentIndex - 1, 0)].focus()\n } else if (inputRef.current?.selectionStart === 0) {\n buttons[buttons.length - 1].focus()\n }\n }\n\n const focusNextDeletionButton = () => {\n const result = getDeletionButtons()\n\n if (!result) return\n\n const { buttons, currentIndex } = result\n\n if (currentIndex === -1) return\n\n const nextIndex = currentIndex + 1\n\n if (nextIndex < buttons.length) {\n buttons[nextIndex].focus()\n } else {\n // キー入力が input に影響しないようにフォーカスタイミングを遅らせる\n setTimeout(() => {\n inputRef.current?.focus()\n })\n }\n }\n\n const focus = () => {\n latest.onFocus?.()\n setIsExpanded(true)\n }\n\n const blur = () => {\n if (latest.isExpanded) {\n latest.onBlur?.()\n setIsExpanded(false)\n }\n }\n\n return {\n handleDelete,\n blur,\n handleDelegateKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {\n if (latest.isComposing) return\n\n if (ESCAPE_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n blur()\n } else if (e.key === 'Tab') {\n if (latest.isExpanded) {\n // フォーカスがコンポーネントを抜けるように先に input をフォーカスしておく\n inputRef.current?.focus()\n }\n\n blur()\n } else if (ARROW_LEFT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusPrevDeletionButton()\n } else if (ARROW_RIGHT_KEY_REGEX.test(e.key)) {\n e.stopPropagation()\n focusNextDeletionButton()\n } else if (\n e.key === 'Backspace' &&\n latest.isInputEmpty &&\n latest.selectedItems.length > 0 &&\n latest.selectedItems[latest.selectedItems.length - 1].deletable !== false\n ) {\n e.preventDefault()\n e.stopPropagation()\n\n const lastItem = latest.selectedItems[latest.selectedItems.length - 1]\n\n handleDelete(lastItem)\n setHighlighted(true)\n latest.setInputValueIfUncontrolled(innerText(lastItem.label))\n } else {\n e.stopPropagation()\n inputRef.current?.focus()\n }\n\n latest.handleKeyDownListBox(e)\n },\n handleDelegateClick: (e: MouseEvent<HTMLElement>) => {\n if (\n !latest.disabled &&\n !latest.isExpanded &&\n !findDelegateTarget(e, DELETE_BUTTON_CLASSNAME)\n ) {\n focus()\n }\n },\n handleChangeInput: (e: ChangeEvent<HTMLInputElement>) => {\n const handlers = [latest.onChange, latest.onChangeInput].filter((h) => !!h)\n\n handlers.forEach((h) => h(e))\n latest.setInputValueIfUncontrolled(e.currentTarget.value)\n },\n handleFocusInput: () => {\n if (!latest.isExpanded) {\n focus()\n }\n },\n handleCompositionStart: () => setIsComposing(true),\n handleCompositionEnd: () => setIsComposing(false),\n handleKeyDownInput: (e: KeyboardEvent<HTMLInputElement>) => {\n if (ARROW_UP_AND_DOWN_KEY_REGEX.test(e.key)) {\n // 上下キー入力はリストボックスの activeDescendant の移動に用いるため、input 内では作用させない\n e.preventDefault()\n }\n },\n // HINT: form内にcomboboxを設置 & 検索inputにfocusした状態で\n // アイテムをキーボードで選択し、Enterを押すとinput上でEnterを押したことになるため、\n // submitイベントが発生し、formが送信される場合がある\n handleDelegateKeyPress: (e: KeyboardEvent<HTMLInputElement>) => {\n preventDefaultWithPressEnter(e)\n latest.onKeyPress?.(e)\n },\n }\n }, [listBoxFunctions, latest])\n\n useOuterClick(\n useMemo(() => [triggerRef, listBoxRef], [listBoxRef]),\n functions.blur,\n )\n\n useImperativeHandle<HTMLInputElement | null, HTMLInputElement | null>(ref, () => inputRef.current)\n\n useEffect(() => {\n if (latest.highlighted) {\n setHighlighted(false)\n inputRef.current?.select()\n } else {\n setInputValueIfUncontrolled('')\n }\n }, [selectedItems, setInputValueIfUncontrolled, inputRef, latest])\n\n useEffect(() => {\n if (isExpanded) {\n inputRef.current?.focus()\n }\n }, [isExpanded, selectedItems, isInputControlled, inputRef])\n\n const classNames = useMemo(() => {\n const {\n wrapper,\n inputArea,\n selectedList,\n inputWrapper,\n input,\n placeholderEl,\n suffixWrapper,\n suffixIcon,\n } = classNameGenerator()\n\n return {\n wrapper: wrapper({ className }),\n inputArea: inputArea(),\n selectedList: selectedList(),\n inputWrapper: inputWrapper(),\n input: input(),\n placeholder: placeholderEl(),\n suffixWrapper: suffixWrapper(),\n suffixIcon: suffixIcon(),\n }\n }, [className])\n\n const localized = useLocalize({\n selectedListAriaLabel: {\n id: 'smarthr-ui/MultiCombobox/selectedListAriaLabel',\n defaultText: '選択済みアイテム',\n },\n })\n\n return (\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n ref={triggerRef}\n role=\"group\"\n onClick={functions.handleDelegateClick}\n onKeyDown={functions.handleDelegateKeyDown}\n onKeyPress={functions.handleDelegateKeyPress}\n className={classNames.wrapper}\n style={{\n ...style,\n width: typeof width === 'number' ? `${width}px` : width,\n }}\n >\n <Scroller className={classNames.inputArea}>\n <ul\n id={selectedListId}\n aria-label={localized.selectedListAriaLabel}\n className={classNames.selectedList}\n >\n {selectedItems.map((selectedItem) => (\n <li key={`${selectedItem.label}-${innerText(selectedItem.value)}`}>\n <MultiSelectedItem\n item={selectedItem}\n disabled={disabled}\n handleDelete={functions.handleDelete}\n enableEllipsis={selectedItemEllipsis}\n />\n </li>\n ))}\n </ul>\n\n <div className={classNames.inputWrapper}>\n <input\n {...rest}\n data-smarthr-ui-input=\"true\"\n type=\"text\"\n name={name}\n value={inputValue}\n disabled={disabled}\n required={required && selectedItems.length === 0}\n ref={inputRef}\n onChange={functions.handleChangeInput}\n onFocus={functions.handleFocusInput}\n onCompositionStart={functions.handleCompositionStart}\n onCompositionEnd={functions.handleCompositionEnd}\n onKeyDown={functions.handleKeyDownInput}\n autoComplete={autoComplete ?? 'off'}\n tabIndex={0}\n role=\"combobox\"\n aria-activedescendant={activeOption?.id}\n aria-controls={`${listBoxId} ${selectedListId}`}\n aria-haspopup=\"listbox\"\n aria-expanded={isExpanded}\n aria-invalid={error || undefined}\n aria-disabled={disabled}\n aria-autocomplete=\"list\"\n className={classNames.input}\n />\n </div>\n\n {selectedItems.length === 0 && placeholder && !isExpanded && (\n <p className={classNames.placeholder}>{placeholder}</p>\n )}\n </Scroller>\n\n <MemoizedCaretDown disabled={disabled} isExpanded={isExpanded} classNames={classNames} />\n\n <ListBox {...listBoxProps} />\n </div>\n )\n}\n\nexport const MultiCombobox = genericsForwardRef(ActualMultiCombobox)\n\nconst MemoizedCaretDown = memo<{\n disabled: boolean\n isExpanded: boolean\n classNames: {\n suffixWrapper: string\n suffixIcon: string\n }\n}>(({ disabled, isExpanded, classNames }) => {\n const theme = useTheme()\n const caretIconColor = isExpanded\n ? theme.textColor.black\n : disabled\n ? theme.textColor.disabled\n : theme.textColor.grey\n\n return (\n <div className={classNames.suffixWrapper}>\n <FaCaretDownIcon color={caretIconColor} className={classNames.suffixIcon} />\n </div>\n )\n})\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA6EA;AAEA;AACE;;;AAGF;AAEA;AACA;AACA;AACA;AAEA;AACE;AACA;;AAGF;AAEA;AACE;AACE;;;;;;;;;;;;AAYC;AACD;AACA;AAEA;;;;;AAKC;AACD;;;;AAIC;AACD;AACA;;;AAGC;AACD;AACD;AACF;AAED;AAkCE;;;;;AAMA;AAEA;;;AAGA;AAEA;;AAEE;;;;AAID;AACD;;;;;;;;AASC;AAED;AACE;;AAGE;AACE;;AAEF;AACE;;AASF;;;;AAII;AACF;;AAEJ;;;AAIE;;;;;AAQI;AACE;AACA;;AAGA;;AACK;;;AAGT;;;AAGN;AAEA;;;;;;;;;;AAUC;;;;;;;;;;;;;;;AAgBA;AAED;AACE;;AAGE;;AAIE;;;AAII;;;;;AAMN;AACF;;AAGE;AAEA;;AAEA;AAEA;AACE;;;;;AAIJ;;AAGE;AAEA;;AAEA;;;AAIA;AAEA;AACE;;;;;AAIE;AACF;;AAEJ;;AAGE;;AAEF;;AAGE;AACE;;;AAGJ;;;;AAKE;;;;;AAKI;;AACK;AACL;;AAEE;;AAGF;;;;AAGA;;;;AAGA;;AACK;AAEL;AACA;AACA;;;AAKA;;;;;;;AAOA;;AAGF;;AAEF;;;AAII;AAEA;;;AAGJ;;AAGE;;;;AAIA;AACE;;;AAGJ;AACA;AACA;;;;;;;;;AASA;;AAEE;;;AAGN;;;;AAUE;;AAEE;;;;;;;;AAQA;;;AAIJ;;;AAaI;;;;;;;;;AASJ;;AAGE;AACE;AACA;AACD;AACF;;;AAIC;AAQI;AACA;;AA4DR;;AAIA;AAQE;;AAEE;AACA;AACE;AACA;;AAON;;"}
|