@moneyforward/mfui-components 3.30.0 → 3.32.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.
Files changed (42) hide show
  1. package/dist/src/Breadcrumbs/Breadcrumbs.d.ts +1 -0
  2. package/dist/src/Breadcrumbs/Breadcrumbs.js +2 -2
  3. package/dist/src/Breadcrumbs/Breadcrumbs.types.d.ts +7 -0
  4. package/dist/src/DataTable/DataTable.js +2 -2
  5. package/dist/src/DataTable/DataTable.types.d.ts +9 -0
  6. package/dist/src/DataTable/DataTableCell/DataTableCell.js +2 -1
  7. package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +2 -1
  8. package/dist/src/DataTable/DataTableProvider.d.ts +2 -2
  9. package/dist/src/DataTable/DataTableProvider.js +4 -1
  10. package/dist/src/DataTable/DataTableRow/DataTableRow.js +34 -3
  11. package/dist/src/DateTimeSelection/shared/BasePicker/constants.d.ts +1 -1
  12. package/dist/src/DateTimeSelection/shared/BasePicker/constants.js +2 -0
  13. package/dist/src/DateTimeSelection/shared/YearSelector/YearSelector.js +7 -1
  14. package/dist/src/DropdownMenu/DropdownMenu.js +26 -2
  15. package/dist/src/DropdownMenu/DropdownMenuButtonTrigger.d.ts +2 -2
  16. package/dist/src/DropdownMenu/DropdownMenuContainer/DropdownMenuContainer.js +28 -3
  17. package/dist/src/DropdownMenu/DropdownMenuContainer/DropdownMenuContainer.types.d.ts +5 -2
  18. package/dist/src/DropdownMenu/DropdownMenuItem/DropdownMenuItem.d.ts +1 -1
  19. package/dist/src/DropdownMenu/DropdownMenuItem/DropdownMenuItem.js +18 -3
  20. package/dist/src/DropdownMenu/DropdownMenuItem/DropdownMenuItem.types.d.ts +6 -0
  21. package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
  22. package/dist/src/MainNavigation/BaseMainNavigation.js +4 -2
  23. package/dist/src/MainNavigation/MainNavigation.js +2 -2
  24. package/dist/src/MainNavigation/MainNavigation.types.d.ts +18 -0
  25. package/dist/src/NumberBox/NumberBox.js +68 -21
  26. package/dist/src/SearchBox/SearchBox.js +18 -4
  27. package/dist/src/SidePane/SidePane.d.ts +3 -1
  28. package/dist/src/SidePane/SidePane.js +8 -4
  29. package/dist/src/SidePane/hooks/useSidePaneStateController.d.ts +7 -2
  30. package/dist/src/SidePane/hooks/useSidePaneStateController.js +11 -5
  31. package/dist/src/Tag/Tag.d.ts +1 -1
  32. package/dist/src/Tag/Tag.js +2 -2
  33. package/dist/src/Tag/Tag.types.d.ts +7 -0
  34. package/dist/src/Tooltip/hooks/usePopoverController.js +29 -14
  35. package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -0
  36. package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +4 -0
  37. package/dist/styled-system/recipes/data-table-header-cell-slot-recipe.d.ts +1 -0
  38. package/dist/styled-system/recipes/data-table-header-cell-slot-recipe.js +4 -0
  39. package/dist/styled-system/recipes/dropdown-menu-item-slot-recipe.d.ts +1 -1
  40. package/dist/styled-system/recipes/dropdown-menu-item-slot-recipe.js +12 -0
  41. package/dist/styles.css +76 -12
  42. package/package.json +2 -2
@@ -1,15 +1,19 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { forwardRef, useCallback, useLayoutEffect, useRef, useState, useEffect } from 'react';
3
+ import { forwardRef, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
4
4
  import { cx } from '../../styled-system/css';
5
5
  import { mergeRefs } from '../utilities/dom/mergeRefs';
6
6
  import { TextBox } from '../TextBox';
7
7
  /**
8
- * Convert full-width digits, minus sign, and decimal point to half-width.
8
+ * Convert full-width digits, minus signs, and decimal points to half-width.
9
+ * Kana-mode IMEs can emit prolonged sound marks for the minus key.
9
10
  *
10
11
  * @param value - The string to convert.
11
12
  */
12
- const toHalfWidth = (value) => value.replaceAll(/[\uFF10-\uFF19\uFF0D\uFF0E]/g, (char) => String.fromCodePoint((char.codePointAt(0) ?? 0) - 0xfe_e0));
13
+ const toHalfWidth = (value) => value
14
+ .replaceAll(/[\uFF10-\uFF19\uFF0D\uFF0E]/g, (char) => String.fromCodePoint((char.codePointAt(0) ?? 0) - 0xfe_e0))
15
+ .replaceAll(/[\u3002\uFF61]/g, '.')
16
+ .replaceAll(/[\u2212\u30FC\uFF70\u2015]/g, '-');
13
17
  /**
14
18
  * Sanitize input: convert full-width characters, then keep only valid numeric characters.
15
19
  * Allows: digits 0-9, a single leading minus, a single decimal point after a digit.
@@ -69,6 +73,35 @@ const formatWithCommas = (raw) => {
69
73
  * @param display - The formatted display string.
70
74
  */
71
75
  const stripFormatting = (display) => display.replaceAll(',', '');
76
+ /**
77
+ * Map a display cursor position to the equivalent index in the raw numeric string.
78
+ *
79
+ * @param display - The formatted display string.
80
+ * @param displayCursor - The cursor position in the display string.
81
+ */
82
+ const mapDisplayCursorToRaw = (display, displayCursor) => {
83
+ const commasBefore = (display.slice(0, displayCursor).match(/,/g) ?? []).length;
84
+ return displayCursor - commasBefore;
85
+ };
86
+ /**
87
+ * Map a raw numeric cursor position to the equivalent index in the formatted display string.
88
+ *
89
+ * @param formatted - The formatted display string.
90
+ * @param rawCursor - The cursor position in the raw numeric string.
91
+ */
92
+ const mapRawCursorToDisplay = (formatted, rawCursor) => {
93
+ let rawCount = 0;
94
+ let index = 0;
95
+ for (const char of formatted) {
96
+ if (rawCount >= rawCursor) {
97
+ return index;
98
+ }
99
+ if (char !== ',')
100
+ rawCount += 1;
101
+ index += 1;
102
+ }
103
+ return formatted.length;
104
+ };
72
105
  /**
73
106
  * NumberBox is a numeric input component that wraps TextBox.
74
107
  * It provides real-time comma formatting, full-width digit conversion,
@@ -79,6 +112,7 @@ const stripFormatting = (display) => display.replaceAll(',', '');
79
112
  export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign = 'right', style, enableClearButton, ...rest }, ref) => {
80
113
  const inputRef = useRef(null);
81
114
  const pendingCursorRef = useRef(null);
115
+ const [cursorRestoreToken, requestCursorRestore] = useReducer((count) => count + 1, 0);
82
116
  const [displayValue, setDisplayValue] = useState(() => formatWithCommas(sanitizeInput(String(value ?? defaultValue ?? ''))));
83
117
  // Sync with controlled value
84
118
  useEffect(() => {
@@ -89,16 +123,22 @@ export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign
89
123
  // Restore cursor position after display value update
90
124
  useLayoutEffect(() => {
91
125
  if (pendingCursorRef.current !== null && inputRef.current) {
92
- inputRef.current.setSelectionRange(pendingCursorRef.current, pendingCursorRef.current);
126
+ const input = inputRef.current;
127
+ const cursor = pendingCursorRef.current;
93
128
  pendingCursorRef.current = null;
129
+ if (input.value !== displayValue) {
130
+ input.value = displayValue;
131
+ }
132
+ input.setSelectionRange(cursor, cursor);
94
133
  }
95
- }, [displayValue]);
134
+ }, [displayValue, cursorRestoreToken]);
96
135
  const handleChange = useCallback((event) => {
136
+ const inputDisplay = event.target.value;
97
137
  const cursorPos = event.target.selectionStart ?? 0;
98
- const rawInput = stripFormatting(toHalfWidth(event.target.value));
99
- // Calculate cursor position accounting for comma changes
100
- const commasBefore = (displayValue.slice(0, cursorPos).match(/,/g) ?? []).length;
101
- let rawCursor = cursorPos - commasBefore;
138
+ const prevSanitized = stripFormatting(displayValue);
139
+ const rawInput = stripFormatting(toHalfWidth(inputDisplay));
140
+ // Use the current input value so comma positions match the cursor.
141
+ let rawCursor = mapDisplayCursorToRaw(inputDisplay, cursorPos);
102
142
  // When two dots appear, the user is moving the decimal point.
103
143
  // Keep the newly typed dot (at rawCursor - 1) and remove the old one.
104
144
  let processedRawInput = rawInput;
@@ -114,22 +154,29 @@ export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign
114
154
  rawCursor -= 1;
115
155
  }
116
156
  }
117
- const sanitized = sanitizeInput(processedRawInput);
118
- const formatted = formatWithCommas(sanitized);
119
- let newCursor = formatted.length;
120
- let rawCount = 0;
121
- let index = 0;
122
- for (const char of formatted) {
123
- if (rawCount >= rawCursor) {
124
- newCursor = index;
125
- break;
157
+ let sanitized = sanitizeInput(processedRawInput);
158
+ // Backspace right after a comma deletes the digit before the comma.
159
+ const isBackspaceOnComma = sanitized === prevSanitized && cursorPos < displayValue.length && displayValue[cursorPos] === ',';
160
+ if (isBackspaceOnComma) {
161
+ const rawPos = mapDisplayCursorToRaw(displayValue, cursorPos);
162
+ if (rawPos > 0) {
163
+ sanitized = prevSanitized.slice(0, rawPos - 1) + prevSanitized.slice(rawPos);
164
+ rawCursor = rawPos - 1;
126
165
  }
127
- if (char !== ',')
128
- rawCount += 1;
129
- index += 1;
166
+ }
167
+ const formatted = formatWithCommas(sanitized);
168
+ let newCursor = mapRawCursorToDisplay(formatted, rawCursor);
169
+ // Rejected characters keep the cursor before the ignored input.
170
+ const rejectedCharCount = rawInput.length - sanitized.length;
171
+ if (sanitized === prevSanitized && !isBackspaceOnComma && rejectedCharCount > 0) {
172
+ const rawBeforeCursor = mapDisplayCursorToRaw(inputDisplay, cursorPos);
173
+ newCursor = mapRawCursorToDisplay(formatted, rawBeforeCursor - rejectedCharCount);
130
174
  }
131
175
  pendingCursorRef.current = newCursor;
132
176
  setDisplayValue(formatted);
177
+ if (formatted === displayValue) {
178
+ requestCursorRestore();
179
+ }
133
180
  onChange?.(sanitized);
134
181
  }, [displayValue, onChange]);
135
182
  return (_jsx(TextBox, { ref: mergeRefs(ref, inputRef), ...rest, className: cx('mfui-NumberBox__root', rest.className), enableClearButton: Boolean(enableClearButton) && displayValue !== '', style: { textAlign, ...style }, value: displayValue, onChange: handleChange }));
@@ -14,10 +14,24 @@ import { normalizeFullWidth } from '../utilities/string/normalizeFullWidth';
14
14
  export const SearchBox = forwardRef(({ className, normalizeFullWidth: shouldNormalizeFullWidth = false, onChange, ...rest }, ref) => {
15
15
  const handleChange = useCallback((event) => {
16
16
  if (shouldNormalizeFullWidth) {
17
- // Rewrite the native input so the displayed value reflects the normalization
18
- // and the consumer's onChange sees the canonical half-width / composed-kana form.
19
- // eslint-disable-next-line no-param-reassign
20
- event.target.value = normalizeFullWidth(event.target.value);
17
+ const { value, selectionStart } = event.target;
18
+ const normalizedValue = normalizeFullWidth(value);
19
+ // Only rewrite the native input when normalization actually changes the value.
20
+ // Assigning to `value` resets the caret to the end even when the string is
21
+ // unchanged, so skipping the no-op assignment keeps the caret in place while
22
+ // the user edits mid-string.
23
+ if (normalizedValue !== value) {
24
+ // Map the caret by normalizing only the text before it. This keeps the caret
25
+ // correct even when normalization changes the string length (e.g. `パ` -> `パ`).
26
+ const caretPosition = selectionStart === null
27
+ ? normalizedValue.length
28
+ : normalizeFullWidth(value.slice(0, selectionStart)).length;
29
+ // Rewrite the native input so the displayed value reflects the normalization
30
+ // and the consumer's onChange sees the canonical half-width / composed-kana form.
31
+ // eslint-disable-next-line no-param-reassign
32
+ event.target.value = normalizedValue;
33
+ event.target.setSelectionRange(caretPosition, caretPosition);
34
+ }
21
35
  }
22
36
  onChange?.(event);
23
37
  }, [shouldNormalizeFullWidth, onChange]);
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * The general purpose SidePane component.
3
- * This component extends the props of `<div>` element with `role="dialog"`.
3
+ * This component extends the props of `<div>` element. The extended props and `ref` are
4
+ * applied to the root overlay element covering the viewport, while `role="dialog"` (and
5
+ * `aria-modal="true"` when `enableModal` is set) are set on the inner pane window element.
4
6
  *
5
7
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
6
8
  */
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useMemo } from 'react';
3
+ import { useCallback, useMemo, useState } from 'react';
4
4
  import { Close, PaneExpansionToRight, PaneExpansionToLeft } from '@moneyforward/mfui-icons-react';
5
5
  import { cx } from '../../styled-system/css';
6
6
  import { Heading } from '../Heading';
@@ -13,16 +13,20 @@ import { Portal, TargetDomNodeProvider, useAutomaticTargetDomNode } from '../Por
13
13
  import { FormFooter } from '../FormFooter';
14
14
  /**
15
15
  * The general purpose SidePane component.
16
- * This component extends the props of `<div>` element with `role="dialog"`.
16
+ * This component extends the props of `<div>` element. The extended props and `ref` are
17
+ * applied to the root overlay element covering the viewport, while `role="dialog"` (and
18
+ * `aria-modal="true"` when `enableModal` is set) are set on the inner pane window element.
17
19
  *
18
20
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog
19
21
  */
20
22
  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) => {
21
23
  const classes = sidePaneSlotRecipe({ position, nonModal: !enableModal });
24
+ const [insideElement, setInsideElement] = useState(null);
22
25
  const { localOpen, handleCloseSidePane, handleStopCloseSidePane, sidePaneElement, setSidePaneElement } = useSidePaneStateController({
23
26
  open,
24
27
  onClose,
25
28
  ref,
29
+ insideElement,
26
30
  });
27
31
  const handleBackdropClick = useCallback(() => {
28
32
  if (!disableBackdropClose) {
@@ -56,8 +60,8 @@ export const SidePane = compatibleForwardRef(({ open, title, renderHeading, onCl
56
60
  const shouldRenderContent = enableAutoUnmount ? localOpen : true;
57
61
  const resolvedFormFooterPosition = formFooterProps?.position ?? 'sticky';
58
62
  const headingNode = title ? (_jsx(Heading, { variant: "pageHeading2", tag: "h2", children: title })) : null;
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: {
63
+ return (_jsx(TargetDomNodeProvider, { targetDomNode: sidePaneElement, children: shouldRenderContent ? (_jsx(Portal, { targetDOMNode: targetElement, children: _jsxs("div", { ref: setSidePaneElement, ...props, className: cx(classes.root, 'mfui-SidePane__root', className), style: {
60
64
  ...props.style,
61
65
  ...(!enableAutoUnmount && !localOpen && { display: 'none' }),
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 }));
66
+ }, onKeyDown: handleOnKeyDown, children: [enableModal ? (_jsx("div", { "data-mfui-content": "backdrop", className: cx(classes.backdrop, 'mfui-SidePane__backdrop'), onClick: handleBackdropClick })) : null, _jsxs("div", { ref: setInsideElement, role: "dialog", "aria-modal": enableModal ? 'true' : undefined, tabIndex: -1, "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 }));
63
67
  });
@@ -3,6 +3,7 @@ type UseSidePaneStateController = {
3
3
  open?: boolean;
4
4
  onClose?: () => void;
5
5
  ref?: ForwardedRef<HTMLDivElement | null>;
6
+ insideElement?: HTMLElement | null;
6
7
  };
7
8
  /**
8
9
  * Custom hook to control the open/close state of a SidePane.
@@ -15,9 +16,13 @@ type UseSidePaneStateController = {
15
16
  *
16
17
  * @param options.onClose - Callback function to execute when closing the SidePane.
17
18
  *
18
- * @returns Object containing functions to handle closing the SidePane and ref Object to attach Dialog element.
19
+ * @param options.insideElement - The pane window element with `role="dialog"`. It receives
20
+ * default focus when the SidePane opens, keeping focus inside the `aria-modal` boundary.
21
+ *
22
+ * @returns Object containing the local open state, close/click handlers, and a callback
23
+ * ref (`setSidePaneElement`) to attach to the root overlay element.
19
24
  */
20
- export declare function useSidePaneStateController({ open, onClose, ref }: UseSidePaneStateController): {
25
+ export declare function useSidePaneStateController({ open, onClose, ref, insideElement }: UseSidePaneStateController): {
21
26
  localOpen: boolean | undefined;
22
27
  handleCloseSidePane: () => void;
23
28
  handleStopCloseSidePane: (event: React.MouseEvent<HTMLDivElement>) => void;
@@ -12,9 +12,13 @@ import { useSidePaneOpenStateWithAnimation } from './useSidePaneOpenStateWithAni
12
12
  *
13
13
  * @param options.onClose - Callback function to execute when closing the SidePane.
14
14
  *
15
- * @returns Object containing functions to handle closing the SidePane and ref Object to attach Dialog element.
15
+ * @param options.insideElement - The pane window element with `role="dialog"`. It receives
16
+ * default focus when the SidePane opens, keeping focus inside the `aria-modal` boundary.
17
+ *
18
+ * @returns Object containing the local open state, close/click handlers, and a callback
19
+ * ref (`setSidePaneElement`) to attach to the root overlay element.
16
20
  */
17
- export function useSidePaneStateController({ open, onClose, ref }) {
21
+ export function useSidePaneStateController({ open, onClose, ref, insideElement }) {
18
22
  const [sidePaneElement, setSidePaneElement] = useState(null);
19
23
  const { localOpen, openSidePane, closeSidePane } = useSidePaneOpenStateWithAnimation({ open, sidePaneElement });
20
24
  const setSidePaneElementRef = useCallback((element) => {
@@ -49,12 +53,14 @@ export function useSidePaneStateController({ open, onClose, ref }) {
49
53
  const handleStopCloseSidePane = useCallback((event) => {
50
54
  event.stopPropagation();
51
55
  }, []);
52
- // Autofocus to the SidePane itself when it opens
56
+ // Autofocus to the pane window (`role="dialog"` element) when it opens.
57
+ // Screen readers honor `aria-modal` only when focus is on the `role="dialog"`
58
+ // element or its descendants, so the root wrapper must not be the focus target.
53
59
  useEffect(() => {
54
60
  if (!localOpen)
55
61
  return;
56
- sidePaneElement?.focus();
57
- }, [sidePaneElement, localOpen]);
62
+ (insideElement ?? sidePaneElement)?.focus();
63
+ }, [insideElement, sidePaneElement, localOpen]);
58
64
  return {
59
65
  localOpen,
60
66
  handleCloseSidePane,
@@ -7,4 +7,4 @@ import { type TagProps } from './Tag.types';
7
7
  * - If `disabled` is `true` and `onClick` is provided, the tag will be displayed as disabled button.
8
8
  * - If `showCloseButton` is `true`, the tag will be displayed with a close button.
9
9
  */
10
- export declare function Tag({ label, onClick, href, customLinkComponent, disabled, onClose, target, rel, className, }: TagProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function Tag({ label, onClick, href, customLinkComponent, disabled, onClose, closeButtonProps, target, rel, className, }: TagProps): import("react/jsx-runtime").JSX.Element;
@@ -14,10 +14,10 @@ import { cx } from '../../styled-system/css';
14
14
  * - If `disabled` is `true` and `onClick` is provided, the tag will be displayed as disabled button.
15
15
  * - If `showCloseButton` is `true`, the tag will be displayed with a close button.
16
16
  */
17
- export function Tag({ label, onClick, href, customLinkComponent, disabled = false, onClose, target, rel, className, }) {
17
+ export function Tag({ label, onClick, href, customLinkComponent, disabled = false, onClose, closeButtonProps, target, rel, className, }) {
18
18
  const isClickable = !!onClick || !!href;
19
19
  const classes = tagSlotRecipe({ interactable: isClickable, showCloseButton: !!onClose });
20
- const closeButton = (_jsx(IconButton, { disabled: disabled, className: cx(classes.closeButton, 'mfui-Tag__closeButton'), "aria-label": `${label}を削除する`, onClick: onClose, children: _jsx(Close, {}) }));
20
+ const closeButton = (_jsx(IconButton, { disabled: disabled, className: cx(classes.closeButton, 'mfui-Tag__closeButton'), "aria-label": closeButtonProps?.['aria-label'] ?? `${label}を削除する`, onClick: onClose, children: _jsx(Close, {}) }));
21
21
  if (!isClickable) {
22
22
  return (_jsx(FocusIndicator, { children: _jsxs("span", { className: cx(classes.root, 'mfui-Tag__root', className), children: [_jsx(Typography, { variant: "condensedInput", children: label }), onClose ? closeButton : null] }) }));
23
23
  }
@@ -33,6 +33,13 @@ export type TagProps = {
33
33
  * The handler to close the tag. When this prop is provided, the close button will be shown
34
34
  */
35
35
  onClose?: (event: MouseEvent<HTMLButtonElement>) => void;
36
+ /**
37
+ * The properties for the close button.
38
+ * If this prop is not passed, the default values for each property will be applied.
39
+ *
40
+ * @property aria-label - The alternative text for the close button. default is '{label}を削除する'.
41
+ */
42
+ closeButtonProps?: Pick<ComponentPropsWithoutRef<'button'>, 'aria-label'>;
36
43
  /**
37
44
  * Class name to be applied to the wrapper element.
38
45
  */
@@ -71,6 +71,31 @@ export const usePopoverController = ({ placement, open, arrowRef }) => {
71
71
  });
72
72
  // Convert calculated placement back to original placement
73
73
  const originalPlacement = reversePlacementMapping[calculatedPlacement];
74
+ const isStartPlacement = originalPlacement.endsWith('Start');
75
+ const isEndPlacement = originalPlacement.endsWith('End');
76
+ const isVerticalPlacement = originalPlacement.startsWith('top') || originalPlacement.startsWith('bottom');
77
+ const arrowHorizontalPosition = (() => {
78
+ if (!isVerticalPlacement)
79
+ return;
80
+ if (isStartPlacement)
81
+ return { left: `${String(ARROW_PADDING)}px`, right: undefined };
82
+ if (isEndPlacement)
83
+ return { left: undefined, right: `${String(ARROW_PADDING)}px` };
84
+ return {
85
+ left: arrowData?.x ? `${String(arrowData.x)}px` : undefined,
86
+ right: undefined,
87
+ };
88
+ })();
89
+ const arrowVerticalPosition = (() => {
90
+ if (originalPlacement.startsWith('top'))
91
+ return { top: '100%', bottom: undefined };
92
+ if (originalPlacement.startsWith('bottom'))
93
+ return { top: undefined, bottom: '100%' };
94
+ return {
95
+ top: arrowData?.y ? `${String(arrowData.y)}px` : undefined,
96
+ bottom: undefined,
97
+ };
98
+ })();
74
99
  return {
75
100
  tooltip: {
76
101
  setReference: setFloating,
@@ -82,20 +107,10 @@ export const usePopoverController = ({ placement, open, arrowRef }) => {
82
107
  arrow: {
83
108
  style: {
84
109
  position: 'absolute',
85
- top: (() => {
86
- // When placement is top, the arrow should be at the bottom
87
- if (originalPlacement.startsWith('top'))
88
- return '100%';
89
- return arrowData?.y ? `${String(arrowData.y)}px` : undefined;
90
- })(),
91
- right: originalPlacement.startsWith('right') ? '100%' : undefined,
92
- left: (() => {
93
- // When placement is left, the arrow should be at the right
94
- if (originalPlacement.startsWith('left'))
95
- return '100%';
96
- return arrowData?.x ? `${String(arrowData.x)}px` : undefined;
97
- })(),
98
- bottom: originalPlacement.startsWith('bottom') ? '100%' : undefined,
110
+ top: arrowVerticalPosition.top,
111
+ bottom: arrowVerticalPosition.bottom,
112
+ left: arrowHorizontalPosition?.left ?? (originalPlacement.startsWith('left') ? '100%' : undefined),
113
+ right: arrowHorizontalPosition?.right ?? (originalPlacement.startsWith('right') ? '100%' : undefined),
99
114
  // Rotate the arrow if it's on the bottom or right
100
115
  transform: `rotate(${originalPlacement.startsWith('bottom') || originalPlacement.startsWith('right') ? '180deg' : '0'})`,
101
116
  },
@@ -7,6 +7,7 @@ interface DataTableCellSlotRecipeVariant {
7
7
  isCheckboxCell: boolean
8
8
  rowHighlighted: boolean
9
9
  fixedColumn: boolean
10
+ verticalBorder: boolean
10
11
  edgeFixedColumn: "left" | "right"
11
12
  layout: "default" | "edge-to-edge"
12
13
  type: "text" | "amount"
@@ -52,6 +52,7 @@ const dataTableCellSlotRecipeVariantKeys = [
52
52
  "isCheckboxCell",
53
53
  "rowHighlighted",
54
54
  "fixedColumn",
55
+ "verticalBorder",
55
56
  "edgeFixedColumn",
56
57
  "layout",
57
58
  "type"
@@ -77,6 +78,9 @@ export const dataTableCellSlotRecipe = /* @__PURE__ */ Object.assign(dataTableCe
77
78
  "fixedColumn": [
78
79
  "true"
79
80
  ],
81
+ "verticalBorder": [
82
+ "true"
83
+ ],
80
84
  "edgeFixedColumn": [
81
85
  "left",
82
86
  "right"
@@ -5,6 +5,7 @@ import type { DistributiveOmit, Pretty } from '../types/system-types';
5
5
  interface DataTableHeaderCellSlotRecipeVariant {
6
6
  size: "small" | "medium"
7
7
  fixedColumn: boolean
8
+ verticalBorder: boolean
8
9
  edgeFixedColumn: "left" | "right"
9
10
  isCheckboxCell: boolean
10
11
  type: "text" | "amount"
@@ -35,6 +35,7 @@ const dataTableHeaderCellSlotRecipeFn = memo((props = {}) => {
35
35
  const dataTableHeaderCellSlotRecipeVariantKeys = [
36
36
  "size",
37
37
  "fixedColumn",
38
+ "verticalBorder",
38
39
  "edgeFixedColumn",
39
40
  "isCheckboxCell",
40
41
  "type"
@@ -54,6 +55,9 @@ export const dataTableHeaderCellSlotRecipe = /* @__PURE__ */ Object.assign(dataT
54
55
  "fixedColumn": [
55
56
  "true"
56
57
  ],
58
+ "verticalBorder": [
59
+ "true"
60
+ ],
57
61
  "edgeFixedColumn": [
58
62
  "left",
59
63
  "right"
@@ -10,7 +10,7 @@ type DropdownMenuItemSlotRecipeVariantMap = {
10
10
  [key in keyof DropdownMenuItemSlotRecipeVariant]: Array<DropdownMenuItemSlotRecipeVariant[key]>
11
11
  }
12
12
 
13
- type DropdownMenuItemSlotRecipeSlot = "root"
13
+ type DropdownMenuItemSlotRecipeSlot = "root" | "content" | "disabledMessage" | "disabledMessageIcon"
14
14
 
15
15
  export type DropdownMenuItemSlotRecipeVariantProps = {
16
16
  [key in keyof DropdownMenuItemSlotRecipeVariant]?: ConditionalValue<DropdownMenuItemSlotRecipeVariant[key]> | undefined
@@ -6,6 +6,18 @@ const dropdownMenuItemSlotRecipeSlotNames = [
6
6
  [
7
7
  "root",
8
8
  "DropdownMenuItem__root"
9
+ ],
10
+ [
11
+ "content",
12
+ "DropdownMenuItem__content"
13
+ ],
14
+ [
15
+ "disabledMessage",
16
+ "DropdownMenuItem__disabledMessage"
17
+ ],
18
+ [
19
+ "disabledMessageIcon",
20
+ "DropdownMenuItem__disabledMessageIcon"
9
21
  ]
10
22
  ];
11
23
  const dropdownMenuItemSlotRecipeSlotFns = /* @__PURE__ */ dropdownMenuItemSlotRecipeSlotNames.map(([slotName, slotKey]) => [slotName, createRecipe(slotKey, dropdownMenuItemSlotRecipeDefaultVariants, getSlotCompoundVariant(dropdownMenuItemSlotRecipeCompoundVariants, slotName))]);
package/dist/styles.css CHANGED
@@ -229,9 +229,13 @@
229
229
  --mfui-colors-mfui\.color\.selected\.border\.none: #2e72d8ff;
230
230
  --mfui-colors-mfui\.color\.selected\.border\.hovered: #0d57b1ff;
231
231
  --mfui-colors-mfui\.color\.selected\.border\.pressed: #003e90ff;
232
- --mfui-colors-mfui\.color\.signal-green\.content\.none: #008644ff;
232
+ --mfui-colors-mfui\.color\.selected\.text-on-background\.none: #165cb7ff;
233
+ --mfui-colors-mfui\.color\.selected\.text-on-background\.hovered: #004396ff;
234
+ --mfui-colors-mfui\.color\.selected\.text-on-background\.pressed: #072977ff;
235
+ --mfui-colors-mfui\.color\.signal-green\.content\.none: #008644ff;
233
236
  --mfui-colors-mfui\.color\.signal-green\.sub-background\.none: #b3eac0ff;
234
- --mfui-colors-mfui\.color\.signal-neutral\.sub-background\.none: #f3f3f3ff;
237
+ --mfui-colors-mfui\.color\.signal-green\.text-on-sub-background\.none: #006d36ff;
238
+ --mfui-colors-mfui\.color\.signal-neutral\.sub-background\.none: #f3f3f3ff;
235
239
  --mfui-colors-mfui\.color\.signal-red\.content\.none: #d7383cff;
236
240
  --mfui-colors-mfui\.color\.signal-red\.content\.hovered: #b40d24ff;
237
241
  --mfui-colors-mfui\.color\.signal-red\.content\.pressed: #8a0017ff;
@@ -242,9 +246,11 @@
242
246
  --mfui-colors-mfui\.color\.signal-red\.background\.hovered: #b40d24ff;
243
247
  --mfui-colors-mfui\.color\.signal-red\.background\.pressed: #8a0017ff;
244
248
  --mfui-colors-mfui\.color\.signal-red\.sub-background\.none: #ffd3ccff;
245
- --mfui-colors-mfui\.color\.signal-yellow\.content\.none: #8b7200ff;
249
+ --mfui-colors-mfui\.color\.signal-red\.text-on-sub-background\.none: #ba1828ff;
250
+ --mfui-colors-mfui\.color\.signal-yellow\.content\.none: #8b7200ff;
246
251
  --mfui-colors-mfui\.color\.signal-yellow\.sub-background\.none: #f7da90ff;
247
- --mfui-shadows-mfui\.elevation\.plus-1\.shadow-1: 0px 1px 1px 1px #00000033;
252
+ --mfui-colors-mfui\.color\.signal-yellow\.text-on-sub-background\.none: #775a01ff;
253
+ --mfui-shadows-mfui\.elevation\.plus-1\.shadow-1: 0px 1px 1px 1px #00000033;
248
254
  --mfui-shadows-mfui\.elevation\.plus-1\.shadow-2: 0px 2px 2px 1px #0000001a;
249
255
  --mfui-shadows-mfui\.elevation\.plus-2\.shadow-1: 0px 4px 8px 0px #00000040;
250
256
  --mfui-shadows-mfui\.elevation\.plus-2\.shadow-2: 0px 6px 16px 0px #00000033;
@@ -818,6 +824,28 @@
818
824
  background-color: var(--mfui-colors-mfui\.color\.base\.background\.pressed);
819
825
  }
820
826
 
827
+ .mfui-eMicFf {
828
+ display: flex;
829
+ flex-direction: column;
830
+ min-width: 0;
831
+ }
832
+
833
+ .mfui-hsjcaJ {
834
+ gap: var(--mfui-spacing-mfui\.size\.spacing\.icon-and-text\.horizontal\.comfort);
835
+ display: flex;
836
+ align-items: flex-start;
837
+ color: var(--mfui-colors-mfui\.color\.neutral\.content\.none);
838
+ overflow-wrap: break-word;
839
+ }
840
+
841
+ .mfui-imxNHe {
842
+ flex: 0 0 auto;
843
+ display: block;
844
+ width: var(--mfui-sizes-mfui\.size\.dimension\.icon\.square\.condensed);
845
+ height: var(--mfui-sizes-mfui\.size\.dimension\.icon\.square\.condensed);
846
+ margin-top: 2px;
847
+ }
848
+
821
849
  .mfui-dHwbKW {
822
850
  gap: var(--mfui-spacing-mfui\.size\.spacing\.icon-and-text\.horizontal\.comfort);
823
851
  display: inline-flex;
@@ -2331,6 +2359,8 @@
2331
2359
  .mfui-hveqnu {
2332
2360
  border: none;
2333
2361
  inset-inline: auto 0;
2362
+ outline: 2px solid transparent;
2363
+ outline-offset: 2px;
2334
2364
  position: absolute;
2335
2365
  display: flex;
2336
2366
  flex-direction: column;
@@ -2543,7 +2573,7 @@
2543
2573
  }
2544
2574
 
2545
2575
  .mfui-fhtZXM {
2546
- max-height: 160px;
2576
+ max-height: 288px;
2547
2577
  width: fit-content;
2548
2578
  }
2549
2579
 
@@ -3067,7 +3097,7 @@
3067
3097
  border-color: var(--mfui-colors-mfui\.color\.selected\.border\.none);
3068
3098
  padding-block: calc(var(--mfui-spacing-mfui\.size\.padding\.sub-navigation\.vertical\.comfort) - 1px);
3069
3099
  padding-inline: calc(var(--mfui-spacing-mfui\.size\.padding\.sub-navigation\.horizontal\.comfort) - 1px);
3070
- color: var(--mfui-colors-mfui\.color\.selected\.sign\.none);
3100
+ color: var(--mfui-colors-mfui\.color\.selected\.text-on-background\.none);
3071
3101
  background-color: var(--mfui-colors-mfui\.color\.selected\.background\.none);
3072
3102
  }
3073
3103
 
@@ -3081,7 +3111,7 @@
3081
3111
 
3082
3112
  .mfui-ljuOJV:is(:focus, [data-focus])[aria-current=page] {
3083
3113
  border-color: var(--mfui-colors-mfui\.color\.selected\.border\.hovered);
3084
- color: var(--mfui-colors-mfui\.color\.selected\.sign\.hovered);
3114
+ color: var(--mfui-colors-mfui\.color\.selected\.text-on-background\.hovered);
3085
3115
  background-color: var(--mfui-colors-mfui\.color\.selected\.background\.hovered);
3086
3116
  }
3087
3117
 
@@ -3091,7 +3121,7 @@
3091
3121
 
3092
3122
  .mfui-ljuOJV:is(:hover, [data-hover])[aria-current=page] {
3093
3123
  border-color: var(--mfui-colors-mfui\.color\.selected\.border\.hovered);
3094
- color: var(--mfui-colors-mfui\.color\.selected\.sign\.hovered);
3124
+ color: var(--mfui-colors-mfui\.color\.selected\.text-on-background\.hovered);
3095
3125
  background-color: var(--mfui-colors-mfui\.color\.selected\.background\.hovered);
3096
3126
  }
3097
3127
 
@@ -3101,7 +3131,7 @@
3101
3131
 
3102
3132
  .mfui-ljuOJV:is(:active, [data-active])[aria-current=page] {
3103
3133
  border-color: var(--mfui-colors-mfui\.color\.selected\.border\.pressed);
3104
- color: var(--mfui-colors-mfui\.color\.selected\.sign\.pressed);
3134
+ color: var(--mfui-colors-mfui\.color\.selected\.text-on-background\.pressed);
3105
3135
  background-color: var(--mfui-colors-mfui\.color\.selected\.background\.pressed);
3106
3136
  }
3107
3137
 
@@ -7358,6 +7388,23 @@ li:last-child > .mfui-cXGJls {
7358
7388
  background-color: inherit;
7359
7389
  }
7360
7390
 
7391
+ .mfui-hGAWTy {
7392
+ border-left-style: solid;
7393
+ border-left-width: var(--mfui-border-widths-mfui\.size\.border\.base\.width\.comfort);
7394
+ border-left-color: var(--mfui-colors-mfui\.color\.neutral\.thin-border\.none);
7395
+ border-right-style: solid;
7396
+ border-right-width: var(--mfui-border-widths-mfui\.size\.border\.base\.width\.comfort);
7397
+ border-right-color: var(--mfui-colors-mfui\.color\.neutral\.thin-border\.none);
7398
+ }
7399
+
7400
+ .mfui-hGAWTy:first-child {
7401
+ border-left-width: 0;
7402
+ }
7403
+
7404
+ .mfui-hGAWTy:last-child {
7405
+ border-right-width: 0;
7406
+ }
7407
+
7361
7408
  .mfui-izXzGC::before {
7362
7409
  border-right-width: var(--mfui-border-widths-mfui\.size\.border\.fixed-cell\.horizontal\.comfort);
7363
7410
  border-right-style: solid;
@@ -7433,6 +7480,23 @@ li:last-child > .mfui-cXGJls {
7433
7480
  position: sticky;
7434
7481
  }
7435
7482
 
7483
+ .mfui-haQzOt {
7484
+ border-left-style: solid;
7485
+ border-left-width: var(--mfui-border-widths-mfui\.size\.border\.base\.width\.comfort);
7486
+ border-left-color: var(--mfui-colors-mfui\.color\.neutral\.sub-border\.none);
7487
+ border-right-style: solid;
7488
+ border-right-width: var(--mfui-border-widths-mfui\.size\.border\.base\.width\.comfort);
7489
+ border-right-color: var(--mfui-colors-mfui\.color\.neutral\.sub-border\.none);
7490
+ }
7491
+
7492
+ .mfui-haQzOt:first-child {
7493
+ border-left-width: 0;
7494
+ }
7495
+
7496
+ .mfui-haQzOt:last-child {
7497
+ border-right-width: 0;
7498
+ }
7499
+
7436
7500
  .mfui-fGBQCd::before {
7437
7501
  border-right-width: var(--mfui-border-widths-mfui\.size\.border\.fixed-cell\.horizontal\.comfort);
7438
7502
  border-right-style: solid;
@@ -7988,17 +8052,17 @@ li:last-child > .mfui-cXGJls {
7988
8052
 
7989
8053
  .mfui-cNbYAX {
7990
8054
  background-color: var(--mfui-colors-mfui\.color\.signal-green\.sub-background\.none);
7991
- color: var(--mfui-colors-mfui\.color\.signal-green\.content\.none);
8055
+ color: var(--mfui-colors-mfui\.color\.signal-green\.text-on-sub-background\.none);
7992
8056
  }
7993
8057
 
7994
8058
  .mfui-bAfwQA {
7995
8059
  background-color: var(--mfui-colors-mfui\.color\.signal-red\.sub-background\.none);
7996
- color: var(--mfui-colors-mfui\.color\.signal-red\.content\.none);
8060
+ color: var(--mfui-colors-mfui\.color\.signal-red\.text-on-sub-background\.none);
7997
8061
  }
7998
8062
 
7999
8063
  .mfui-dZGLHT {
8000
8064
  background-color: var(--mfui-colors-mfui\.color\.signal-yellow\.sub-background\.none);
8001
- color: var(--mfui-colors-mfui\.color\.signal-yellow\.content\.none);
8065
+ color: var(--mfui-colors-mfui\.color\.signal-yellow\.text-on-sub-background\.none);
8002
8066
  }
8003
8067
 
8004
8068
  .mfui-bqPyQm {