From c977e9f39d3c05ce8278d0d3b72df3a7c5ee662a Mon Sep 17 00:00:00 2001 From: Mayank <9084735+mayank99@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:47:40 -0400 Subject: [PATCH] enable `size` middleware for popovers (#2190) also added max-height to Popover --- .changeset/young-mayflies-listen.md | 7 +++ apps/website/src/content/docs/popover.mdx | 2 +- .../src/core/ComboBox/ComboBox.tsx | 1 + packages/itwinui-react/src/core/Menu/Menu.tsx | 4 ++ .../src/core/Popover/Popover.tsx | 43 ++++++++++++++++--- .../itwinui-react/src/core/Select/Select.tsx | 1 + 6 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 .changeset/young-mayflies-listen.md diff --git a/.changeset/young-mayflies-listen.md b/.changeset/young-mayflies-listen.md new file mode 100644 index 00000000000..9b949758c11 --- /dev/null +++ b/.changeset/young-mayflies-listen.md @@ -0,0 +1,7 @@ +--- +'@itwin/itwinui-react': minor +--- + +`Popover` now enables the [`size` middleware](https://floating-ui.com/docs/size) to prevent it from overflowing the viewport. +- This also affects other popover-like components (e.g. `Select`, `ComboBox`, `DropdownMenu`). +- `Popover` now also sets a default max-height of `400px` to prevent it from becoming too large. This can be customized using the `middleware.size.maxHeight` prop. diff --git a/apps/website/src/content/docs/popover.mdx b/apps/website/src/content/docs/popover.mdx index f650624d742..5fe2c7b76d6 100644 --- a/apps/website/src/content/docs/popover.mdx +++ b/apps/website/src/content/docs/popover.mdx @@ -36,7 +36,7 @@ Popover handles positioning using an external library called [Floating UI](float There are some advanced positioning options available. - The `positionReference` prop can be used to position the popover relative to a different element than the trigger. This can be useful, for example, when the trigger is part of a larger group of elements that should all be considered together. -- The `middleware` prop exposes more granular control over the positioning logic. By default, `Popover` enables the [`flip`](https://floating-ui.com/docs/flip) and [`shift`](https://floating-ui.com/docs/shift) middlewares. You might also find the [`offset`](https://floating-ui.com/docs/offset) middleware useful for adding a gap between the trigger and the popover. +- The `middleware` prop exposes more granular control over the positioning logic. By default, `Popover` enables the [`flip`](https://floating-ui.com/docs/flip), [`shift`](https://floating-ui.com/docs/shift) and [`size`](https://floating-ui.com/docs/size) middlewares. You might also find the [`offset`](https://floating-ui.com/docs/offset) middleware useful for adding a gap between the trigger and the popover. ### Portals diff --git a/packages/itwinui-react/src/core/ComboBox/ComboBox.tsx b/packages/itwinui-react/src/core/ComboBox/ComboBox.tsx index 5bc8672a90a..68c2fb13967 100644 --- a/packages/itwinui-react/src/core/ComboBox/ComboBox.tsx +++ b/packages/itwinui-react/src/core/ComboBox/ComboBox.tsx @@ -572,6 +572,7 @@ export const ComboBox = React.forwardRef( visible: isOpen, onVisibleChange: (open) => (open ? show() : hide()), matchWidth: true, + middleware: { size: { maxHeight: 'var(--iui-menu-max-height)' } }, closeOnOutsideClick: true, interactions: { click: false, focus: true }, }); diff --git a/packages/itwinui-react/src/core/Menu/Menu.tsx b/packages/itwinui-react/src/core/Menu/Menu.tsx index fed9850ab99..4cad7e58bd3 100644 --- a/packages/itwinui-react/src/core/Menu/Menu.tsx +++ b/packages/itwinui-react/src/core/Menu/Menu.tsx @@ -179,6 +179,10 @@ export const Menu = React.forwardRef((props, ref) => { ...restInteractionsProps, }, ...restPopoverProps, + middleware: { + size: { maxHeight: 'var(--iui-menu-max-height)' }, + ...restPopoverProps.middleware, + }, }); const { getReferenceProps, getFloatingProps, getItemProps } = useInteractions( diff --git a/packages/itwinui-react/src/core/Popover/Popover.tsx b/packages/itwinui-react/src/core/Popover/Popover.tsx index 9c72b24b23d..fd89e1e29ec 100644 --- a/packages/itwinui-react/src/core/Popover/Popover.tsx +++ b/packages/itwinui-react/src/core/Popover/Popover.tsx @@ -74,7 +74,7 @@ type PopoverOptions = { /** * Middleware options. * - * By default, `flip` and `shift` are enabled. + * By default, `flip`, `shift` and `size` are enabled. * * @see https://floating-ui.com/docs/middleware */ @@ -82,6 +82,15 @@ type PopoverOptions = { offset?: number; flip?: boolean; shift?: boolean; + size?: + | boolean + | { + /** + * Override the maximum height of the popover. Must be a CSS-compatible value. + * @default "400px" + */ + maxHeight?: string; + }; autoPlacement?: boolean; hide?: boolean; inline?: boolean; @@ -176,10 +185,13 @@ export const usePopover = (options: PopoverOptions & PopoverInternalProps) => { const tree = useFloatingTree(); const middleware = React.useMemo( - () => ({ flip: true, shift: true, ...options.middleware }), + () => ({ flip: true, shift: true, size: true, ...options.middleware }), [options.middleware], ); + const maxHeight = + typeof middleware.size === 'boolean' ? '400px' : middleware.size?.maxHeight; + const [open, onOpenChange] = useControlledState( false, visible, @@ -204,11 +216,17 @@ export const usePopover = (options: PopoverOptions & PopoverInternalProps) => { middleware.offset !== undefined && offset(middleware.offset), middleware.flip && flip({ padding: 4 }), middleware.shift && shift({ padding: 4 }), - matchWidth && + (matchWidth || middleware.size) && size({ padding: 4, - apply: ({ rects }) => { - setReferenceWidth(rects.reference.width); + apply: ({ rects, availableHeight }) => { + if (middleware.size) { + setAvailableHeight(Math.round(availableHeight)); + } + + if (matchWidth) { + setReferenceWidth(rects.reference.width); + } }, } as SizeOptions), middleware.autoPlacement && autoPlacement({ padding: 4 }), @@ -251,6 +269,7 @@ export const usePopover = (options: PopoverOptions & PopoverInternalProps) => { ]); const [referenceWidth, setReferenceWidth] = React.useState(); + const [availableHeight, setAvailableHeight] = React.useState(); const getFloatingProps = React.useCallback( (userProps?: React.HTMLProps) => @@ -258,6 +277,10 @@ export const usePopover = (options: PopoverOptions & PopoverInternalProps) => { ...userProps, style: { ...floating.floatingStyles, + ...(middleware.size && + availableHeight && { + maxBlockSize: `min(${availableHeight}px, ${maxHeight})`, + }), zIndex: 9999, ...(matchWidth && referenceWidth ? { @@ -268,7 +291,15 @@ export const usePopover = (options: PopoverOptions & PopoverInternalProps) => { ...userProps?.style, }, }), - [floating.floatingStyles, interactions, matchWidth, referenceWidth], + [ + floating.floatingStyles, + interactions, + matchWidth, + referenceWidth, + middleware.size, + availableHeight, + maxHeight, + ], ); const getReferenceProps = React.useCallback( diff --git a/packages/itwinui-react/src/core/Select/Select.tsx b/packages/itwinui-react/src/core/Select/Select.tsx index 413851f7c7a..bbccca9e824 100644 --- a/packages/itwinui-react/src/core/Select/Select.tsx +++ b/packages/itwinui-react/src/core/Select/Select.tsx @@ -418,6 +418,7 @@ const CustomSelect = React.forwardRef((props, forwardedRef) => { visible: isOpen, matchWidth: true, closeOnOutsideClick: true, + middleware: { size: { maxHeight: 'var(--iui-menu-max-height)' } }, ...popoverProps, onVisibleChange: (open) => (open ? show() : hide()), });