@moneyforward/mfui-components 4.8.0 → 4.10.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/Badge/Badge.js +2 -1
- package/dist/src/DataTable/DataTable.types.d.ts +1 -0
- package/dist/src/DataTable/DataTableBody/DataTableBody.js +5 -2
- package/dist/src/DataTable/DataTableFilterButton/DataTableFilterButton.js +3 -9
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +7 -3
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.types.d.ts +18 -6
- package/dist/src/DataTable/hooks/useVirtualizedColumns.js +2 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/CalendarPopover/CalendarPopover.js +1 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +2 -4
- package/dist/src/DateTimeSelection/FilterDateRangePicker/DateRangePickerContent/DateRangePickerContent.js +1 -1
- package/dist/src/DateTimeSelection/FilterDateRangePicker/FilterDateRangePicker.js +2 -4
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +2 -4
- package/dist/src/DateTimeSelection/FilterMonthPicker/MonthPickerPopover/MonthPickerPopover.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +2 -3
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/MonthRangePickerPopover/MonthRangePickerPopover.js +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +2 -2
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +34 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +18 -17
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.types.d.ts +8 -0
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.d.ts +2 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/hooks/useDateRangeTriggerValueController.js +2 -1
- package/dist/src/DateTimeSelection/shared/DayCell/DayCell.js +2 -1
- package/dist/src/DateTimeSelection/shared/accessibility/constants.d.ts +1 -1
- package/dist/src/DateTimeSelection/shared/accessibility/constants.js +0 -4
- package/dist/src/DateTimeSelection/shared/accessibility/generateDateLabel.d.ts +4 -0
- package/dist/src/DateTimeSelection/shared/accessibility/generateDateLabel.js +4 -7
- package/dist/src/Dialog/Dialog.types.d.ts +17 -0
- package/dist/src/FileDropZone/FileDropZone.js +23 -6
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +2 -2
- package/dist/src/Popover/Popover.d.ts +8 -1
- package/dist/src/Popover/Popover.js +125 -14
- package/dist/src/Popover/Popover.types.d.ts +16 -3
- package/dist/src/SectionMessage/SectionMessage.d.ts +6 -0
- package/dist/src/SectionMessage/SectionMessage.js +16 -1
- package/dist/src/SectionMessage/SectionMessage.types.d.ts +4 -0
- package/dist/src/Skeleton/Skeleton.d.ts +5 -2
- package/dist/src/Skeleton/Skeleton.js +5 -2
- package/dist/src/Skeleton/Skeleton.types.d.ts +9 -1
- package/dist/src/VisuallyHidden/VisuallyHidden.d.ts +17 -0
- package/dist/src/VisuallyHidden/VisuallyHidden.js +21 -0
- package/dist/src/VisuallyHidden/VisuallyHidden.types.d.ts +19 -0
- package/dist/src/VisuallyHidden/VisuallyHidden.types.js +1 -0
- package/dist/src/VisuallyHidden/index.d.ts +2 -0
- package/dist/src/VisuallyHidden/index.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/utilities/dom/usePopoverMenuPlacement.js +0 -5
- package/dist/src/utilities/env/isProduction.d.ts +14 -0
- package/dist/src/utilities/env/isProduction.js +22 -0
- package/dist/src/utilities/react/isComponentOrWrapped.js +2 -1
- package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/badge-slot-recipe.js +0 -4
- package/dist/styled-system/recipes/day-cell-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/day-cell-slot-recipe.js +0 -4
- package/dist/styled-system/recipes/index.d.ts +2 -1
- package/dist/styled-system/recipes/index.js +1 -0
- package/dist/styled-system/recipes/popover-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/popover-slot-recipe.js +4 -0
- package/dist/styled-system/recipes/visually-hidden-slot-recipe.d.ts +33 -0
- package/dist/styled-system/recipes/visually-hidden-slot-recipe.js +28 -0
- package/dist/styled-system/tokens/index.js +4 -0
- package/dist/styled-system/tokens/tokens.d.ts +1 -1
- package/dist/styles.css +29 -38
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -75,6 +75,8 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
|
|
|
75
75
|
const buttonRef = useRef(null);
|
|
76
76
|
const previousValueRef = useRef(value);
|
|
77
77
|
const isUserInitiatedClearRef = useRef(false);
|
|
78
|
+
// Counts nested dragenter/dragleave pairs so moving over descendants does not drop the highlight
|
|
79
|
+
const dragDepthRef = useRef(0);
|
|
78
80
|
const displayValues = isControlled ? value : internalValues;
|
|
79
81
|
const errorMessages = (Array.isArray(errorMessage) ? errorMessage : [errorMessage]).filter((message) => Boolean(message));
|
|
80
82
|
const fileBoxMaxWidthStyle = { maxWidth: fileBoxWidth };
|
|
@@ -141,25 +143,38 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
|
|
|
141
143
|
if (isControlled) {
|
|
142
144
|
resetFileInput();
|
|
143
145
|
}
|
|
146
|
+
dragDepthRef.current = 0;
|
|
144
147
|
setIsDragging(false);
|
|
145
148
|
}, [isControlled, onChange, resetFileInput]);
|
|
146
149
|
const handleDragEnter = useCallback((event) => {
|
|
147
150
|
event.preventDefault();
|
|
148
151
|
event.stopPropagation();
|
|
152
|
+
dragDepthRef.current += 1;
|
|
149
153
|
setIsDragging(true);
|
|
150
154
|
}, []);
|
|
151
155
|
const handleDragLeave = useCallback((event) => {
|
|
152
156
|
event.preventDefault();
|
|
153
157
|
event.stopPropagation();
|
|
154
|
-
|
|
158
|
+
/*
|
|
159
|
+
* `dragleave` also bubbles up from descendants, and moving into a child fires its `dragenter`
|
|
160
|
+
* before the container's `dragleave`. Only clear the highlight once every entered element has
|
|
161
|
+
* been left, so it stays applied for as long as the pointer is inside the drop zone.
|
|
162
|
+
*/
|
|
163
|
+
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
|
164
|
+
if (dragDepthRef.current === 0) {
|
|
165
|
+
setIsDragging(false);
|
|
166
|
+
}
|
|
155
167
|
}, []);
|
|
156
168
|
const handleDragOver = useCallback((event) => {
|
|
157
169
|
event.preventDefault();
|
|
158
170
|
event.stopPropagation();
|
|
159
|
-
|
|
171
|
+
// eslint-disable-next-line no-param-reassign -- Setting the drop effect on the event is how the copy cursor is requested
|
|
172
|
+
event.dataTransfer.dropEffect = disabled ? 'none' : 'copy';
|
|
173
|
+
}, [disabled]);
|
|
160
174
|
const handleDrop = useCallback((event) => {
|
|
161
175
|
event.preventDefault();
|
|
162
176
|
event.stopPropagation();
|
|
177
|
+
dragDepthRef.current = 0;
|
|
163
178
|
setIsDragging(false);
|
|
164
179
|
if (disabled) {
|
|
165
180
|
return;
|
|
@@ -210,11 +225,13 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
|
|
|
210
225
|
}
|
|
211
226
|
}, [disabled]);
|
|
212
227
|
const handleContainerClick = useCallback((event) => {
|
|
213
|
-
//
|
|
214
|
-
// and
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
// A click anywhere in the drop zone opens the file dialog, except on the buttons
|
|
229
|
+
// and the `extraElement` content, which handle their own clicks
|
|
230
|
+
const target = event.target;
|
|
231
|
+
if (target.closest('button, [data-mfui-content="file-drop-zone-extra"]')) {
|
|
232
|
+
return;
|
|
217
233
|
}
|
|
234
|
+
handleFileInputTrigger();
|
|
218
235
|
}, [handleFileInputTrigger]);
|
|
219
236
|
const renderDisplayValues = useMemo(() => {
|
|
220
237
|
if (displayValues !== undefined && displayValues.length > 0) {
|
|
@@ -324,7 +324,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
324
324
|
return (_jsxs(Tag, { className: cx(classes.infiniteScrollError, 'mfui-MultipleSelectBox__infiniteScrollError'), role: "alert", children: [_jsxs("div", { className: cx(classes.infiniteScrollErrorMessage, 'mfui-MultipleSelectBox__infiniteScrollErrorMessage'), "aria-live": "polite", children: [_jsx(Error, { "aria-hidden": true, className: cx(classes.infiniteScrollErrorIcon, 'mfui-MultipleSelectBox__infiniteScrollErrorIcon') }), _jsx(Typography, { variant: "body", children: infiniteScrollErrorMessage })] }), _jsx("div", { className: cx(classes.infiniteScrollErrorButton, 'mfui-MultipleSelectBox__infiniteScrollErrorButton'), children: _jsx(Button, { size: "small", onClick: (event) => {
|
|
325
325
|
event.stopPropagation();
|
|
326
326
|
retryInfiniteScroll();
|
|
327
|
-
}, children: infiniteScrollRetryButtonText }) })] }));
|
|
327
|
+
}, children: infiniteScrollRetryButtonText }) })] }, "infinite-scroll-error"));
|
|
328
328
|
}, [
|
|
329
329
|
infiniteScrollError,
|
|
330
330
|
classes.infiniteScrollError,
|
|
@@ -336,7 +336,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
336
336
|
infiniteScrollRetryButtonText,
|
|
337
337
|
]);
|
|
338
338
|
// Render infinite scroll loading indicator
|
|
339
|
-
const renderInfiniteScrollLoading = useCallback(() => isInfiniteScrollLoading && enableInfiniteScroll ? (_jsx("div", { className: cx(classes.infiniteScrollLoading, 'mfui-MultipleSelectBox__infiniteScrollLoading'), children: _jsx(ProgressIndicator, {}) })) : null, [isInfiniteScrollLoading, enableInfiniteScroll, classes.infiniteScrollLoading]);
|
|
339
|
+
const renderInfiniteScrollLoading = useCallback(() => isInfiniteScrollLoading && enableInfiniteScroll ? (_jsx("div", { className: cx(classes.infiniteScrollLoading, 'mfui-MultipleSelectBox__infiniteScrollLoading'), children: _jsx(ProgressIndicator, {}) }, "infinite-scroll-loading")) : null, [isInfiniteScrollLoading, enableInfiniteScroll, classes.infiniteScrollLoading]);
|
|
340
340
|
const searchNode = enableSearchOptions ? (_jsx(SearchBox, { ref: searchInputRef, enableClearButton: true, value: searchText, textBoxSize: "small", autoComplete: "off", "aria-label": "\u691C\u7D22\u3059\u308B", onChange: onSearchTextChange, ...searchBoxProps })) : null;
|
|
341
341
|
const allOptionsControlsNode = enableAllOptionsControls ? (_jsxs(HStack, { justifyContent: "space-between", children: [_jsx(Button, { ref: selectAllButtonRef, size: "small", onClick: handleSelectAll, children: selectAllButtonProps?.label ?? 'すべて選択' }), _jsx(Button, { ref: clearAllButtonRef, size: "small", onClick: handleClearAll, children: clearAllButtonProps?.label ?? 'すべて解除' })] })) : null;
|
|
342
342
|
const selectedCountNode = enableApplyControls || showSelectedCount ? (_jsx(Typography, { variant: "body", color: "neutral.600", children: enableApplyControls
|
|
@@ -5,5 +5,12 @@ import { type PopoverProps } from './Popover.types';
|
|
|
5
5
|
*
|
|
6
6
|
* You can control the open state of the Popover by passing the `open` and `onOpenStateChanged` props
|
|
7
7
|
* Or just leave it as an uncontrolled popover, it will automatically handle by itself.
|
|
8
|
+
*
|
|
9
|
+
* ## Layering
|
|
10
|
+
*
|
|
11
|
+
* The popover content is drawn on the `--mfui-z-index-popover` layer (`6000`), above `Dialog` and `SidePane`
|
|
12
|
+
* and below `Toast` and `Tooltip`.
|
|
13
|
+
* To place it above an application overlay with a higher `z-index`, set `z-index` through `contentProps.className`.
|
|
14
|
+
* See the "Layering" page in Storybook for the full layer order.
|
|
8
15
|
*/
|
|
9
|
-
export declare function Popover({ renderTrigger, renderContent, open, onOpenStateChanged, onOpen, onClose, enableAutoFocusOnTrigger, enableAutoFocusOnPopover, minWidth, maxHeight, enableConstrainedContentWidth, contentProps, targetDOMNode, enableAutoUnmount, onBlur, allowedPlacements, enableViewportConstraint, enableAutomaticPortalTargetResolution, enableCenteredLayoutOnNarrowViewport, }: PopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export declare function Popover({ renderTrigger, renderContent, open, onOpenStateChanged, onOpen, onClose, enableAutoFocusOnTrigger, returnFocusRef, enableAutoFocusOnPopover, minWidth, maxHeight, enableConstrainedContentWidth, contentProps, targetDOMNode, enableAutoUnmount, onBlur, allowedPlacements, enableViewportConstraint, enableAutomaticPortalTargetResolution, enableCenteredLayoutOnNarrowViewport, }: PopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -8,6 +8,7 @@ import { useDisclosure } from '../utilities/state/useDisclosure';
|
|
|
8
8
|
import { Portal, TargetDomNodeProvider, useAutomaticTargetDomNode } from '../Portal';
|
|
9
9
|
import { useOnClickOutside } from '../utilities/dom/useOnClickOutside';
|
|
10
10
|
import { getRootNode } from '../utilities/dom/getRootNode';
|
|
11
|
+
import { getDeepActiveElement } from '../utilities/dom/getDeepActiveElement';
|
|
11
12
|
import { getTargetDomNode } from '../utilities/dom/getTargetDomNode';
|
|
12
13
|
import { popoverSlotRecipe } from '../../styled-system/recipes';
|
|
13
14
|
import { useUpdateEffect } from '../utilities/effect/useUpdateEffect';
|
|
@@ -18,10 +19,19 @@ import { cx } from '../../styled-system/css';
|
|
|
18
19
|
*
|
|
19
20
|
* You can control the open state of the Popover by passing the `open` and `onOpenStateChanged` props
|
|
20
21
|
* Or just leave it as an uncontrolled popover, it will automatically handle by itself.
|
|
22
|
+
*
|
|
23
|
+
* ## Layering
|
|
24
|
+
*
|
|
25
|
+
* The popover content is drawn on the `--mfui-z-index-popover` layer (`6000`), above `Dialog` and `SidePane`
|
|
26
|
+
* and below `Toast` and `Tooltip`.
|
|
27
|
+
* To place it above an application overlay with a higher `z-index`, set `z-index` through `contentProps.className`.
|
|
28
|
+
* See the "Layering" page in Storybook for the full layer order.
|
|
21
29
|
*/
|
|
22
|
-
export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged, onOpen, onClose, enableAutoFocusOnTrigger = true, enableAutoFocusOnPopover = true, minWidth, maxHeight, enableConstrainedContentWidth = false, contentProps = {}, targetDOMNode, enableAutoUnmount = true, onBlur, allowedPlacements, enableViewportConstraint = true, enableAutomaticPortalTargetResolution = false, enableCenteredLayoutOnNarrowViewport = false, }) {
|
|
23
|
-
//
|
|
24
|
-
|
|
30
|
+
export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged, onOpen, onClose, enableAutoFocusOnTrigger = true, returnFocusRef, enableAutoFocusOnPopover = true, minWidth, maxHeight, enableConstrainedContentWidth = false, contentProps = {}, targetDOMNode, enableAutoUnmount = true, onBlur, allowedPlacements, enableViewportConstraint = true, enableAutomaticPortalTargetResolution = false, enableCenteredLayoutOnNarrowViewport = false, }) {
|
|
31
|
+
// The last open / close request made by the Popover itself: the next open state, its event, and,
|
|
32
|
+
// for Escape and click outside, how it closed. Read once by the open / close effect below.
|
|
33
|
+
const [pendingChange, setPendingChange] = useState(null);
|
|
34
|
+
const usedChangeRef = useRef(null);
|
|
25
35
|
const { isOpen: localIsOpen, toggle: localToggle, open: localOpen, close: localClose, } = useDisclosure({
|
|
26
36
|
value: open,
|
|
27
37
|
onToggle: onOpenStateChanged,
|
|
@@ -49,6 +59,82 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
49
59
|
refs.setFloating(node);
|
|
50
60
|
setFloatingElement(node);
|
|
51
61
|
}, [refs]);
|
|
62
|
+
/*
|
|
63
|
+
* MEMO: How focus returns to the trigger when the Popover closes
|
|
64
|
+
*
|
|
65
|
+
* Popover follows the rules of the native HTML popover:
|
|
66
|
+
* - After a click outside, focus does not return. The user has moved on to something else, so
|
|
67
|
+
* focus is left where the browser put it.
|
|
68
|
+
* - After Escape, focus returns. Popover's own Escape handler calls `preventDefault()`, which
|
|
69
|
+
* would otherwise skip the return, so Escape is tracked separately.
|
|
70
|
+
* - Focus returns only if it was still inside the Popover, or was lost when the Popover was
|
|
71
|
+
* removed. A Popover closed by its parent never takes focus away from where the user is.
|
|
72
|
+
* - Returning focus does not scroll the page.
|
|
73
|
+
* Components whose trigger button is not the reference element (the Filter pickers) pass that
|
|
74
|
+
* button as `returnFocusRef`.
|
|
75
|
+
* Before, focus always returned to the trigger, except after Escape. Because `focus()` scrolls to
|
|
76
|
+
* the element by default, a popover that was open lower on the page pulled the page down to its
|
|
77
|
+
* trigger when the user clicked anywhere else (e.g. the Popover docs page, where `WithMaxHeight`
|
|
78
|
+
* starts open).
|
|
79
|
+
*
|
|
80
|
+
* Options evaluated:
|
|
81
|
+
* - A. Fix only the story that starts open: one line, but products can hit the same jump.
|
|
82
|
+
* - B. Hide that story from the docs page: hides the problem instead of fixing it.
|
|
83
|
+
* - C. (picked) Follow the native popover rules above: fixes the cause for every component in
|
|
84
|
+
* one place and matches the HTML standard, Radix and the WAI-ARIA APG (Escape returns focus to
|
|
85
|
+
* the menu button or the element that opened the dialog). It changes visible focus behavior:
|
|
86
|
+
* no focus after a click outside, and focus after Escape.
|
|
87
|
+
* - C2. Pass a close reason to `onClose` and let each component decide: works, but adds public
|
|
88
|
+
* API and repeats the same rule in menus, pickers and DataTableFilterButton.
|
|
89
|
+
* - D. Return focus with `preventScroll: true` only: no jump, but focus is still taken away from
|
|
90
|
+
* what the user clicked, which the HTML standard does not do.
|
|
91
|
+
* - E. Rebuild Popover on the native `popover` attribute: the browser handles outside clicks,
|
|
92
|
+
* Escape, focus, and stacking itself. Not done yet, see the TODO below.
|
|
93
|
+
* - F. Native `popover` plus CSS anchor positioning: no positioning library at all, but too
|
|
94
|
+
* early (Baseline since January 2026, still partial support in Chrome and Firefox).
|
|
95
|
+
*
|
|
96
|
+
* C is the safest fix today, and it already behaves like E, so it does not block E later.
|
|
97
|
+
*
|
|
98
|
+
* TODO: Rebuild Popover on the native `popover` attribute (option E)
|
|
99
|
+
* - Scope: Popover, `useOnClickOutside`, `usePopoverMenuPlacement`, and the Portal helpers
|
|
100
|
+
* (around 900 lines), the 12 components built on Popover, and the 22 components that expose
|
|
101
|
+
* `targetDOMNode`, which the top layer makes mostly unnecessary (deprecate, then remove in
|
|
102
|
+
* a major version).
|
|
103
|
+
* - Blockers: jsdom does not implement the Popover API (jsdom/jsdom#3721, open since May 2024),
|
|
104
|
+
* so tests need a stub or a real browser runner. Top-layer stacking changes the order
|
|
105
|
+
* documented in `docs/Layering.mdx`. MFUI has no written browser support policy yet.
|
|
106
|
+
* - Timing: Popover API is Baseline "newly available" since January 2025, so "widely
|
|
107
|
+
* available" (30 months later) from around July 2027. `@floating-ui/dom` already detects
|
|
108
|
+
* `:popover-open` elements, so positioning can stay on Floating UI.
|
|
109
|
+
* - Order: test setup -> Popover core with the same public API -> components and docs ->
|
|
110
|
+
* deprecate `targetDOMNode`.
|
|
111
|
+
*
|
|
112
|
+
* Resources checked (October 2026):
|
|
113
|
+
* - HTML Standard, popover light dismiss and the hide popover algorithm: https://html.spec.whatwg.org/multipage/popover.html
|
|
114
|
+
* - HTML Standard, `activeElement` and focus fixup: https://html.spec.whatwg.org/multipage/interaction.html
|
|
115
|
+
* - WAI-ARIA APG, Menu pattern (Escape): https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
|
|
116
|
+
* - WAI-ARIA APG, Dialog pattern (focus on close): https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
|
|
117
|
+
* - MDN, HTMLElement.focus() `preventScroll`: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
|
|
118
|
+
* - MDN, button click focus (Safari does not focus on click): https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button
|
|
119
|
+
* - Radix Popover (`hasInteractedOutsideRef`): https://github.com/radix-ui/primitives/blob/main/packages/react/popover/src/popover.tsx
|
|
120
|
+
* - Floating UI FloatingFocusManager `returnFocus`: https://floating-ui.com/docs/FloatingFocusManager
|
|
121
|
+
* - Base UI Popover `finalFocus`: https://base-ui.com/react/components/popover
|
|
122
|
+
* - MDN, Popover API Baseline: https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
|
|
123
|
+
* - MDN, anchor-name Baseline: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/anchor-name
|
|
124
|
+
* - Can I use, CSS anchor positioning: https://caniuse.com/css-anchor-positioning
|
|
125
|
+
* - web.dev, Baseline definitions: https://web.dev/baseline
|
|
126
|
+
* - jsdom Popover API issue: https://github.com/jsdom/jsdom/issues/3721
|
|
127
|
+
*/
|
|
128
|
+
// Whether focus is still inside the Popover, or was lost to the body when the Popover was
|
|
129
|
+
// removed. Shadow DOM aware: looks through shadow roots for the element that really has focus,
|
|
130
|
+
// so it also works when the content is not in the same shadow tree as the trigger.
|
|
131
|
+
const isFocusInsideOrLost = useCallback(() => {
|
|
132
|
+
const ownerDocument = refs.reference.current?.ownerDocument ?? document;
|
|
133
|
+
const activeElement = getDeepActiveElement(ownerDocument);
|
|
134
|
+
if (!activeElement || activeElement === ownerDocument.body)
|
|
135
|
+
return true;
|
|
136
|
+
return !!refs.floating.current?.contains(activeElement);
|
|
137
|
+
}, [refs.reference, refs.floating]);
|
|
52
138
|
// Fire onOpen / onClose *only* when the open state actually changes.
|
|
53
139
|
// We keep a ref of the previous open state so a new toggleEvent while already
|
|
54
140
|
// open does not retrigger `onOpen`, which previously caused unexpected focus
|
|
@@ -56,6 +142,14 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
56
142
|
const previousOpenRef = useRef(localIsOpen);
|
|
57
143
|
useUpdateEffect(() => {
|
|
58
144
|
const wasOpen = previousOpenRef.current;
|
|
145
|
+
// Use the request only once, and only for the change it asked for. Any other change comes from the
|
|
146
|
+
// `open` prop, so a later change never reuses an old event (e.g. a prevented Escape).
|
|
147
|
+
const change = pendingChange && pendingChange !== usedChangeRef.current && pendingChange.nextOpen === localIsOpen
|
|
148
|
+
? pendingChange
|
|
149
|
+
: null;
|
|
150
|
+
usedChangeRef.current = pendingChange;
|
|
151
|
+
const toggleEvent = change?.event;
|
|
152
|
+
const closeSource = change?.source;
|
|
59
153
|
if (!wasOpen && localIsOpen) {
|
|
60
154
|
// Transition: closed -> open
|
|
61
155
|
onOpen?.(toggleEvent);
|
|
@@ -66,20 +160,32 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
66
160
|
else if (wasOpen && !localIsOpen) {
|
|
67
161
|
// Transition: open -> closed
|
|
68
162
|
onClose?.(toggleEvent);
|
|
69
|
-
if (
|
|
70
|
-
|
|
163
|
+
if (enableAutoFocusOnTrigger &&
|
|
164
|
+
closeSource !== 'outside-press' &&
|
|
165
|
+
(closeSource === 'escape-key' || !toggleEvent?.defaultPrevented) &&
|
|
166
|
+
isFocusInsideOrLost()) {
|
|
167
|
+
// Like the native popover, returning focus does not scroll the page.
|
|
168
|
+
(returnFocusRef?.current ?? refs.reference.current)?.focus({ preventScroll: true });
|
|
71
169
|
}
|
|
72
170
|
}
|
|
73
171
|
// Update tracker for next invocation
|
|
74
172
|
previousOpenRef.current = localIsOpen;
|
|
75
|
-
}, [
|
|
173
|
+
}, [
|
|
174
|
+
localIsOpen,
|
|
175
|
+
onOpen,
|
|
176
|
+
onClose,
|
|
177
|
+
enableAutoFocusOnPopover,
|
|
178
|
+
enableAutoFocusOnTrigger,
|
|
179
|
+
isFocusInsideOrLost,
|
|
180
|
+
returnFocusRef,
|
|
181
|
+
]);
|
|
76
182
|
const handleKeyDownTrigger = useCallback((event) => {
|
|
77
183
|
switch (event.key) {
|
|
78
184
|
case 'ArrowDown':
|
|
79
185
|
case 'ArrowUp':
|
|
80
186
|
event.stopPropagation();
|
|
81
187
|
event.preventDefault();
|
|
82
|
-
|
|
188
|
+
setPendingChange({ nextOpen: true, event });
|
|
83
189
|
localOpen();
|
|
84
190
|
break;
|
|
85
191
|
}
|
|
@@ -89,7 +195,7 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
89
195
|
case 'Escape':
|
|
90
196
|
event.stopPropagation();
|
|
91
197
|
event.preventDefault();
|
|
92
|
-
|
|
198
|
+
setPendingChange({ nextOpen: false, event, source: 'escape-key' });
|
|
93
199
|
localClose();
|
|
94
200
|
break;
|
|
95
201
|
}
|
|
@@ -148,7 +254,7 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
148
254
|
}, [onBlur, refs.reference, refs.floating]);
|
|
149
255
|
// Close the menu when clicking outside the menu
|
|
150
256
|
useOnClickOutside(refs.floating, (event) => {
|
|
151
|
-
|
|
257
|
+
setPendingChange({ nextOpen: false, source: 'outside-press' });
|
|
152
258
|
localClose();
|
|
153
259
|
// Also trigger onBlur when clicking outside, since this represents the user
|
|
154
260
|
// clicking away from the component entirely. We create a synthetic FocusEvent
|
|
@@ -163,18 +269,23 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
163
269
|
timeStamp: event.timeStamp,
|
|
164
270
|
nativeEvent: event,
|
|
165
271
|
});
|
|
166
|
-
},
|
|
272
|
+
},
|
|
273
|
+
// A closed Popover ignores clicks outside, like the native popover's light dismiss. Without this,
|
|
274
|
+
// a closed Popover kept mounted by `enableAutoUnmount={false}` reported a close and a blur on every click.
|
|
275
|
+
// Uses the shown state (`open` first), so a Popover kept open by a parent that refused a close still
|
|
276
|
+
// asks again on the next click outside.
|
|
277
|
+
{ ignoreRefs: [refs.reference], disabled: !(open ?? localIsOpen) });
|
|
167
278
|
const options = useMemo(() => {
|
|
168
279
|
const toggle = (event) => {
|
|
169
|
-
|
|
280
|
+
setPendingChange({ nextOpen: !localIsOpen, event });
|
|
170
281
|
localToggle();
|
|
171
282
|
};
|
|
172
283
|
const open = (event) => {
|
|
173
|
-
|
|
284
|
+
setPendingChange({ nextOpen: true, event });
|
|
174
285
|
localOpen();
|
|
175
286
|
};
|
|
176
287
|
const close = (event) => {
|
|
177
|
-
|
|
288
|
+
setPendingChange({ nextOpen: false, event });
|
|
178
289
|
localClose();
|
|
179
290
|
};
|
|
180
291
|
return {
|
|
@@ -265,5 +376,5 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
265
376
|
return (_jsxs(_Fragment, { children: [renderTrigger(options), shouldRenderTheContent ? (_jsx(Portal, { targetDOMNode: targetElement, children: _jsx("div", { ref: setFloatingRef, id: contentProps.id, tabIndex: contentProps.tabIndex ?? 0, "data-mfui-content": "popover-portal-content", "data-mfui-popover-depth": nestingDepth, className: cx(classes.root, 'mfui-Popover__root', contentProps.className), style: {
|
|
266
377
|
...floatingStyles,
|
|
267
378
|
...(!enableAutoUnmount && !isPopoverOpen && { display: 'none' }),
|
|
268
|
-
}, onKeyDown: handleKeyDownPopover, children: _jsx(TargetDomNodeProvider, { targetDomNode: floatingElement, children: _jsx(PopoverContextProvider, { children: renderContent(options) }) }) }) })) : null] }));
|
|
379
|
+
}, onKeyDown: handleKeyDownPopover, children: _jsx(TargetDomNodeProvider, { targetDomNode: floatingElement, children: _jsx("div", { className: cx(classes.contentWrapper, 'mfui-Popover__contentWrapper'), children: _jsx(PopoverContextProvider, { children: renderContent(options) }) }) }) }) })) : null] }));
|
|
269
380
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ComponentProps, type CSSProperties, type KeyboardEventHandler, type ReactNode, type SyntheticEvent, type FocusEventHandler } from 'react';
|
|
1
|
+
import { type ComponentProps, type CSSProperties, type KeyboardEventHandler, type ReactNode, type RefObject, type SyntheticEvent, type FocusEventHandler } from 'react';
|
|
2
2
|
type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
3
3
|
type Alignment = 'start' | 'end';
|
|
4
4
|
/**
|
|
@@ -102,8 +102,10 @@ export type PopoverProps = {
|
|
|
102
102
|
onOpen?: (event?: SyntheticEvent) => void;
|
|
103
103
|
/**
|
|
104
104
|
* This callback function will be called with the Popover closed.
|
|
105
|
-
* The default action is focusing on the Trigger
|
|
106
|
-
*
|
|
105
|
+
* The default action is focusing on the Trigger, like the native HTML popover: not after a click outside, and only
|
|
106
|
+
* when focus is still inside the Popover. Focusing does not scroll the page.
|
|
107
|
+
* You can prevent that action by using `event.preventDefault()` in the callback function, except after Escape.
|
|
108
|
+
* To never return focus, use `enableAutoFocusOnTrigger={false}`.
|
|
107
109
|
*
|
|
108
110
|
* @param event - The trigger event which makes the Popover close, this param can be undefined if you use `toggle` or
|
|
109
111
|
* `close` without passing the event parameter. If you don't pass the event, you can't prevent the default action.
|
|
@@ -115,6 +117,17 @@ export type PopoverProps = {
|
|
|
115
117
|
* @default true - By default, it will automatically focus on the trigger element when the Popover closes.
|
|
116
118
|
*/
|
|
117
119
|
enableAutoFocusOnTrigger?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The element to return focus to when the Popover closes, instead of the Trigger.
|
|
122
|
+
* Use it when the element registered by `setTriggerRef` is not the button that opens the Popover.
|
|
123
|
+
*
|
|
124
|
+
* This prop is intended for internal MFUI components like the Filter pickers.
|
|
125
|
+
*
|
|
126
|
+
* @internal
|
|
127
|
+
*
|
|
128
|
+
* @default undefined
|
|
129
|
+
*/
|
|
130
|
+
returnFocusRef?: RefObject<HTMLElement | null>;
|
|
118
131
|
/**
|
|
119
132
|
* The flag to enable or disable autofocus on the popover element when the Popover opens.
|
|
120
133
|
*
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This component switches the variants of looks depends on the props `messageType` and `region`.
|
|
6
6
|
*
|
|
7
|
+
* The wrapper is a live region whose urgency follows `messageType`:
|
|
8
|
+
* - `error`: sets `role="alert"` — announces immediately to screen readers.
|
|
9
|
+
* - `neutral` / `info` / `success` / `caution` / `busy` / `progress`: set `role="status"` — announces politely when the user is idle.
|
|
10
|
+
*
|
|
11
|
+
* A `role` or `aria-live` passed via props takes precedence over these defaults.
|
|
12
|
+
*
|
|
7
13
|
* Also extends the props of `<div>` element (the wrapper).
|
|
8
14
|
*
|
|
9
15
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
|
|
@@ -6,12 +6,27 @@ import { sectionMessageSlotRecipe } from '../../styled-system/recipes';
|
|
|
6
6
|
import { IconButton } from '../IconButton';
|
|
7
7
|
import { ProgressIndicator } from '../ProgressIndicator';
|
|
8
8
|
import { Typography } from '../Typography';
|
|
9
|
+
const messageTypeRoleMap = {
|
|
10
|
+
neutral: 'status',
|
|
11
|
+
info: 'status',
|
|
12
|
+
success: 'status',
|
|
13
|
+
error: 'alert',
|
|
14
|
+
caution: 'status',
|
|
15
|
+
busy: 'status',
|
|
16
|
+
progress: 'status',
|
|
17
|
+
};
|
|
9
18
|
/**
|
|
10
19
|
* The SectionMessage component is used to display an inline message inside a page or some container.
|
|
11
20
|
* Note: We changed the name of the component from `InlineMessage` to `SectionMessage`.
|
|
12
21
|
*
|
|
13
22
|
* This component switches the variants of looks depends on the props `messageType` and `region`.
|
|
14
23
|
*
|
|
24
|
+
* The wrapper is a live region whose urgency follows `messageType`:
|
|
25
|
+
* - `error`: sets `role="alert"` — announces immediately to screen readers.
|
|
26
|
+
* - `neutral` / `info` / `success` / `caution` / `busy` / `progress`: set `role="status"` — announces politely when the user is idle.
|
|
27
|
+
*
|
|
28
|
+
* A `role` or `aria-live` passed via props takes precedence over these defaults.
|
|
29
|
+
*
|
|
15
30
|
* Also extends the props of `<div>` element (the wrapper).
|
|
16
31
|
*
|
|
17
32
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div
|
|
@@ -36,5 +51,5 @@ export const SectionMessage = forwardRef(({ messageType = 'neutral', region = 'i
|
|
|
36
51
|
progress: (_jsx(ProgressIndicator, { ...statusIconA11yProps, className: cx(classes.statusIcon, 'mfui-SectionMessage__statusIcon') })),
|
|
37
52
|
}[messageType];
|
|
38
53
|
const contentIsString = typeof children === 'string';
|
|
39
|
-
return (_jsxs("div", { ref: ref, className: cx(classes.root, 'mfui-SectionMessage__root', className), ...props,
|
|
54
|
+
return (_jsxs("div", { ref: ref, role: messageTypeRoleMap[messageType], className: cx(classes.root, 'mfui-SectionMessage__root', className), ...props, children: [_jsxs("div", { className: cx(classes.topSection, 'mfui-SectionMessage__topSection'), children: [_jsxs("div", { className: cx(classes.content, 'mfui-SectionMessage__content'), children: [StatusIcon, contentIsString ? _jsx(Typography, { variant: "body", children: children }) : children] }), _jsxs("div", { className: cx(classes.rightSection, 'mfui-SectionMessage__rightSection'), children: [_jsx("div", { className: cx(classes.actionSlot, classes.desktopActionSlot, 'mfui-SectionMessage__actionSlot'), "data-mfui-content": "SectionMessage-action-slot-desktop", children: actionSlot }), onClose ? (_jsx(IconButton, { "aria-label": closeButtonProps?.['aria-label'] ?? '閉じる', onClick: onClose, children: _jsx(Close, { "aria-hidden": true, className: cx(classes.closeIcon, 'mfui-SectionMessage__closeIcon') }) })) : null] })] }), actionSlot && !disableResponsive ? (_jsx("div", { className: cx(classes.actionSlot, classes.mobileActionSlot, 'mfui-SectionMessage__actionSlot'), "data-mfui-content": "SectionMessage-action-slot-mobile", children: actionSlot })) : null] }));
|
|
40
55
|
});
|
|
@@ -9,6 +9,10 @@ export type SectionMessageProps = {
|
|
|
9
9
|
/**
|
|
10
10
|
* The type of the message
|
|
11
11
|
*
|
|
12
|
+
* It also determines the live-region role of the wrapper:
|
|
13
|
+
* `error` sets `role="alert"` (assertive), and every other type sets `role="status"` (polite).
|
|
14
|
+
* Pass `role` or `aria-live` to override this default.
|
|
15
|
+
*
|
|
12
16
|
* @default 'neutral'
|
|
13
17
|
*/
|
|
14
18
|
messageType?: SectionMessageSlotRecipeVariant['messageType'];
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { type SkeletonProps } from './Skeleton.types';
|
|
2
1
|
/**
|
|
3
2
|
* The Skeleton component is used to indicate loading state of content before the data gets loaded.
|
|
3
|
+
*
|
|
4
|
+
* The root element is decorative and hidden from assistive technologies with `aria-hidden="true"` by default.
|
|
4
5
|
*/
|
|
5
|
-
export declare const Skeleton: import("react").ForwardRefExoticComponent<
|
|
6
|
+
export declare const Skeleton: import("react").ForwardRefExoticComponent<{
|
|
7
|
+
className?: string;
|
|
8
|
+
} & Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "children"> & import("react").RefAttributes<HTMLDivElement>>;
|
|
@@ -4,8 +4,11 @@ import { cx } from '../../styled-system/css';
|
|
|
4
4
|
import { skeletonSlotRecipe } from '../../styled-system/recipes';
|
|
5
5
|
/**
|
|
6
6
|
* The Skeleton component is used to indicate loading state of content before the data gets loaded.
|
|
7
|
+
*
|
|
8
|
+
* The root element is decorative and hidden from assistive technologies with `aria-hidden="true"` by default.
|
|
7
9
|
*/
|
|
8
|
-
export const Skeleton = forwardRef(({ className }, ref) => {
|
|
10
|
+
export const Skeleton = forwardRef(({ className, ...props }, ref) => {
|
|
9
11
|
const classes = skeletonSlotRecipe();
|
|
10
|
-
|
|
12
|
+
// `aria-hidden` is placed before the spread so that consumers can override it.
|
|
13
|
+
return _jsx("div", { ref: ref, "aria-hidden": "true", className: cx(classes.root, 'mfui-Skeleton__root', className), ...props });
|
|
11
14
|
});
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef } from 'react';
|
|
1
2
|
/**
|
|
2
3
|
* The props for the Skeleton component.
|
|
4
|
+
*
|
|
5
|
+
* The root element is decorative and is rendered with `aria-hidden="true"` by default, so it is
|
|
6
|
+
* omitted from the accessibility tree. Expose the loading state on a surrounding element instead
|
|
7
|
+
* (e.g. `role="status"` with visually hidden text, or `aria-busy`).
|
|
8
|
+
* Pass `aria-hidden={false}` to override the default.
|
|
9
|
+
*
|
|
10
|
+
* `children` is not accepted because the root is hidden from assistive technologies.
|
|
3
11
|
*/
|
|
4
12
|
export type SkeletonProps = {
|
|
5
13
|
/**
|
|
6
14
|
* The additional class name for the root element.
|
|
7
15
|
*/
|
|
8
16
|
className?: string;
|
|
9
|
-
}
|
|
17
|
+
} & Omit<ComponentPropsWithoutRef<'div'>, 'children'>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The VisuallyHidden component hides its content visually while keeping it available to assistive technologies.
|
|
3
|
+
*
|
|
4
|
+
* Use it to provide text for screen readers that is not shown on screen, for example an accessible name
|
|
5
|
+
* for a heading that only renders a `<Skeleton />` while loading.
|
|
6
|
+
* Use it instead of `aria-label` on role-less elements such as `<span>` or `<div>`, where `aria-label` is prohibited.
|
|
7
|
+
*
|
|
8
|
+
* Do not combine it with `aria-hidden` or `display: none`, because that removes the content from the accessibility tree.
|
|
9
|
+
*
|
|
10
|
+
* Also extends the props of `<span>` element.
|
|
11
|
+
*
|
|
12
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span
|
|
13
|
+
*/
|
|
14
|
+
export declare const VisuallyHidden: import("react").ForwardRefExoticComponent<{
|
|
15
|
+
children?: import("react").ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
} & Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cx } from '../../styled-system/css';
|
|
4
|
+
import { visuallyHiddenSlotRecipe } from '../../styled-system/recipes';
|
|
5
|
+
/**
|
|
6
|
+
* The VisuallyHidden component hides its content visually while keeping it available to assistive technologies.
|
|
7
|
+
*
|
|
8
|
+
* Use it to provide text for screen readers that is not shown on screen, for example an accessible name
|
|
9
|
+
* for a heading that only renders a `<Skeleton />` while loading.
|
|
10
|
+
* Use it instead of `aria-label` on role-less elements such as `<span>` or `<div>`, where `aria-label` is prohibited.
|
|
11
|
+
*
|
|
12
|
+
* Do not combine it with `aria-hidden` or `display: none`, because that removes the content from the accessibility tree.
|
|
13
|
+
*
|
|
14
|
+
* Also extends the props of `<span>` element.
|
|
15
|
+
*
|
|
16
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span
|
|
17
|
+
*/
|
|
18
|
+
export const VisuallyHidden = forwardRef(({ className, children, ...props }, ref) => {
|
|
19
|
+
const classes = visuallyHiddenSlotRecipe();
|
|
20
|
+
return (_jsx("span", { ref: ref, className: cx(classes.root, 'mfui-VisuallyHidden__root', className), ...props, children: children }));
|
|
21
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The props for the VisuallyHidden component.
|
|
4
|
+
*
|
|
5
|
+
* Also extends the props of `<span>` element.
|
|
6
|
+
*/
|
|
7
|
+
export type VisuallyHiddenProps = {
|
|
8
|
+
/**
|
|
9
|
+
* The content that is hidden visually but still exposed to assistive technologies.
|
|
10
|
+
*
|
|
11
|
+
* Keep it to non-interactive content such as text. Focusable elements would receive keyboard
|
|
12
|
+
* focus while staying invisible, which confuses sighted keyboard users.
|
|
13
|
+
*/
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* The additional class name for the root element.
|
|
17
|
+
*/
|
|
18
|
+
className?: string;
|
|
19
|
+
} & ComponentPropsWithoutRef<'span'>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { VisuallyHidden } from './VisuallyHidden';
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -105,7 +105,6 @@ export function usePopoverMenuPlacement({ minWidth, maxHeight, allowedPlacements
|
|
|
105
105
|
Object.assign(elements.floating.style, {
|
|
106
106
|
maxWidth: `${String(viewportWidth - horizontalMargin * 2)}px`,
|
|
107
107
|
maxHeight: maxHeightPx,
|
|
108
|
-
overflow: 'hidden',
|
|
109
108
|
minWidth: 'auto', // Reset minWidth for centered layout
|
|
110
109
|
});
|
|
111
110
|
}
|
|
@@ -114,8 +113,6 @@ export function usePopoverMenuPlacement({ minWidth, maxHeight, allowedPlacements
|
|
|
114
113
|
Object.assign(elements.floating.style, {
|
|
115
114
|
// Vertical padding for Popover height (12px) matching token: padding-main-container-vertical-comfort
|
|
116
115
|
maxHeight: maxHeightPx,
|
|
117
|
-
// Prevent root element from scrolling; child elements handle overflow
|
|
118
|
-
overflow: 'hidden',
|
|
119
116
|
// Constrain width to exactly match the trigger element
|
|
120
117
|
minWidth: referenceWidthPx,
|
|
121
118
|
maxWidth: referenceWidthPx,
|
|
@@ -128,8 +125,6 @@ export function usePopoverMenuPlacement({ minWidth, maxHeight, allowedPlacements
|
|
|
128
125
|
maxWidth: `calc(${String(Math.max(0, availableWidth))}px - 14px)`,
|
|
129
126
|
// Vertical padding for Popover height (12px) matching token: padding-main-container-vertical-comfort
|
|
130
127
|
maxHeight: maxHeightPx,
|
|
131
|
-
// Prevent root element from scrolling; child elements handle overflow
|
|
132
|
-
overflow: 'hidden',
|
|
133
128
|
// Make the menu to be resized at least to the width of the trigger
|
|
134
129
|
minWidth: minWidth ?? referenceWidthPx,
|
|
135
130
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether the code runs in a production build, so that development-only warnings can be skipped.
|
|
3
|
+
*
|
|
4
|
+
* The read is written so that `tsc` emits it in the dot form `process.env.NODE_ENV`, which bundlers replace with a
|
|
5
|
+
* string literal. In a production bundle, the check therefore folds to a constant even when the browser has no
|
|
6
|
+
* `process` global.
|
|
7
|
+
*
|
|
8
|
+
* When nothing replaces the read and `process` is not defined, such as on the Storybook Vite dev server for
|
|
9
|
+
* unbundled source, the bare read throws a `ReferenceError`. That environment is treated as non-production:
|
|
10
|
+
* that is where the development warnings are meant to be seen.
|
|
11
|
+
*
|
|
12
|
+
* @returns `true` only when `NODE_ENV` is `'production'`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isProduction(): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns whether the code runs in a production build, so that development-only warnings can be skipped.
|
|
3
|
+
*
|
|
4
|
+
* The read is written so that `tsc` emits it in the dot form `process.env.NODE_ENV`, which bundlers replace with a
|
|
5
|
+
* string literal. In a production bundle, the check therefore folds to a constant even when the browser has no
|
|
6
|
+
* `process` global.
|
|
7
|
+
*
|
|
8
|
+
* When nothing replaces the read and `process` is not defined, such as on the Storybook Vite dev server for
|
|
9
|
+
* unbundled source, the bare read throws a `ReferenceError`. That environment is treated as non-production:
|
|
10
|
+
* that is where the development warnings are meant to be seen.
|
|
11
|
+
*
|
|
12
|
+
* @returns `true` only when `NODE_ENV` is `'production'`.
|
|
13
|
+
*/
|
|
14
|
+
export function isProduction() {
|
|
15
|
+
try {
|
|
16
|
+
// The cast lets `noPropertyAccessFromIndexSignature` accept the dot form, which bundlers only replace in this form.
|
|
17
|
+
return process.env.NODE_ENV === 'production';
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|