@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.
@@ -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 viewport.
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
  */
@@ -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
- fallbackStrategy: 'bestFit',
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
- const placementBase = popoverPlacement.split('-')[0];
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,
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mezzanine-ui/react",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "React components for mezzanine-ui",
5
5
  "author": "Mezzanine",
6
6
  "repository": {
@@ -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 };