@mezzanine-ui/react 1.3.1 → 1.4.1

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
  */
@@ -180,6 +180,9 @@ function Dropdown(props) {
180
180
  }
181
181
  return 'bottom';
182
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);
183
186
  const customWidthMiddleware = useMemo(() => {
184
187
  if (!customWidth) {
185
188
  return null;
@@ -214,8 +217,11 @@ function Dropdown(props) {
214
217
  const flipMiddleware = useMemo(() => {
215
218
  if (!flip$1)
216
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.
217
223
  return flip({
218
- fallbackStrategy: 'bestFit',
224
+ fallbackAxisSideDirection: 'end',
219
225
  padding: 8,
220
226
  });
221
227
  }, [flip$1]);
@@ -244,9 +250,12 @@ function Dropdown(props) {
244
250
  if (isInline) {
245
251
  return 'bottom';
246
252
  }
247
- 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];
248
257
  return placementBase === 'top' ? 'top' : 'bottom';
249
- }, [isInline, popoverPlacement]);
258
+ }, [flip$1, isInline, popoverPlacement, resolvedPlacement]);
250
259
  const setOpen = useCallback((next) => {
251
260
  const nextValue = typeof next === 'function'
252
261
  ? next(isOpen)
@@ -554,7 +563,7 @@ function Dropdown(props) {
554
563
  }, [isInline, isOpen, setOpen]);
555
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 },
556
565
  jsx("div", { children: inlineTriggerElement }))), isOpen && (createElement(Translate, { ...translateProps, from: translateFrom, key: "inline-list", in: true },
557
- 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: {
558
567
  placement: popoverPlacement,
559
568
  middleware: [
560
569
  offsetMiddleware,
@@ -24,9 +24,6 @@ const NavigationOption = forwardRef((props, ref) => {
24
24
  const currentKey = id || title || href || uuid;
25
25
  const currentPath = useMemo(() => [...parentPath, currentKey], [parentPath, currentKey]);
26
26
  const currentPathKey = currentPath.join('::');
27
- const Component = href && !children
28
- ? ((_a = anchorComponent !== null && anchorComponent !== void 0 ? anchorComponent : optionsAnchorComponent) !== null && _a !== void 0 ? _a : 'a')
29
- : 'div';
30
27
  const flattenedChildren = useMemo(() => flattenChildren(children), [children]);
31
28
  const { badge, items } = useMemo(() => {
32
29
  let badgeComponent = null;
@@ -49,6 +46,12 @@ const NavigationOption = forwardRef((props, ref) => {
49
46
  });
50
47
  return { badge: badgeComponent, items };
51
48
  }, [flattenedChildren]);
49
+ // Group vs leaf is decided by the presence of real sub-options, not by
50
+ // raw `children` — a lone `Badge` child is rendered inline on a leaf.
51
+ const hasSubOptions = items.length > 0;
52
+ const Component = href && !hasSubOptions
53
+ ? ((_a = anchorComponent !== null && anchorComponent !== void 0 ? anchorComponent : optionsAnchorComponent) !== null && _a !== void 0 ? _a : 'a')
54
+ : 'div';
52
55
  // Default open if current path is activated
53
56
  useEffect(() => {
54
57
  if (activatedPathKey === currentPathKey ||
@@ -83,7 +86,7 @@ const NavigationOption = forwardRef((props, ref) => {
83
86
  resizeObserver.disconnect();
84
87
  };
85
88
  }, [title]);
86
- return (jsxs("li", { ...rest, ref: ref, className: cx(navigationOptionClasses.host, open && navigationOptionClasses.open, !children && navigationOptionClasses.basic, (active !== null && active !== void 0 ? active : (activatedPath === null || activatedPath === void 0 ? void 0 : activatedPath[currentLevel - 1]) === currentKey) &&
89
+ return (jsxs("li", { ...rest, ref: ref, className: cx(navigationOptionClasses.host, open && navigationOptionClasses.open, !hasSubOptions && navigationOptionClasses.basic, (active !== null && active !== void 0 ? active : (activatedPath === null || activatedPath === void 0 ? void 0 : activatedPath[currentLevel - 1]) === currentKey) &&
87
90
  navigationOptionClasses.active, collapsed && navigationOptionClasses.collapsed, (collapsed && collapsedHiddenKeys.has(currentKey)) ||
88
91
  (!collapsed && !filter)
89
92
  ? navigationOptionClasses.hidden
@@ -95,16 +98,18 @@ const NavigationOption = forwardRef((props, ref) => {
95
98
  if (collapsed) {
96
99
  handleCollapseChange(false);
97
100
  }
98
- if (!children)
101
+ if (!hasSubOptions)
99
102
  setActivatedPath(currentPath);
100
103
  }, onKeyDown: (e) => {
101
104
  if (e.key === 'Enter' || e.key === ' ') {
102
105
  e.preventDefault();
103
106
  setOpen(!open);
104
- if (!children)
107
+ if (!hasSubOptions)
105
108
  setActivatedPath(currentPath);
106
109
  }
107
- }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ref: tooltipChildRef, role: "menuitem", tabIndex: 0, children: [icon && jsx(Icon, { className: navigationOptionClasses.icon, icon: icon }), jsx("span", { className: navigationOptionClasses.titleWrapper, children: jsx(Fade, { ref: titleRef, in: collapsed === false || !icon, children: jsx("span", { className: navigationOptionClasses.title, children: collapsed && !icon ? Array.from(title).slice(0, 2).join('') : title }) }) }), badge, children && (jsx(Icon, { className: navigationOptionClasses.toggleIcon, icon: GroupToggleIcon }))] })) }), children && !collapsed && (jsx(Collapse, { lazyMount: true, className: cx(navigationOptionClasses.childrenWrapper), in: open, children: jsx(NavigationOptionLevelContext.Provider, { value: {
110
+ }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ref: tooltipChildRef, role: "menuitem", tabIndex: 0, children: [icon && jsx(Icon, { className: navigationOptionClasses.icon, icon: icon }), jsx("span", { className: navigationOptionClasses.titleWrapper, children: jsx(Fade, { ref: titleRef, in: collapsed === false || !icon, children: jsx("span", { className: navigationOptionClasses.title, children: collapsed && !icon
111
+ ? Array.from(title).slice(0, 2).join('')
112
+ : title }) }) }), badge, hasSubOptions && (jsx(Icon, { className: navigationOptionClasses.toggleIcon, icon: GroupToggleIcon }))] })) }), hasSubOptions && !collapsed && (jsx(Collapse, { lazyMount: true, className: cx(navigationOptionClasses.childrenWrapper), in: open, children: jsx(NavigationOptionLevelContext.Provider, { value: {
108
113
  level: currentLevel,
109
114
  path: currentPath,
110
115
  }, children: jsx("ul", { className: navigationOptionClasses.group, children: items }) }) }))] }));
@@ -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 };
@@ -11,7 +11,7 @@ import cx from 'clsx';
11
11
  const isMultipleSelection = (props) => props.mode === 'multiple';
12
12
  function SelectTriggerComponent(props) {
13
13
  var _a, _b, _c;
14
- const { active, className, clearable: clearableProp = false, disabled, forceHideSuffixActionIcon, inputProps, innerRef, inputRef, isForceClearable = false, mode = 'single', onTagClose, overflowStrategy = 'counter', placeholder, readOnly, required, searchText, size = 'main', showTextInputAfterTags = false, suffixAction, suffixActionIcon: suffixActionIconProp, type = 'default', onClick, ...restTextFieldProps } = props;
14
+ const { active, className, clearable: clearableProp = false, disabled, error, forceHideSuffixActionIcon, inputProps, innerRef, inputRef, isForceClearable = false, mode = 'single', onTagClose, overflowStrategy = 'counter', placeholder, readOnly, required, searchText, size = 'main', showTextInputAfterTags = false, suffixAction, suffixActionIcon: suffixActionIconProp, type = 'default', onClick, ...restTextFieldProps } = props;
15
15
  const renderValueProp = 'renderValue' in props ? props.renderValue : undefined;
16
16
  // Exclude renderValue to avoid leaking unknown props to DOM.
17
17
  const sanitizedTextFieldProps = (() => {
@@ -60,9 +60,7 @@ function SelectTriggerComponent(props) {
60
60
  return (jsxs(TextField, { ref: innerRef, ...interactiveProps, ...sanitizedTextFieldProps, onClick: onClick, active: active, className: cx(selectClasses.trigger, selectClasses.triggerMode(mode), selectClasses.triggerSelected(Array.isArray(props.value) ? (_a = props.value) === null || _a === void 0 ? void 0 : _a.length : props.value), {
61
61
  [selectClasses.triggerReadOnly]: readOnly,
62
62
  [selectClasses.triggerDisabled]: disabled,
63
- }, className), error: type === 'error', clearable: shouldEnableClearable, forceShowClearable: shouldEnableClearable, size: size, suffix: forceHideSuffixActionIcon ? undefined : suffixActionIcon, children: [jsx("input", { ...inputProps, ref: inputRef, "aria-autocomplete": "list", "aria-haspopup": "listbox", autoComplete: "off", className: cx(selectClasses.triggerInput, inputProps === null || inputProps === void 0 ? void 0 : inputProps.className), disabled: disabled, placeholder: placeholder, readOnly: (_b = inputProps === null || inputProps === void 0 ? void 0 : inputProps.readOnly) !== null && _b !== void 0 ? _b : true, required: required, type: "text", value: renderValue() }), isMultipleSelection(props) && ((_c = props.value) === null || _c === void 0 ? void 0 : _c.length)
64
- ? (jsx(SelectTriggerTags, { disabled: disabled, overflowStrategy: overflowStrategy, inputProps: inputProps, inputRef: inputRef, onTagClose: onTagClose, readOnly: readOnly, required: required, searchText: searchText, size: size, showTextInputAfterTags: showTextInputAfterTags, value: props.value }))
65
- : null] }));
63
+ }, className), error: error || type === 'error', clearable: shouldEnableClearable, forceShowClearable: shouldEnableClearable, size: size, suffix: forceHideSuffixActionIcon ? undefined : suffixActionIcon, children: [jsx("input", { ...inputProps, ref: inputRef, "aria-autocomplete": "list", "aria-haspopup": "listbox", autoComplete: "off", className: cx(selectClasses.triggerInput, inputProps === null || inputProps === void 0 ? void 0 : inputProps.className), disabled: disabled, placeholder: placeholder, readOnly: (_b = inputProps === null || inputProps === void 0 ? void 0 : inputProps.readOnly) !== null && _b !== void 0 ? _b : true, required: required, type: "text", value: renderValue() }), isMultipleSelection(props) && ((_c = props.value) === null || _c === void 0 ? void 0 : _c.length) ? (jsx(SelectTriggerTags, { disabled: disabled, overflowStrategy: overflowStrategy, inputProps: inputProps, inputRef: inputRef, onTagClose: onTagClose, readOnly: readOnly, required: required, searchText: searchText, size: size, showTextInputAfterTags: showTextInputAfterTags, value: props.value })) : null] }));
66
64
  }
67
65
  const SelectTrigger = forwardRef((props, ref) => {
68
66
  if (props.mode === 'multiple') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mezzanine-ui/react",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
4
4
  "description": "React components for mezzanine-ui",
5
5
  "author": "Mezzanine",
6
6
  "repository": {