@moneyforward/mfui-components 3.14.1 → 3.16.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/dist/src/DateTimeSelection/DatePicker/DatePicker.js +7 -2
- package/dist/src/DateTimeSelection/shared/BasePicker/YearSelector/YearSelector.js +19 -5
- package/dist/src/DateTimeSelection/shared/CalendarLocale/DisplayJapaneseCalendarContext.d.ts +6 -0
- package/dist/src/DateTimeSelection/shared/CalendarLocale/DisplayJapaneseCalendarContext.js +10 -0
- package/dist/src/DateTimeSelection/shared/YearSelector/YearSelector.js +17 -4
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.d.ts +28 -0
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +55 -0
- package/dist/src/FormFooter/FormFooter.types.d.ts +2 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.d.ts +1 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +16 -5
- package/dist/src/MultipleSelectBox/MultipleSelectBox.types.d.ts +60 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useApplyControls.d.ts +1 -0
- package/dist/src/MultipleSelectBox/hooks/useApplyControls.js +16 -0
- package/dist/src/MultipleSelectBox/hooks/useCreateOption.d.ts +23 -0
- package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +53 -0
- package/dist/src/MultipleSelectBox/index.d.ts +1 -1
- package/dist/src/SidePane/SidePane.d.ts +3 -0
- package/dist/src/SidePane/SidePane.js +4 -2
- package/dist/src/SidePane/SidePane.types.d.ts +14 -0
- package/dist/src/SubNavigation/SubNavigation.d.ts +1 -1
- package/dist/src/SubNavigation/SubNavigation.js +17 -15
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +58 -0
- package/dist/src/SubNavigation/index.d.ts +1 -1
- package/dist/src/Tag/Tag.types.d.ts +17 -1
- package/dist/styled-system/recipes/form-footer-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/form-footer-slot-recipe.js +2 -1
- package/dist/styled-system/recipes/multiple-select-box-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/multiple-select-box-slot-recipe.js +12 -0
- package/dist/styled-system/recipes/side-pane-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/side-pane-slot-recipe.js +8 -0
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +4 -0
- package/dist/styles.css +82 -17
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -3,6 +3,8 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { BasePicker } from '../shared/BasePicker';
|
|
4
4
|
import { DatePickerPanel } from './DatePickerPanel';
|
|
5
5
|
import { CalendarLocaleProvider } from '../shared/CalendarLocale/CalendarLocaleContext';
|
|
6
|
+
import { DisplayJapaneseCalendarProvider } from '../shared/CalendarLocale/DisplayJapaneseCalendarContext';
|
|
7
|
+
import { dateToEnglishEraShort } from '../shared/utilities/japaneseCalendar';
|
|
6
8
|
/**
|
|
7
9
|
* DatePicker component
|
|
8
10
|
*
|
|
@@ -13,6 +15,9 @@ export function DatePicker({ format = 'YYYY/MM/DD', checkDisabledDate, minDate,
|
|
|
13
15
|
const customFormatValue = useMemo(() => {
|
|
14
16
|
if (!displayJapaneseCalendar)
|
|
15
17
|
return;
|
|
18
|
+
if (calendarLocale === 'en') {
|
|
19
|
+
return (date) => dateToEnglishEraShort(date);
|
|
20
|
+
}
|
|
16
21
|
const formatter = new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {
|
|
17
22
|
era: 'long',
|
|
18
23
|
year: 'numeric',
|
|
@@ -20,6 +25,6 @@ export function DatePicker({ format = 'YYYY/MM/DD', checkDisabledDate, minDate,
|
|
|
20
25
|
day: 'numeric',
|
|
21
26
|
});
|
|
22
27
|
return (date) => formatter.format(date);
|
|
23
|
-
}, [displayJapaneseCalendar]);
|
|
24
|
-
return (_jsx(BasePicker, { ...props, format: format, baseFormat: "YYYY-MM-DD", calendarLocale: calendarLocale, customFormatValue: customFormatValue, renderPopoverContent: ({ value, onChange }) => (_jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(DatePickerPanel, { value: value, checkDisabledDate: checkDisabledDate, minDate: minDate, maxDate: maxDate, prevMonthIconButtonProps: prevMonthIconButtonProps, nextMonthIconButtonProps: nextMonthIconButtonProps, todayButtonProps: todayButtonProps, onChange: onChange }) })) }));
|
|
28
|
+
}, [displayJapaneseCalendar, calendarLocale]);
|
|
29
|
+
return (_jsx(BasePicker, { ...props, format: format, baseFormat: "YYYY-MM-DD", calendarLocale: calendarLocale, customFormatValue: customFormatValue, renderPopoverContent: ({ value, onChange }) => (_jsx(DisplayJapaneseCalendarProvider, { value: displayJapaneseCalendar, children: _jsx(CalendarLocaleProvider, { value: calendarLocale, children: _jsx(DatePickerPanel, { value: value, checkDisabledDate: checkDisabledDate, minDate: minDate, maxDate: maxDate, prevMonthIconButtonProps: prevMonthIconButtonProps, nextMonthIconButtonProps: nextMonthIconButtonProps, todayButtonProps: todayButtonProps, onChange: onChange }) }) })) }));
|
|
25
30
|
}
|
|
@@ -4,13 +4,27 @@ import { SelectBox } from '../../../../SelectBox';
|
|
|
4
4
|
import { yearSelectorSlotRecipe } from '../../../../../styled-system/recipes';
|
|
5
5
|
import { MAXIMUM_VIEWABLE_YEAR, MINIMUM_VIEWABLE_YEAR } from '../constants';
|
|
6
6
|
import { cx } from '../../../../../styled-system/css';
|
|
7
|
+
import { useCalendarLocale } from '../../CalendarLocale/CalendarLocaleContext';
|
|
8
|
+
import { useDisplayJapaneseCalendar } from '../../CalendarLocale/DisplayJapaneseCalendarContext';
|
|
9
|
+
import { yearToEraLabel } from '../../utilities/japaneseCalendar';
|
|
7
10
|
export function YearSelector({ value, onChange, targetDOMNode }) {
|
|
11
|
+
const calendarLocale = useCalendarLocale();
|
|
12
|
+
const displayJapaneseCalendar = useDisplayJapaneseCalendar();
|
|
8
13
|
const internalYearString = useMemo(() => (value ?? new Date()).getFullYear().toString(), [value]);
|
|
9
|
-
const options = useMemo(() => Array.from({ length: MAXIMUM_VIEWABLE_YEAR - MINIMUM_VIEWABLE_YEAR + 1 }).map((_, i) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
const options = useMemo(() => Array.from({ length: MAXIMUM_VIEWABLE_YEAR - MINIMUM_VIEWABLE_YEAR + 1 }).map((_, i) => {
|
|
15
|
+
const year = MINIMUM_VIEWABLE_YEAR + i;
|
|
16
|
+
let label;
|
|
17
|
+
if (displayJapaneseCalendar) {
|
|
18
|
+
label = yearToEraLabel(year, calendarLocale ?? 'ja');
|
|
19
|
+
}
|
|
20
|
+
else if (calendarLocale === 'ja') {
|
|
21
|
+
label = `${String(year)}年`;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
label = String(year);
|
|
25
|
+
}
|
|
26
|
+
return { value: String(year), label };
|
|
27
|
+
}), [calendarLocale, displayJapaneseCalendar]);
|
|
14
28
|
const selectedOption = useMemo(() => options.find((o) => o.value === internalYearString), [options, internalYearString]);
|
|
15
29
|
const handleChange = useCallback((option) => {
|
|
16
30
|
if (option) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
const DisplayJapaneseCalendarContext = createContext(false);
|
|
5
|
+
export function DisplayJapaneseCalendarProvider({ value, children }) {
|
|
6
|
+
return _jsx(DisplayJapaneseCalendarContext.Provider, { value: value, children: children });
|
|
7
|
+
}
|
|
8
|
+
export function useDisplayJapaneseCalendar() {
|
|
9
|
+
return useContext(DisplayJapaneseCalendarContext);
|
|
10
|
+
}
|
|
@@ -5,13 +5,26 @@ import { yearSelectorSlotRecipe } from '../../../../styled-system/recipes';
|
|
|
5
5
|
import { MAXIMUM_VIEWABLE_YEAR, MINIMUM_VIEWABLE_YEAR } from './constants';
|
|
6
6
|
import { cx } from '../../../../styled-system/css';
|
|
7
7
|
import { useCalendarLocale } from '../CalendarLocale/CalendarLocaleContext';
|
|
8
|
+
import { useDisplayJapaneseCalendar } from '../CalendarLocale/DisplayJapaneseCalendarContext';
|
|
9
|
+
import { yearToEraLabel } from '../utilities/japaneseCalendar';
|
|
8
10
|
export function YearSelector({ value, onChange, triggerProps, triggerWrapperProps, targetDOMNode }) {
|
|
9
11
|
const calendarLocale = useCalendarLocale();
|
|
12
|
+
const displayJapaneseCalendar = useDisplayJapaneseCalendar();
|
|
10
13
|
const internalYearString = useMemo(() => (value ?? new Date()).getFullYear().toString(), [value]);
|
|
11
|
-
const options = useMemo(() => Array.from({ length: MAXIMUM_VIEWABLE_YEAR - MINIMUM_VIEWABLE_YEAR + 1 }).map((_, i) =>
|
|
12
|
-
|
|
13
|
-
label
|
|
14
|
-
|
|
14
|
+
const options = useMemo(() => Array.from({ length: MAXIMUM_VIEWABLE_YEAR - MINIMUM_VIEWABLE_YEAR + 1 }).map((_, i) => {
|
|
15
|
+
const year = MINIMUM_VIEWABLE_YEAR + i;
|
|
16
|
+
let label;
|
|
17
|
+
if (displayJapaneseCalendar) {
|
|
18
|
+
label = yearToEraLabel(year, calendarLocale ?? 'ja');
|
|
19
|
+
}
|
|
20
|
+
else if (calendarLocale === 'ja') {
|
|
21
|
+
label = `${String(year)}年`;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
label = String(year);
|
|
25
|
+
}
|
|
26
|
+
return { value: String(year), label };
|
|
27
|
+
}), [calendarLocale, displayJapaneseCalendar]);
|
|
15
28
|
const selectedOption = useMemo(() => options.find((o) => o.value === internalYearString), [options, internalYearString]);
|
|
16
29
|
const handleChange = useCallback((option) => {
|
|
17
30
|
if (option) {
|
|
@@ -44,5 +44,33 @@ export declare function warekiToDate(wareki: string): WarekiResult<Date>;
|
|
|
44
44
|
* @returns true if the string appears to be a Japanese era format
|
|
45
45
|
*/
|
|
46
46
|
export declare function isJapaneseEraFormat(inputString: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Converts a Western calendar year to an era label string for display in YearSelector.
|
|
49
|
+
*
|
|
50
|
+
* Uses December 31 to resolve era transition years to the later era:
|
|
51
|
+
* 2019 → 令和元年 (ja) / Reiwa 1 (en)
|
|
52
|
+
* 1989 → 平成元年 (ja) / Heisei 1 (en)
|
|
53
|
+
* 1926 → 昭和元年 (ja) / Showa 1 (en)
|
|
54
|
+
* 1912 → 大正元年 (ja) / Taisho 1 (en)
|
|
55
|
+
* Years before MEIJI_START_YEAR (< 1868): "1867年" (ja) / "1867" (en)
|
|
56
|
+
* First year of each era: "元年" suffix in Japanese (e.g., 令和元年, not 令和1年)
|
|
57
|
+
*
|
|
58
|
+
* @param year - Western calendar year (e.g., 2025)
|
|
59
|
+
*
|
|
60
|
+
* @param locale - 'ja' for Japanese labels, 'en' for romanized labels
|
|
61
|
+
*
|
|
62
|
+
* @returns Era label string (e.g., "令和7年", "令和元年", "Reiwa 7", "1867年", "1867")
|
|
63
|
+
*/
|
|
64
|
+
export declare function yearToEraLabel(year: number, locale: 'ja' | 'en'): string;
|
|
65
|
+
/**
|
|
66
|
+
* Formats a Date to a short English era string for display in the DatePicker trigger input.
|
|
67
|
+
* Format: "{EraName} {eraYear}/{month}/{day}" (e.g., "Reiwa 7/12/25").
|
|
68
|
+
* For pre-Meiji dates (year < 1868): falls back to the Gregorian year (e.g., "1867/12/25").
|
|
69
|
+
*
|
|
70
|
+
* @param date - The date to format
|
|
71
|
+
*
|
|
72
|
+
* @returns Short English era string (e.g., "Reiwa 7/12/25")
|
|
73
|
+
*/
|
|
74
|
+
export declare function dateToEnglishEraShort(date: Date): string;
|
|
47
75
|
export { warekiReg, dateToWareki, fullWidthToHalfWidth, selectGengo, WAREKI_START_YEARS };
|
|
48
76
|
export type { WarekiResult };
|
|
@@ -204,5 +204,60 @@ export function warekiToDate(wareki) {
|
|
|
204
204
|
export function isJapaneseEraFormat(inputString) {
|
|
205
205
|
return warekiReg.eraDetection.test(inputString);
|
|
206
206
|
}
|
|
207
|
+
const ERA_NAMES_EN = {
|
|
208
|
+
令和: 'Reiwa',
|
|
209
|
+
平成: 'Heisei',
|
|
210
|
+
昭和: 'Showa',
|
|
211
|
+
大正: 'Taisho',
|
|
212
|
+
明治: 'Meiji',
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Converts a Western calendar year to an era label string for display in YearSelector.
|
|
216
|
+
*
|
|
217
|
+
* Uses December 31 to resolve era transition years to the later era:
|
|
218
|
+
* 2019 → 令和元年 (ja) / Reiwa 1 (en)
|
|
219
|
+
* 1989 → 平成元年 (ja) / Heisei 1 (en)
|
|
220
|
+
* 1926 → 昭和元年 (ja) / Showa 1 (en)
|
|
221
|
+
* 1912 → 大正元年 (ja) / Taisho 1 (en)
|
|
222
|
+
* Years before MEIJI_START_YEAR (< 1868): "1867年" (ja) / "1867" (en)
|
|
223
|
+
* First year of each era: "元年" suffix in Japanese (e.g., 令和元年, not 令和1年)
|
|
224
|
+
*
|
|
225
|
+
* @param year - Western calendar year (e.g., 2025)
|
|
226
|
+
*
|
|
227
|
+
* @param locale - 'ja' for Japanese labels, 'en' for romanized labels
|
|
228
|
+
*
|
|
229
|
+
* @returns Era label string (e.g., "令和7年", "令和元年", "Reiwa 7", "1867年", "1867")
|
|
230
|
+
*/
|
|
231
|
+
export function yearToEraLabel(year, locale) {
|
|
232
|
+
if (year < MEIJI_START_YEAR) {
|
|
233
|
+
return locale === 'ja' ? `${String(year)}年` : String(year);
|
|
234
|
+
}
|
|
235
|
+
const gengo = selectGengo(year, 12, 31);
|
|
236
|
+
const eraYear = year - WAREKI_START_YEARS[gengo] + 1;
|
|
237
|
+
if (locale === 'en') {
|
|
238
|
+
return `${ERA_NAMES_EN[gengo]} ${String(eraYear)}`;
|
|
239
|
+
}
|
|
240
|
+
return `${gengo}${eraYear === 1 ? '元' : String(eraYear)}年`;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Formats a Date to a short English era string for display in the DatePicker trigger input.
|
|
244
|
+
* Format: "{EraName} {eraYear}/{month}/{day}" (e.g., "Reiwa 7/12/25").
|
|
245
|
+
* For pre-Meiji dates (year < 1868): falls back to the Gregorian year (e.g., "1867/12/25").
|
|
246
|
+
*
|
|
247
|
+
* @param date - The date to format
|
|
248
|
+
*
|
|
249
|
+
* @returns Short English era string (e.g., "Reiwa 7/12/25")
|
|
250
|
+
*/
|
|
251
|
+
export function dateToEnglishEraShort(date) {
|
|
252
|
+
const year = date.getFullYear();
|
|
253
|
+
const month = date.getMonth() + 1;
|
|
254
|
+
const day = date.getDate();
|
|
255
|
+
if (year < MEIJI_START_YEAR) {
|
|
256
|
+
return `${String(year)}/${String(month)}/${String(day)}`;
|
|
257
|
+
}
|
|
258
|
+
const gengo = selectGengo(year, month, day);
|
|
259
|
+
const eraYear = year - WAREKI_START_YEARS[gengo] + 1;
|
|
260
|
+
return `${ERA_NAMES_EN[gengo]} ${String(eraYear)}/${String(month)}/${String(day)}`;
|
|
261
|
+
}
|
|
207
262
|
// Export the utility functions and constants
|
|
208
263
|
export { warekiReg, dateToWareki, fullWidthToHalfWidth, selectGengo, WAREKI_START_YEARS };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
export type FormFooterPosition = 'fixed' | 'stacking';
|
|
2
|
+
export type FormFooterPosition = 'fixed' | 'stacking' | 'sticky';
|
|
3
3
|
export type FormFooterSectionPosition = 'fill' | 'center';
|
|
4
4
|
export type FormFooterProps = {
|
|
5
5
|
/**
|
|
@@ -11,6 +11,7 @@ export type FormFooterProps = {
|
|
|
11
11
|
*
|
|
12
12
|
* - `"stacking"`: Displayed inline at the bottom of its container. Use for creation forms where preventing mid-session abandonment is important.
|
|
13
13
|
* - `"fixed"`: Pinned to the bottom of the viewport at all times with a separator border. Use for edit screens where users return frequently.
|
|
14
|
+
* - `"sticky"`: Sits inline below the last form field, and pins to the bottom of the nearest scrolling ancestor (e.g. inside `SidePane`) when the form overflows. Use inside scrollable containers such as `SidePane`.
|
|
14
15
|
*
|
|
15
16
|
* @default "stacking"
|
|
16
17
|
*/
|
|
@@ -4,4 +4,4 @@ import { type MultipleSelectBoxProps, type AllowedValueTypes } from './MultipleS
|
|
|
4
4
|
* This component is separated from the SelectBox component because it has a different behavior.
|
|
5
5
|
* This component switches the variants of looks and behaviors depends on the props: size, invalid, disabled.
|
|
6
6
|
*/
|
|
7
|
-
export declare function MultipleSelectBox<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ id, triggerProps, triggerWrapperProps, size, options, defaultValue, placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions, notFoundMessage, searchBoxProps, loading, onSearchOptions, clearButtonProps, disableClearButton, optionPanelProps, popoverWrapperProps, enableAllOptionsControls, enableApplyControls, showSelectedCount, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount, onBlur, showGroupOptionDivider, enableVirtualization, virtualizationOptions, infiniteScroll, }: MultipleSelectBoxProps<T, AdditionalProps>): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function MultipleSelectBox<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ id, triggerProps, triggerWrapperProps, size, options, defaultValue, placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions, notFoundMessage, searchBoxProps, loading, onSearchOptions, clearButtonProps, disableClearButton, optionPanelProps, popoverWrapperProps, enableAllOptionsControls, enableApplyControls, showSelectedCount, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount, onBlur, showGroupOptionDivider, enableVirtualization, virtualizationOptions, infiniteScroll, enableCreateOption, onCreateOption, createOptionProps, }: MultipleSelectBoxProps<T, AdditionalProps>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useId, useMemo, useRef } from 'react';
|
|
4
|
-
import { CheckboxChecked, CheckboxUnchecked, Error } from '@moneyforward/mfui-icons-react';
|
|
4
|
+
import { Add, CheckboxChecked, CheckboxUnchecked, Error } from '@moneyforward/mfui-icons-react';
|
|
5
5
|
import { FocusIndicator } from '../FocusIndicator';
|
|
6
6
|
import { Typography } from '../Typography';
|
|
7
7
|
import { useFocusTrap } from '../utilities/dom/useFocusTrap';
|
|
@@ -18,8 +18,10 @@ import { useInitialFocusOnOptionPanelOpen } from './hooks/useInitialFocusOnOptio
|
|
|
18
18
|
import { useSelectedValues } from './hooks/useSelectedValues';
|
|
19
19
|
import { useOptionKeyboardNavigation } from './hooks/useOptionKeyboardNavigation';
|
|
20
20
|
import { useOptionSelectionManagement } from './hooks/useOptionSelectionManagement';
|
|
21
|
+
import { HelpMessage } from '../HelpMessage';
|
|
21
22
|
import { Popover } from '../Popover';
|
|
22
23
|
import { useApplyControls } from './hooks/useApplyControls';
|
|
24
|
+
import { useCreateOption } from './hooks/useCreateOption';
|
|
23
25
|
import { cx } from '../../styled-system/css';
|
|
24
26
|
import { flattenOptions } from './utils/flattenOptions';
|
|
25
27
|
import { isSelectableOption, isOptionDisabled, isOptionSelected } from './utils/isSelectableOption';
|
|
@@ -34,7 +36,7 @@ const SKELETON_ITEM_COUNT = 4;
|
|
|
34
36
|
*/
|
|
35
37
|
export function MultipleSelectBox({ id, triggerProps, triggerWrapperProps, size, options = [], defaultValue,
|
|
36
38
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
37
|
-
placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, popoverWrapperProps, enableAllOptionsControls = false, enableApplyControls = false, showSelectedCount = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, infiniteScroll, }) {
|
|
39
|
+
placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, popoverWrapperProps, enableAllOptionsControls = false, enableApplyControls = false, showSelectedCount = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, infiniteScroll, enableCreateOption = false, onCreateOption, createOptionProps, }) {
|
|
38
40
|
const classes = multipleSelectBoxSlotRecipe({ showGroupOptionDivider });
|
|
39
41
|
const triggerRef = useRef(null);
|
|
40
42
|
const listBoxId = useId();
|
|
@@ -57,7 +59,7 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
|
|
|
57
59
|
onToggle: (nextValue) => {
|
|
58
60
|
onOpenStateChanged?.(nextValue);
|
|
59
61
|
if (nextValue && enableApplyControls) {
|
|
60
|
-
|
|
62
|
+
openWithSelectedOptions(localSelectedOptions);
|
|
61
63
|
}
|
|
62
64
|
},
|
|
63
65
|
onClose: () => {
|
|
@@ -69,6 +71,13 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
|
|
|
69
71
|
},
|
|
70
72
|
});
|
|
71
73
|
const { searchText, filteredOptions, onSearchTextChange } = useSearchBox(flattenedOptions, enableSearchOptions, onSearchOptions);
|
|
74
|
+
const { isCreateOptionVisible, isCreating, createError, handleCreateOption } = useCreateOption({
|
|
75
|
+
enableCreateOption,
|
|
76
|
+
enableSearchOptions,
|
|
77
|
+
searchText,
|
|
78
|
+
filteredOptions,
|
|
79
|
+
onCreateOption,
|
|
80
|
+
});
|
|
72
81
|
// Determine if virtualization should be enabled
|
|
73
82
|
const shouldVirtualize = enableVirtualization && !loading && flattenedOptions.length > 0;
|
|
74
83
|
// Initialize virtualization
|
|
@@ -98,7 +107,7 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
|
|
|
98
107
|
});
|
|
99
108
|
// Disable infinite scroll when there's an error
|
|
100
109
|
const enableInfiniteScroll = baseEnabledInfiniteScroll && !infiniteScrollError;
|
|
101
|
-
const { temporaryValues, updateTemporaryValues, initializeTemporaryValues, handleCancelButtonClick, handleApplyButtonClick, } = useApplyControls({
|
|
110
|
+
const { temporaryValues, updateTemporaryValues, openWithSelectedOptions, initializeTemporaryValues, handleCancelButtonClick, handleApplyButtonClick, } = useApplyControls({
|
|
102
111
|
options: flattenedOptions,
|
|
103
112
|
onValuesChange: updateSelectedOptions,
|
|
104
113
|
closeOptionPanel,
|
|
@@ -328,7 +337,9 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
|
|
|
328
337
|
: emptyMessage }) }, "empty"),
|
|
329
338
|
renderInfiniteScrollError(),
|
|
330
339
|
].filter(Boolean) }) }));
|
|
340
|
+
const createOptionAreaNode = isCreateOptionVisible ? (_jsxs("div", { className: cx(classes.createOptionArea, 'mfui-MultipleSelectBox__createOptionArea'), children: [_jsxs("button", { "data-mfui-content": "create-new-option", type: "button", className: cx(classes.createOptionButton, 'mfui-MultipleSelectBox__createOptionButton'), disabled: isCreating, onClick: handleCreateOption, children: [_jsx(Add, { "aria-hidden": true }), createOptionProps?.renderLabel?.(searchText.trim()) ?? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "strongBody", children: searchText.trim() }), _jsx(Typography, { variant: "body", children: "\u3092\u8FFD\u52A0" })] }))] }), createError ? (_jsx("div", { className: cx(classes.createOptionErrorMessage, 'mfui-MultipleSelectBox__createOptionErrorMessage'), children: _jsx(HelpMessage, { messageType: "error", children: createOptionProps?.renderErrorMessage?.(searchText.trim()) ??
|
|
341
|
+
`${searchText.trim()}を追加できませんでした。` }) })) : null] })) : null;
|
|
331
342
|
return (_jsx(Popover, { renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsx(MultipleSelectBoxTrigger, { ref: triggerRef, wrapperRef: setTriggerRef, selectedOptions: localSelectedOptions, id: id, disabled: disabled, triggerProps: triggerProps, triggerWrapperProps: triggerWrapperProps, name: name,
|
|
332
343
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
333
|
-
placeholder: placeholder, size: size, listBoxId: listBoxId, isOptionPanelOpen: isOptionPanelOpen, invalid: invalid, showDisplayValueAsTag: showDisplayValueAsTag, renderDisplayValue: renderDisplayValue, clearButtonProps: clearButtonProps, disableClearButton: disableClearButton, updateSelectedValues: updateSelectedValues, onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur })), contentProps: { className: classes.popover }, minWidth: optionPanelProps?.minWidth, maxHeight: popoverWrapperProps?.maxHeight, allowedPlacements: optionPanelProps?.allowedPlacements, renderContent: () => (_jsxs("div", { ref: optionPanelRef, className: cx(classes.optionPanel, 'mfui-MultipleSelectBox__optionPanel', optionPanelProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: [enableSearchOptions || enableAllOptionsControls ? (_jsx(VStack, { className: cx(classes.menuHeader, 'mfui-MultipleSelectBox__menuHeader'), gap: "8px", children: renderPopoverHeader ? (renderPopoverHeader({ searchNode, allOptionsControlsNode })) : (_jsxs(_Fragment, { children: [searchNode, allOptionsControlsNode] })) })) : null, renderOptionList ? renderOptionList({ optionsNode }) : optionsNode, enableApplyControls || showSelectedCount ? (_jsx(HStack, { className: cx(classes.menuFooter, 'mfui-MultipleSelectBox__menuFooter'), justifyContent: "space-between", alignItems: "center", children: renderPopoverFooter ? (renderPopoverFooter({ selectedCountNode, applyControlsNode })) : (_jsxs(_Fragment, { children: [selectedCountNode, applyControlsNode] })) })) : null] })), open: isOptionPanelOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged: toggleOptionPanel, onBlur: onBlur }));
|
|
344
|
+
placeholder: placeholder, size: size, listBoxId: listBoxId, isOptionPanelOpen: isOptionPanelOpen, invalid: invalid, showDisplayValueAsTag: showDisplayValueAsTag, renderDisplayValue: renderDisplayValue, clearButtonProps: clearButtonProps, disableClearButton: disableClearButton, updateSelectedValues: updateSelectedValues, onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur })), contentProps: { className: classes.popover }, minWidth: optionPanelProps?.minWidth, maxHeight: popoverWrapperProps?.maxHeight, allowedPlacements: optionPanelProps?.allowedPlacements, renderContent: () => (_jsxs("div", { ref: optionPanelRef, className: cx(classes.optionPanel, 'mfui-MultipleSelectBox__optionPanel', optionPanelProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: [enableSearchOptions || enableAllOptionsControls ? (_jsx(VStack, { className: cx(classes.menuHeader, 'mfui-MultipleSelectBox__menuHeader'), gap: "8px", children: renderPopoverHeader ? (renderPopoverHeader({ searchNode, allOptionsControlsNode })) : (_jsxs(_Fragment, { children: [searchNode, allOptionsControlsNode] })) })) : null, renderOptionList ? (renderOptionList({ optionsNode, createOptionAreaNode })) : (_jsxs(_Fragment, { children: [optionsNode, createOptionAreaNode] })), enableApplyControls || showSelectedCount ? (_jsx(HStack, { className: cx(classes.menuFooter, 'mfui-MultipleSelectBox__menuFooter'), justifyContent: "space-between", alignItems: "center", children: renderPopoverFooter ? (renderPopoverFooter({ selectedCountNode, applyControlsNode })) : (_jsxs(_Fragment, { children: [selectedCountNode, applyControlsNode] })) })) : null] })), open: isOptionPanelOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged: toggleOptionPanel, onBlur: onBlur }));
|
|
334
345
|
}
|
|
@@ -164,6 +164,29 @@ export type ApplyButtonProps = {
|
|
|
164
164
|
*/
|
|
165
165
|
label?: string;
|
|
166
166
|
};
|
|
167
|
+
/**
|
|
168
|
+
* Properties for the "Create Option" feature
|
|
169
|
+
*/
|
|
170
|
+
export type CreateOptionProps = {
|
|
171
|
+
/**
|
|
172
|
+
* Custom render function for the create button label.
|
|
173
|
+
* Receives the current search text and should return a React node.
|
|
174
|
+
*
|
|
175
|
+
* @param searchText - The current trimmed search text
|
|
176
|
+
*
|
|
177
|
+
* @default (searchText) => <><strong>{searchText}</strong>を追加</>
|
|
178
|
+
*/
|
|
179
|
+
renderLabel?: (searchText: string) => React.ReactNode;
|
|
180
|
+
/**
|
|
181
|
+
* Custom render function for the error message when creation fails.
|
|
182
|
+
* Receives the search text that was being created.
|
|
183
|
+
*
|
|
184
|
+
* @param searchText - The trimmed search text that failed to be created
|
|
185
|
+
*
|
|
186
|
+
* @default (searchText) => `${searchText}を追加できませんでした。`
|
|
187
|
+
*/
|
|
188
|
+
renderErrorMessage?: (searchText: string) => React.ReactNode;
|
|
189
|
+
};
|
|
167
190
|
export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>> = {
|
|
168
191
|
/**
|
|
169
192
|
* The selectable options for the list
|
|
@@ -327,9 +350,10 @@ export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, Additio
|
|
|
327
350
|
* @example
|
|
328
351
|
* ```tsx
|
|
329
352
|
* <MultipleSelectBox
|
|
330
|
-
* renderOptionList={({ optionsNode }) => (
|
|
353
|
+
* renderOptionList={({ optionsNode, createOptionAreaNode }) => (
|
|
331
354
|
* <>
|
|
332
355
|
* {optionsNode}
|
|
356
|
+
* {createOptionAreaNode}
|
|
333
357
|
* <div>Custom content below the list</div>
|
|
334
358
|
* </>
|
|
335
359
|
* )}
|
|
@@ -338,6 +362,7 @@ export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, Additio
|
|
|
338
362
|
*/
|
|
339
363
|
renderOptionList?: (nodes: {
|
|
340
364
|
optionsNode: React.ReactNode;
|
|
365
|
+
createOptionAreaNode: React.ReactNode;
|
|
341
366
|
}) => React.ReactNode;
|
|
342
367
|
/**
|
|
343
368
|
* Customize the popover footer content.
|
|
@@ -505,5 +530,39 @@ export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, Additio
|
|
|
505
530
|
* ```
|
|
506
531
|
*/
|
|
507
532
|
infiniteScroll?: InfiniteScrollConfig;
|
|
533
|
+
/**
|
|
534
|
+
* Whether to enable the "Create New Option" feature.
|
|
535
|
+
* When enabled, a create button appears below the option list when the search text
|
|
536
|
+
* does not exactly match any existing option.
|
|
537
|
+
*
|
|
538
|
+
* Requires `enableSearchOptions` to be `true` to function.
|
|
539
|
+
*
|
|
540
|
+
* @default false
|
|
541
|
+
*/
|
|
542
|
+
enableCreateOption?: boolean;
|
|
543
|
+
/**
|
|
544
|
+
* Callback invoked when the user clicks the create button.
|
|
545
|
+
* The parent is responsible for adding the new option to the `options` list.
|
|
546
|
+
* If this callback throws or returns a rejected promise, an error message is displayed.
|
|
547
|
+
*
|
|
548
|
+
* @param text - The trimmed search text to create as a new option
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```tsx
|
|
552
|
+
* <MultipleSelectBox
|
|
553
|
+
* enableSearchOptions
|
|
554
|
+
* enableCreateOption
|
|
555
|
+
* onCreateOption={async (text) => {
|
|
556
|
+
* await api.createOption(text);
|
|
557
|
+
* setOptions(prev => [...prev, { label: text, value: text }]);
|
|
558
|
+
* }}
|
|
559
|
+
* />
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
onCreateOption?: (text: string) => Promise<void> | void;
|
|
563
|
+
/**
|
|
564
|
+
* Properties for customizing the "Create Option" button and error message.
|
|
565
|
+
*/
|
|
566
|
+
createOptionProps?: CreateOptionProps;
|
|
508
567
|
} & Pick<MultipleSelectBoxTriggerProps<T, AdditionalProps>, keyof PassedProps<T, AdditionalProps>>;
|
|
509
568
|
export { type InfiniteScrollDirection } from '../utilities/dom/useInfiniteScroll';
|
|
@@ -33,7 +33,7 @@ placeholder, renderDisplayValue, updateSelectedValues, clearButtonProps, disable
|
|
|
33
33
|
return (_jsx(Typography, { variant: textVariant, className: cx(classes.placeholder, 'mfui-MultipleSelectBoxTrigger__placeholder'), "data-mfui-content": "multiple-select-box-trigger-placeholder", children: placeholder }));
|
|
34
34
|
}
|
|
35
35
|
if (showDisplayValueAsTag) {
|
|
36
|
-
return (_jsx("div", { className: cx(classes.tagList, 'mfui-MultipleSelectBoxTrigger__tagList'), "data-mfui-content": "multiple-select-box-trigger-selected-options", children: selectedOptions.map((option, index) => (_jsx(Tag, { className: cx(classes.tagItem, 'mfui-MultipleSelectBoxTrigger__tagItem'), label: option.label, onClose: (event) => {
|
|
36
|
+
return (_jsx("div", { className: cx(classes.tagList, 'mfui-MultipleSelectBoxTrigger__tagList'), "data-mfui-content": "multiple-select-box-trigger-selected-options", children: selectedOptions.map((option, index) => (_jsx(Tag, { className: cx(classes.tagItem, 'mfui-MultipleSelectBoxTrigger__tagItem'), label: option.label, disabled: disabled, onClose: (event) => {
|
|
37
37
|
event.stopPropagation();
|
|
38
38
|
onDeselectTag(option.value);
|
|
39
39
|
} }, option.value ?? `tag-${option.label}-${String(index)}`))) }));
|
|
@@ -23,6 +23,7 @@ type UseApplyControlsProps<T extends AllowedValueTypes = string, AdditionalProps
|
|
|
23
23
|
export declare function useApplyControls<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ options, onValuesChange, closeOptionPanel }: UseApplyControlsProps<T, AdditionalProps>): {
|
|
24
24
|
temporaryValues: Set<MultipleSelectBoxOption<T, AdditionalProps>["value"]>;
|
|
25
25
|
updateTemporaryValues: (values: MultipleSelectBoxOption<T, AdditionalProps>["value"][]) => void;
|
|
26
|
+
openWithSelectedOptions: (selectedOptions: MultipleSelectBoxOption<T, AdditionalProps>[]) => void;
|
|
26
27
|
initializeTemporaryValues: () => void;
|
|
27
28
|
handleCancelButtonClick: () => void;
|
|
28
29
|
handleApplyButtonClick: () => void;
|
|
@@ -33,6 +33,21 @@ export function useApplyControls({ options, onValuesChange, closeOptionPanel })
|
|
|
33
33
|
return newMap;
|
|
34
34
|
});
|
|
35
35
|
}, [options]);
|
|
36
|
+
// Initialize temporary state from full option objects when the panel opens.
|
|
37
|
+
// This must receive full option objects (not just value keys) so that previously-applied
|
|
38
|
+
// options are preserved even when they are not in the currently-visible filtered list.
|
|
39
|
+
const openWithSelectedOptions = useCallback((selectedOptions) => {
|
|
40
|
+
const newMap = new Map();
|
|
41
|
+
const newValues = new Set();
|
|
42
|
+
for (const option of selectedOptions) {
|
|
43
|
+
if (option.value != null) {
|
|
44
|
+
newMap.set(option.value, option);
|
|
45
|
+
newValues.add(option.value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setTemporaryValues(newValues);
|
|
49
|
+
setTemporaryOptionsMap(newMap);
|
|
50
|
+
}, []);
|
|
36
51
|
// Initialize temporary selection state (called when the panel closes without applying)
|
|
37
52
|
const initializeTemporaryValues = useCallback(() => {
|
|
38
53
|
setTemporaryValues(new Set());
|
|
@@ -52,6 +67,7 @@ export function useApplyControls({ options, onValuesChange, closeOptionPanel })
|
|
|
52
67
|
return {
|
|
53
68
|
temporaryValues,
|
|
54
69
|
updateTemporaryValues,
|
|
70
|
+
openWithSelectedOptions,
|
|
55
71
|
initializeTemporaryValues,
|
|
56
72
|
handleCancelButtonClick,
|
|
57
73
|
handleApplyButtonClick,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type MultipleSelectBoxOption, type AllowedValueTypes } from '../MultipleSelectBox.types';
|
|
2
|
+
type UseCreateOptionProps<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>> = {
|
|
3
|
+
enableCreateOption: boolean;
|
|
4
|
+
enableSearchOptions: boolean;
|
|
5
|
+
searchText: string;
|
|
6
|
+
filteredOptions: MultipleSelectBoxOption<T, AdditionalProps>[];
|
|
7
|
+
onCreateOption?: (text: string) => Promise<void> | void;
|
|
8
|
+
};
|
|
9
|
+
type UseCreateOptionReturn = {
|
|
10
|
+
isCreateOptionVisible: boolean;
|
|
11
|
+
isCreating: boolean;
|
|
12
|
+
createError: boolean;
|
|
13
|
+
handleCreateOption: () => void;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Custom hook to manage the "Create New Option" feature for MultipleSelectBox.
|
|
17
|
+
*
|
|
18
|
+
* Handles visibility logic (shown only when search text has no exact match),
|
|
19
|
+
* loading state while the async callback is in flight, and error state when
|
|
20
|
+
* the callback rejects.
|
|
21
|
+
*/
|
|
22
|
+
export declare const useCreateOption: <T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ enableCreateOption, enableSearchOptions, searchText, filteredOptions, onCreateOption, }: UseCreateOptionProps<T, AdditionalProps>) => UseCreateOptionReturn;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { isSelectableOption } from '../utils/isSelectableOption';
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook to manage the "Create New Option" feature for MultipleSelectBox.
|
|
6
|
+
*
|
|
7
|
+
* Handles visibility logic (shown only when search text has no exact match),
|
|
8
|
+
* loading state while the async callback is in flight, and error state when
|
|
9
|
+
* the callback rejects.
|
|
10
|
+
*/
|
|
11
|
+
export const useCreateOption = ({ enableCreateOption, enableSearchOptions, searchText, filteredOptions, onCreateOption, }) => {
|
|
12
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
13
|
+
const [createError, setCreateError] = useState(false);
|
|
14
|
+
const trimmedSearchText = searchText.trim();
|
|
15
|
+
const isCreateOptionVisible = useMemo(() => {
|
|
16
|
+
if (!enableCreateOption || !enableSearchOptions)
|
|
17
|
+
return false;
|
|
18
|
+
if (!trimmedSearchText)
|
|
19
|
+
return false;
|
|
20
|
+
const exactMatchExists = filteredOptions.some((option) => isSelectableOption(option) && option.label === trimmedSearchText);
|
|
21
|
+
return !exactMatchExists;
|
|
22
|
+
}, [enableCreateOption, enableSearchOptions, trimmedSearchText, filteredOptions]);
|
|
23
|
+
// Clear error state when search text changes
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setCreateError(false);
|
|
26
|
+
}, [searchText]);
|
|
27
|
+
const doCreate = useCallback(async () => {
|
|
28
|
+
try {
|
|
29
|
+
await onCreateOption?.(trimmedSearchText);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
setCreateError(true);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
setIsCreating(false);
|
|
36
|
+
}
|
|
37
|
+
}, [onCreateOption, trimmedSearchText]);
|
|
38
|
+
const handleCreateOption = useCallback(() => {
|
|
39
|
+
if (!onCreateOption || isCreating)
|
|
40
|
+
return;
|
|
41
|
+
setCreateError(false);
|
|
42
|
+
setIsCreating(true);
|
|
43
|
+
doCreate().catch(() => {
|
|
44
|
+
// Error handling is done within doCreate
|
|
45
|
+
});
|
|
46
|
+
}, [onCreateOption, isCreating, doCreate]);
|
|
47
|
+
return {
|
|
48
|
+
isCreateOptionVisible,
|
|
49
|
+
isCreating,
|
|
50
|
+
createError,
|
|
51
|
+
handleCreateOption,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { MultipleSelectBox } from './MultipleSelectBox';
|
|
2
|
-
export type { MultipleSelectBoxProps, MultipleSelectBoxOption, InfiniteScrollDirection, } from './MultipleSelectBox.types';
|
|
2
|
+
export type { MultipleSelectBoxProps, MultipleSelectBoxOption, CreateOptionProps, InfiniteScrollDirection, } from './MultipleSelectBox.types';
|
|
@@ -17,6 +17,9 @@ export declare const SidePane: import("react").ForwardRefExoticComponent<{
|
|
|
17
17
|
enableModal?: boolean;
|
|
18
18
|
disableBackdropClose?: boolean;
|
|
19
19
|
enableAutoUnmount?: boolean;
|
|
20
|
+
formFooterProps?: Pick<import("..").FormFooterProps, "optionsSlot" | "actionsSlot"> & {
|
|
21
|
+
position?: Exclude<import("..").FormFooterProps["position"], "fixed">;
|
|
22
|
+
};
|
|
20
23
|
insideProps?: {
|
|
21
24
|
className?: string;
|
|
22
25
|
};
|
|
@@ -10,13 +10,14 @@ import { useSidePaneStateController } from './hooks/useSidePaneStateController';
|
|
|
10
10
|
import { useFocusTrap } from '../utilities/dom/useFocusTrap';
|
|
11
11
|
import { compatibleForwardRef } from '../utilities/react/compatibleForwardRef';
|
|
12
12
|
import { Portal, TargetDomNodeProvider, useAutomaticTargetDomNode } from '../Portal';
|
|
13
|
+
import { FormFooter } from '../FormFooter';
|
|
13
14
|
/**
|
|
14
15
|
* The general purpose SidePane component.
|
|
15
16
|
* This component extends the props of `<div>` element with `role="dialog"`.
|
|
16
17
|
*
|
|
17
18
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
|
|
18
19
|
*/
|
|
19
|
-
export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onClose, className, children, actionSlot, position = 'right', enableModal = false, enableAutoUnmount = true, disableBackdropClose = false, closeButtonProps, targetDOMNode, insideProps, ...props }, ref) => {
|
|
20
|
+
export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onClose, className, children, actionSlot, formFooterProps, position = 'right', enableModal = false, enableAutoUnmount = true, disableBackdropClose = false, closeButtonProps, targetDOMNode, insideProps, ...props }, ref) => {
|
|
20
21
|
const classes = sidePaneSlotRecipe({ position, nonModal: !enableModal });
|
|
21
22
|
const { localOpen, handleCloseSidePane, handleStopCloseSidePane, sidePaneElement, setSidePaneElement } = useSidePaneStateController({
|
|
22
23
|
open,
|
|
@@ -53,9 +54,10 @@ export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onCl
|
|
|
53
54
|
}, [closeButtonProps?.isCollapseIcon, position]);
|
|
54
55
|
const targetElement = useAutomaticTargetDomNode(targetDOMNode, sidePaneElement);
|
|
55
56
|
const shouldRenderContent = enableAutoUnmount ? localOpen : true;
|
|
57
|
+
const resolvedFormFooterPosition = formFooterProps?.position ?? 'sticky';
|
|
56
58
|
const headingNode = title ? (_jsx(Heading, { variant: "pageHeading2", tag: "h2", children: title })) : null;
|
|
57
59
|
return (_jsx(TargetDomNodeProvider, { targetDomNode: sidePaneElement, children: shouldRenderContent ? (_jsx(Portal, { targetDOMNode: targetElement, children: _jsxs("div", { ref: setSidePaneElement, role: "dialog", ...props, className: cx(classes.root, 'mfui-SidePane__root', className), tabIndex: -1, style: {
|
|
58
60
|
...props.style,
|
|
59
61
|
...(!enableAutoUnmount && !localOpen && { display: 'none' }),
|
|
60
|
-
}, onKeyDown: handleOnKeyDown, children: [enableModal ? (_jsx("div", { "data-mfui-content": "backdrop", className: cx(classes.backdrop, 'mfui-SidePane__backdrop'), onClick: handleBackdropClick })) : null, _jsxs("div", { "data-mfui-content": "inside", className: cx(classes.inside, 'mfui-SidePane__inside', insideProps?.className), onClick: handleStopCloseSidePane, children: [_jsxs("header", { className: cx(classes.header, 'mfui-SidePane__header'), children: [renderHeading ? (renderHeading({ headingNode })) : (_jsx("div", { className: cx(classes.title, 'mfui-SidePane__title'), children: headingNode })), _jsxs("div", { className: cx(classes.actionSlotWrapper, 'mfui-SidePane__actionSlotWrapper'), children: [!!actionSlot && (_jsx("div", { className: cx(classes.actionSlot, 'mfui-SidePane__actionSlot'), children: actionSlot })), _jsx("div", { className: cx(classes.closeButtonWrapper, 'mfui-SidePane__closeButtonWrapper'), children: _jsx(IconButton, { "aria-label": closeButtonProps?.['aria-label'] ?? '閉じる', onClick: handleCloseSidePane, children: closeButtonIcon }) })] })] }),
|
|
62
|
+
}, onKeyDown: handleOnKeyDown, children: [enableModal ? (_jsx("div", { "data-mfui-content": "backdrop", className: cx(classes.backdrop, 'mfui-SidePane__backdrop'), onClick: handleBackdropClick })) : null, _jsxs("div", { "data-mfui-content": "inside", className: cx(classes.inside, 'mfui-SidePane__inside', insideProps?.className), onClick: handleStopCloseSidePane, children: [_jsxs("header", { className: cx(classes.header, 'mfui-SidePane__header'), children: [renderHeading ? (renderHeading({ headingNode })) : (_jsx("div", { className: cx(classes.title, 'mfui-SidePane__title'), children: headingNode })), _jsxs("div", { className: cx(classes.actionSlotWrapper, 'mfui-SidePane__actionSlotWrapper'), children: [!!actionSlot && (_jsx("div", { className: cx(classes.actionSlot, 'mfui-SidePane__actionSlot'), children: actionSlot })), _jsx("div", { className: cx(classes.closeButtonWrapper, 'mfui-SidePane__closeButtonWrapper'), children: _jsx(IconButton, { "aria-label": closeButtonProps?.['aria-label'] ?? '閉じる', onClick: handleCloseSidePane, children: closeButtonIcon }) })] })] }), _jsxs("main", { className: cx(classes.content, 'mfui-SidePane__content'), children: [_jsx("div", { className: cx(classes.contentBody, 'mfui-SidePane__contentBody'), children: children }), resolvedFormFooterPosition === 'stacking' ? (_jsx("div", { className: cx(classes.footer, 'mfui-SidePane__footer'), children: _jsx(FormFooter, { sectionPosition: "fill", ...formFooterProps, position: resolvedFormFooterPosition }) })) : null] }), formFooterProps && resolvedFormFooterPosition !== 'stacking' ? (_jsx("div", { className: cx(classes.footer, 'mfui-SidePane__footer'), children: _jsx(FormFooter, { sectionPosition: "fill", ...formFooterProps, position: resolvedFormFooterPosition }) })) : null] })] }) })) : null }));
|
|
61
63
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
import { type SidePaneSlotRecipeVariant } from '../../styled-system/recipes';
|
|
3
|
+
import { type FormFooterProps } from '../FormFooter';
|
|
3
4
|
export type OnCloseFunction = () => void;
|
|
4
5
|
export type SidePaneRenderOptions = {
|
|
5
6
|
/**
|
|
@@ -109,6 +110,19 @@ export type SidePaneProps = {
|
|
|
109
110
|
* @default true
|
|
110
111
|
*/
|
|
111
112
|
enableAutoUnmount?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Props to render a `FormFooter` inside the SidePane.
|
|
115
|
+
*
|
|
116
|
+
* - `position: 'sticky'` (default): the footer is always pinned to the pane bottom, outside the scrollable content area.
|
|
117
|
+
* - `position: 'stacking'`: the footer flows after the content inside the scroll area.
|
|
118
|
+
*
|
|
119
|
+
* `position: 'fixed'` is not supported here because it anchors to the viewport rather than the pane.
|
|
120
|
+
*
|
|
121
|
+
* @default undefined
|
|
122
|
+
*/
|
|
123
|
+
formFooterProps?: Pick<FormFooterProps, 'optionsSlot' | 'actionsSlot'> & {
|
|
124
|
+
position?: Exclude<FormFooterProps['position'], 'fixed'>;
|
|
125
|
+
};
|
|
112
126
|
/**
|
|
113
127
|
* Additional props to apply to the inside container element of the SidePane.
|
|
114
128
|
* This element wraps the header and content sections.
|
|
@@ -2,4 +2,4 @@ import { type SubNavigationProps } from './SubNavigation.types';
|
|
|
2
2
|
/**
|
|
3
3
|
* This component is for the navigation links under the main navigation.
|
|
4
4
|
*/
|
|
5
|
-
export declare function SubNavigation({ orientation, navigationItems, customLinkComponent, className, }: SubNavigationProps): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare function SubNavigation({ orientation, navigationItems, customLinkComponent, renderNavigationLabel, headerSlot, className, }: SubNavigationProps): import("react/jsx-runtime").JSX.Element;
|