@mezzanine-ui/react 1.3.0 → 1.4.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/Dropdown/Dropdown.d.ts +6 -2
- package/Dropdown/Dropdown.js +14 -15
- package/Popper/Popper.d.ts +7 -0
- package/Popper/Popper.js +6 -1
- package/Select/Select.d.ts +8 -0
- package/Select/Select.js +2 -2
- package/package.json +1 -1
- package/utils/getElementRef.d.ts +27 -0
- package/utils/getElementRef.js +52 -0
package/Dropdown/Dropdown.d.ts
CHANGED
|
@@ -146,8 +146,12 @@ export interface DropdownProps extends DropdownItemSharedProps {
|
|
|
146
146
|
sameWidth?: boolean;
|
|
147
147
|
/**
|
|
148
148
|
* Whether to enable floating-ui `flip` middleware.
|
|
149
|
-
* When `true`, the dropdown automatically flips to the opposite side
|
|
150
|
-
* (e.g. `bottom-start` → `top-start`) if it would overflow the
|
|
149
|
+
* When `true`, the dropdown automatically flips to the opposite side along
|
|
150
|
+
* the main axis (e.g. `bottom-start` → `top-start`) if it would overflow the
|
|
151
|
+
* viewport, and the enter transition slides from the resolved side. The flip
|
|
152
|
+
* is main-axis only (no `shift`/`crossAxis`), so a `sameWidth` menu stays
|
|
153
|
+
* horizontally aligned with its anchor — matching the `InputTriggerPopper`
|
|
154
|
+
* behavior used by the DatePicker/TimePicker menus.
|
|
151
155
|
* Off by default to preserve existing placement behavior across consumers.
|
|
152
156
|
* @default false
|
|
153
157
|
*/
|
package/Dropdown/Dropdown.js
CHANGED
|
@@ -10,20 +10,10 @@ import Button from '../Button/Button.js';
|
|
|
10
10
|
import { useDocumentEvents } from '../hooks/useDocumentEvents.js';
|
|
11
11
|
import Translate from '../Transition/Translate.js';
|
|
12
12
|
import { composeRefs } from '../utils/composeRefs.js';
|
|
13
|
+
import { getElementRef } from '../utils/getElementRef.js';
|
|
13
14
|
import DropdownItem from './DropdownItem.js';
|
|
14
15
|
import Popper from '../Popper/Popper.js';
|
|
15
16
|
|
|
16
|
-
/**
|
|
17
|
-
* Extracts ref from a ReactElement, supporting both React 18 and 19.
|
|
18
|
-
* In React 18, ref is on the element itself; in React 19, ref is in props.
|
|
19
|
-
*/
|
|
20
|
-
function getElementRef(element) {
|
|
21
|
-
var _a;
|
|
22
|
-
// React 19: ref is in props
|
|
23
|
-
const propsRef = (_a = element.props) === null || _a === void 0 ? void 0 : _a.ref;
|
|
24
|
-
// React 18: ref is on the element itself
|
|
25
|
-
return propsRef !== null && propsRef !== void 0 ? propsRef : element.ref;
|
|
26
|
-
}
|
|
27
17
|
/**
|
|
28
18
|
* 下拉選單元件,以 `Button` 或 `Input` 作為觸發元素,點擊後展開選項列表。
|
|
29
19
|
*
|
|
@@ -190,6 +180,9 @@ function Dropdown(props) {
|
|
|
190
180
|
}
|
|
191
181
|
return 'bottom';
|
|
192
182
|
}, [inputPosition, placement]);
|
|
183
|
+
// Tracks the placement actually resolved by floating-ui. Only meaningful when
|
|
184
|
+
// `flip` is enabled, where it may differ from `popoverPlacement` after a flip.
|
|
185
|
+
const [resolvedPlacement, setResolvedPlacement] = useState(popoverPlacement);
|
|
193
186
|
const customWidthMiddleware = useMemo(() => {
|
|
194
187
|
if (!customWidth) {
|
|
195
188
|
return null;
|
|
@@ -224,8 +217,11 @@ function Dropdown(props) {
|
|
|
224
217
|
const flipMiddleware = useMemo(() => {
|
|
225
218
|
if (!flip$1)
|
|
226
219
|
return null;
|
|
220
|
+
// Main-axis flip only (bottom <-> top), aligned with `InputTriggerPopper`.
|
|
221
|
+
// No `shift`/`crossAxis` so a sameWidth menu keeps its horizontal alignment
|
|
222
|
+
// with the anchor instead of being pushed sideways.
|
|
227
223
|
return flip({
|
|
228
|
-
|
|
224
|
+
fallbackAxisSideDirection: 'end',
|
|
229
225
|
padding: 8,
|
|
230
226
|
});
|
|
231
227
|
}, [flip$1]);
|
|
@@ -254,9 +250,12 @@ function Dropdown(props) {
|
|
|
254
250
|
if (isInline) {
|
|
255
251
|
return 'bottom';
|
|
256
252
|
}
|
|
257
|
-
|
|
253
|
+
// When flip is enabled, follow the placement resolved by floating-ui so the
|
|
254
|
+
// enter transition slides from the correct side after a flip. Otherwise keep
|
|
255
|
+
// the static placement to preserve existing behavior for non-flip consumers.
|
|
256
|
+
const placementBase = (flip$1 ? resolvedPlacement : popoverPlacement).split('-')[0];
|
|
258
257
|
return placementBase === 'top' ? 'top' : 'bottom';
|
|
259
|
-
}, [isInline, popoverPlacement]);
|
|
258
|
+
}, [flip$1, isInline, popoverPlacement, resolvedPlacement]);
|
|
260
259
|
const setOpen = useCallback((next) => {
|
|
261
260
|
const nextValue = typeof next === 'function'
|
|
262
261
|
? next(isOpen)
|
|
@@ -564,7 +563,7 @@ function Dropdown(props) {
|
|
|
564
563
|
}, [isInline, isOpen, setOpen]);
|
|
565
564
|
return (jsxs("div", { id: id, ref: containerRef, className: cx(dropdownClasses.root, dropdownClasses.inputPosition(inputPosition)), children: [isInline && (jsxs(TransitionGroup, { component: null, children: [!isOpen && inlineTriggerElement && (createElement(Translate, { ...translateProps, from: translateFrom, key: "inline-trigger", in: true },
|
|
566
565
|
jsx("div", { children: inlineTriggerElement }))), isOpen && (createElement(Translate, { ...translateProps, from: translateFrom, key: "inline-list", in: true },
|
|
567
|
-
jsx("div", { children: jsx(DropdownItem, { ...baseDropdownItemProps, headerContent: inlineTriggerElement }) })))] })), !isInline && (jsx(Popper, { ref: popperRef, anchor: anchorRef, className: dropdownClasses.popperWithPortal, controllerRef: popperControllerRef, open: isOpen, disablePortal: !globalPortal, options: {
|
|
566
|
+
jsx("div", { children: jsx(DropdownItem, { ...baseDropdownItemProps, headerContent: inlineTriggerElement }) })))] })), !isInline && (jsx(Popper, { ref: popperRef, anchor: anchorRef, className: dropdownClasses.popperWithPortal, controllerRef: popperControllerRef, onPlacementChange: setResolvedPlacement, open: isOpen, disablePortal: !globalPortal, options: {
|
|
568
567
|
placement: popoverPlacement,
|
|
569
568
|
middleware: [
|
|
570
569
|
offsetMiddleware,
|
package/Popper/Popper.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface PopperProps extends Pick<PortalProps, 'container' | 'disablePor
|
|
|
23
23
|
* Provide `controllerRef` if you need access to `useFloating` results.
|
|
24
24
|
*/
|
|
25
25
|
controllerRef?: Ref<PopperController>;
|
|
26
|
+
/**
|
|
27
|
+
* Callback fired whenever the resolved placement changes, including when
|
|
28
|
+
* floating-ui middleware (e.g. `flip`) flips the popper to the opposite
|
|
29
|
+
* side. Receives the actual placement after all middleware run, which may
|
|
30
|
+
* differ from `options.placement`.
|
|
31
|
+
*/
|
|
32
|
+
onPlacementChange?: (placement: PopperPlacement) => void;
|
|
26
33
|
/**
|
|
27
34
|
* The portal element will show if open=true
|
|
28
35
|
* @default false
|
package/Popper/Popper.js
CHANGED
|
@@ -10,7 +10,7 @@ import Portal from '../Portal/Portal.js';
|
|
|
10
10
|
|
|
11
11
|
const Popper = forwardRef(function Popper(props, ref) {
|
|
12
12
|
var _a, _b, _c, _d;
|
|
13
|
-
const { anchor, arrow: arrow$1, children, container, controllerRef, disablePortal, open = false, options, style, ...rest } = props;
|
|
13
|
+
const { anchor, arrow: arrow$1, children, container, controllerRef, disablePortal, onPlacementChange, open = false, options, style, ...rest } = props;
|
|
14
14
|
const arrowRef = useRef(null);
|
|
15
15
|
const anchorEl = getElement(anchor);
|
|
16
16
|
const floatingReturn = useFloating({
|
|
@@ -42,6 +42,11 @@ const Popper = forwardRef(function Popper(props, ref) {
|
|
|
42
42
|
update();
|
|
43
43
|
}
|
|
44
44
|
}, [open, update]);
|
|
45
|
+
// Notify consumers of the resolved placement so they can react to
|
|
46
|
+
// middleware-driven flips (e.g. adjusting enter-transition direction).
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
onPlacementChange === null || onPlacementChange === void 0 ? void 0 : onPlacementChange(floatingReturn.placement);
|
|
49
|
+
}, [floatingReturn.placement, onPlacementChange]);
|
|
45
50
|
// 計算箭頭的位置和旋轉角度
|
|
46
51
|
const arrowX = (_b = floatingReturn.middlewareData.arrow) === null || _b === void 0 ? void 0 : _b.x;
|
|
47
52
|
const arrowY = (_c = floatingReturn.middlewareData.arrow) === null || _c === void 0 ? void 0 : _c.y;
|
package/Select/Select.d.ts
CHANGED
|
@@ -54,6 +54,14 @@ export interface SelectBaseProps extends Omit<SelectTriggerProps, 'active' | 'in
|
|
|
54
54
|
* The z-index of the dropdown.
|
|
55
55
|
*/
|
|
56
56
|
dropdownZIndex?: number | string;
|
|
57
|
+
/**
|
|
58
|
+
* Whether to enable floating-ui `flip` middleware for the dropdown menu.
|
|
59
|
+
* When `true`, the menu flips from below to above the input (and back) along
|
|
60
|
+
* the main axis if it would overflow the viewport, keeping its width and
|
|
61
|
+
* horizontal alignment with the input. Forwarded to the underlying `Dropdown`.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
flip?: boolean;
|
|
57
65
|
/**
|
|
58
66
|
* Whether to enable portal for the dropdown.
|
|
59
67
|
* @default true
|
package/Select/Select.js
CHANGED
|
@@ -45,7 +45,7 @@ import cx from 'clsx';
|
|
|
45
45
|
*/
|
|
46
46
|
const Select = forwardRef(function Select(props, ref) {
|
|
47
47
|
const { disabled: disabledFromFormControl, fullWidth: fullWidthFromFormControl, required: requiredFromFormControl, severity, } = useContext(FormControlContext) || {};
|
|
48
|
-
const { className, clearable = false, defaultValue, disabled = disabledFromFormControl || false, error = severity === 'error' || false, fullWidth = fullWidthFromFormControl || false, inputProps, inputRef, loading = false, loadingPosition = 'bottom', loadingText, menuMaxHeight, mode = 'single', onBlur, onChange: onChangeProp, onClear: onClearProp, onFocus, onLeaveBottom, onReachBottom, onScroll, options: optionsProp, placeholder = '', prefix, readOnly = false, renderValue, required = requiredFromFormControl || false, overflowStrategy, size, suffixActionIcon, type = 'default', value: valueProp, dropdownZIndex, globalPortal = true, } = props;
|
|
48
|
+
const { className, clearable = false, defaultValue, disabled = disabledFromFormControl || false, error = severity === 'error' || false, fullWidth = fullWidthFromFormControl || false, inputProps, inputRef, loading = false, loadingPosition = 'bottom', loadingText, menuMaxHeight, mode = 'single', onBlur, onChange: onChangeProp, onClear: onClearProp, onFocus, onLeaveBottom, onReachBottom, onScroll, options: optionsProp, placeholder = '', prefix, readOnly = false, renderValue, required = requiredFromFormControl || false, overflowStrategy, size, suffixActionIcon, type = 'default', value: valueProp, dropdownZIndex, flip = false, globalPortal = true, } = props;
|
|
49
49
|
const dropdownStatus = loading
|
|
50
50
|
? 'loading'
|
|
51
51
|
: undefined;
|
|
@@ -256,7 +256,7 @@ const Select = forwardRef(function Select(props, ref) {
|
|
|
256
256
|
onChange,
|
|
257
257
|
value,
|
|
258
258
|
}), [onChange, value]);
|
|
259
|
-
return (jsx(SelectControlContext.Provider, { value: context, children: jsx("div", { ref: nodeRef, className: cx(selectClasses.host, fullWidth && selectClasses.hostFullWidth, mode && selectClasses.hostMode(mode)), children: jsx(Dropdown, { disabled: readOnly || disabled, globalPortal: globalPortal, loadingPosition: loadingPosition, loadingText: loadingText, maxHeight: menuMaxHeight, mode: mode, onLeaveBottom: onLeaveBottom, onReachBottom: onReachBottom, onScroll: onScroll, onSelect: handleDropdownSelect, onVisibilityChange: handleVisibilityChange, open: readOnly ? false : open, options: options, sameWidth: true, status: dropdownStatus, type: dropdownType, value: dropdownValue, zIndex: dropdownZIndex, children: jsx(SelectTrigger, { ref: composedRef, active: !readOnly && open, className: className, clearable: clearable, disabled: disabled, error: error, fullWidth: fullWidth, inputRef: inputRef, mode: mode, onTagClose: onChange, onClear: onClear, onKeyDown: onKeyDownTextField, prefix: prefix, readOnly: readOnly, ...(mode === 'single' && renderValue ? { renderValue } : {}), required: required, inputProps: resolvedInputProps, overflowStrategy: overflowStrategy, size: size, suffixActionIcon: suffixActionIcon, value: value === null ? undefined : value, placeholder: getPlaceholder() }) }) }) }));
|
|
259
|
+
return (jsx(SelectControlContext.Provider, { value: context, children: jsx("div", { ref: nodeRef, className: cx(selectClasses.host, fullWidth && selectClasses.hostFullWidth, mode && selectClasses.hostMode(mode)), children: jsx(Dropdown, { disabled: readOnly || disabled, flip: flip, globalPortal: globalPortal, loadingPosition: loadingPosition, loadingText: loadingText, maxHeight: menuMaxHeight, mode: mode, onLeaveBottom: onLeaveBottom, onReachBottom: onReachBottom, onScroll: onScroll, onSelect: handleDropdownSelect, onVisibilityChange: handleVisibilityChange, open: readOnly ? false : open, options: options, sameWidth: true, status: dropdownStatus, type: dropdownType, value: dropdownValue, zIndex: dropdownZIndex, children: jsx(SelectTrigger, { ref: composedRef, active: !readOnly && open, className: className, clearable: clearable, disabled: disabled, error: error, fullWidth: fullWidth, inputRef: inputRef, mode: mode, onTagClose: onChange, onClear: onClear, onKeyDown: onKeyDownTextField, prefix: prefix, readOnly: readOnly, ...(mode === 'single' && renderValue ? { renderValue } : {}), required: required, inputProps: resolvedInputProps, overflowStrategy: overflowStrategy, size: size, suffixActionIcon: suffixActionIcon, value: value === null ? undefined : value, placeholder: getPlaceholder() }) }) }) }));
|
|
260
260
|
});
|
|
261
261
|
|
|
262
262
|
export { Select as default };
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ReactElement, Ref } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Helper type to extract ref from a ReactElement.
|
|
4
|
+
* Models `ref` on the element itself, which is compatible with React 18 and 19.
|
|
5
|
+
*/
|
|
6
|
+
export type ReactElementWithRef<P, E extends Element = HTMLElement> = ReactElement<P> & {
|
|
7
|
+
ref?: Ref<E>;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Extracts ref from a ReactElement, supporting both React 18 and 19.
|
|
11
|
+
* In React 18, ref is on the element itself; in React 19, ref is in props.
|
|
12
|
+
*
|
|
13
|
+
* Reading the "wrong" location in dev mode triggers React warning getters:
|
|
14
|
+
*
|
|
15
|
+
* - React 18 installs a warning getter on `props.ref` when the element was
|
|
16
|
+
* created with a ref — accessing it logs
|
|
17
|
+
* "`ref` is not a prop. Trying to access it will result in `undefined` being returned."
|
|
18
|
+
* - React 19 installs a deprecation getter on `element.ref` when the element
|
|
19
|
+
* was created with a ref — accessing it logs
|
|
20
|
+
* "Accessing element.ref was removed in React 19."
|
|
21
|
+
*
|
|
22
|
+
* So instead of unconditionally reading `props.ref` first, detect the
|
|
23
|
+
* dev-mode warning getters (marked with `isReactWarning`) and read the ref
|
|
24
|
+
* from the location where it actually lives. Same approach as
|
|
25
|
+
* `getElementRef` in radix-ui/primitives.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getElementRef<E extends Element = HTMLElement>(element: ReactElementWithRef<unknown, E>): Ref<E> | undefined;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the given property getter is a React dev-mode warning getter.
|
|
3
|
+
* React marks them with `isReactWarning = true`
|
|
4
|
+
* (see `defineRefPropWarningGetter` in the React source).
|
|
5
|
+
*/
|
|
6
|
+
function isReactWarningGetter(getter) {
|
|
7
|
+
return (typeof getter === 'function' &&
|
|
8
|
+
Boolean(getter.isReactWarning));
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extracts ref from a ReactElement, supporting both React 18 and 19.
|
|
12
|
+
* In React 18, ref is on the element itself; in React 19, ref is in props.
|
|
13
|
+
*
|
|
14
|
+
* Reading the "wrong" location in dev mode triggers React warning getters:
|
|
15
|
+
*
|
|
16
|
+
* - React 18 installs a warning getter on `props.ref` when the element was
|
|
17
|
+
* created with a ref — accessing it logs
|
|
18
|
+
* "`ref` is not a prop. Trying to access it will result in `undefined` being returned."
|
|
19
|
+
* - React 19 installs a deprecation getter on `element.ref` when the element
|
|
20
|
+
* was created with a ref — accessing it logs
|
|
21
|
+
* "Accessing element.ref was removed in React 19."
|
|
22
|
+
*
|
|
23
|
+
* So instead of unconditionally reading `props.ref` first, detect the
|
|
24
|
+
* dev-mode warning getters (marked with `isReactWarning`) and read the ref
|
|
25
|
+
* from the location where it actually lives. Same approach as
|
|
26
|
+
* `getElementRef` in radix-ui/primitives.
|
|
27
|
+
*/
|
|
28
|
+
function getElementRef(element) {
|
|
29
|
+
var _a, _b, _c;
|
|
30
|
+
const props = element.props;
|
|
31
|
+
// React 18 dev mode: `props.ref` is a warning getter; the actual ref
|
|
32
|
+
// lives on the element itself.
|
|
33
|
+
const propsRefGetter = props
|
|
34
|
+
? (_a = Object.getOwnPropertyDescriptor(props, 'ref')) === null || _a === void 0 ? void 0 : _a.get
|
|
35
|
+
: undefined;
|
|
36
|
+
if (isReactWarningGetter(propsRefGetter)) {
|
|
37
|
+
return element.ref;
|
|
38
|
+
}
|
|
39
|
+
// React 19 dev mode: `element.ref` may be a deprecation warning getter;
|
|
40
|
+
// the actual ref lives in props as a regular property.
|
|
41
|
+
const elementRefGetter = (_b = Object.getOwnPropertyDescriptor(element, 'ref')) === null || _b === void 0 ? void 0 : _b.get;
|
|
42
|
+
if (isReactWarningGetter(elementRefGetter)) {
|
|
43
|
+
return props === null || props === void 0 ? void 0 : props.ref;
|
|
44
|
+
}
|
|
45
|
+
// No warning getters (production builds, or no ref was given):
|
|
46
|
+
// prefer `props.ref` (React 19), fall back to `element.ref` (React 18).
|
|
47
|
+
// Safe on React 19 dev — its `element.ref` deprecation getter is only
|
|
48
|
+
// installed when a ref exists, in which case `props.ref` is returned here.
|
|
49
|
+
return (_c = props === null || props === void 0 ? void 0 : props.ref) !== null && _c !== void 0 ? _c : element.ref;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { getElementRef };
|