Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(select): pass portalContainer to the pop-over #3698

Open
wants to merge 4 commits into
base: canary
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-eagles-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nextui-org/select": patch
---

Currently, the select component does not pass the `portalContainer` prop to its child pop-over. Consequently, the pop-over defaults to using `document.body` as the `portalContainer`. This causes the pop-over to close immediately when the parent of the select component is scrollable. This fix ensures that the `portalContainer` prop is correctly passed to the pop-over, addressing the issue.
2 changes: 1 addition & 1 deletion apps/docs/content/components/select/open-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function App() {
const [isOpen, setIsOpen] = React.useState(false);

return (
<div className="flex w-full max-w-xs items-center gap-2">
<div className="flex w-full max-w-xs gap-2">
<Select
isOpen={isOpen}
label="Favorite Animal"
Expand Down
50 changes: 26 additions & 24 deletions apps/docs/content/docs/components/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ NextUI exports 3 select-related components:

## Usage

<CodeDemo title="Usage" files={selectContent.usage} />
<CodeDemo title="Usage" files={selectContent.usage} previewHeight="350px" />

### Dynamic items

Expand All @@ -54,13 +54,13 @@ Select follows the [Collection Components API](https://react-spectrum.adobe.com/
- **Static**: The usage example above shows the static implementation, which can be used when the full list of options is known ahead of time.
- **Dynamic**: The example below can be used when the options come from an external data source such as an API call, or update over time.

<CodeDemo title="Dynamic items" highlightedLines="8" files={selectContent.dynamic} />
<CodeDemo title="Dynamic items" highlightedLines="8" files={selectContent.dynamic} previewHeight="350px" />

### Multiple Selection

You can use the `selectionMode="multiple"` property to allow multiple selection.

<CodeDemo title="Multiple Selection" files={selectContent.multiple} />
<CodeDemo title="Multiple Selection" files={selectContent.multiple} previewHeight="350px" />

### Disabled

Expand All @@ -70,36 +70,36 @@ You can use the `selectionMode="multiple"` property to allow multiple selection.

You can disable specific items by using the `disabledKeys` property.

<CodeDemo title="Disabled Items" highlightedLines="10" files={selectContent.disabledItems} />
<CodeDemo title="Disabled Items" highlightedLines="10" files={selectContent.disabledItems} previewHeight="350px" />

### Required

If you pass the `isRequired` property to the select, it will have a `danger` asterisk at
the end of the label and the select will be required.

<CodeDemo title="Required" highlightedLines="8" files={selectContent.required} />
<CodeDemo title="Required" highlightedLines="8" files={selectContent.required} previewHeight="350px" />

### Sizes

<CodeDemo title="Sizes" highlightedLines="13,24" files={selectContent.sizes} />
<CodeDemo title="Sizes" highlightedLines="13,24" files={selectContent.sizes} previewHeight="500px" />

### Colors

<CodeDemo title="Colors" files={selectContent.colors} />
<CodeDemo title="Colors" files={selectContent.colors} previewHeight="550px" />

### Variants

<CodeDemo title="Variants" files={selectContent.variants} />
<CodeDemo title="Variants" files={selectContent.variants} previewHeight="550px" />

### Radius

<CodeDemo title="Radius" files={selectContent.radius} />
<CodeDemo title="Radius" files={selectContent.radius} previewHeight="500px" />

### Label Placements

You can change the position of the label by setting the `labelPlacement` property to `inside`, `outside` or `outside-left`.

<CodeDemo title="Label Placements" highlightedLines="19,37" files={selectContent.labelPlacements} />
<CodeDemo title="Label Placements" highlightedLines="19,37" files={selectContent.labelPlacements} previewHeight="500px" />

> **Note**: If the `label` is not passed, the `labelPlacement` property will be `outside` by default.

Expand All @@ -108,22 +108,22 @@ You can change the position of the label by setting the `labelPlacement` propert
You can use the `startContent` and `endContent` properties to add content to the start and end of
the select.

<CodeDemo title="Start Content" highlightedLines="13" files={selectContent.startContent} />
<CodeDemo title="Start Content" highlightedLines="13" files={selectContent.startContent} previewHeight="350px"/>

### Item Start & End Content

Since the `Select` component uses the [Listbox](/docs/components/listbox) component under the hood, you can
use the `startContent` and `endContent` properties of the `SelectItem` component to add content to the start
and end of the select item.

<CodeDemo title="Item Start Content" files={selectContent.itemStartContent} />
<CodeDemo title="Item Start Content" files={selectContent.itemStartContent} previewHeight="350px" />

### Custom Selector Icon

By default the select uses a `chevron-down` icon as the selector icon which rotates when the select is open. You can
customize this icon by passing a custom one to the `selectorIcon` property.

<CodeDemo title="Custom Selector Icon" files={selectContent.customSelectorIcon} />
<CodeDemo title="Custom Selector Icon" files={selectContent.customSelectorIcon} previewHeight="350px" />

> **Note**: Use the `disableSelectorIconRotation` property to disable the rotation of the icon.

Expand All @@ -132,29 +132,29 @@ customize this icon by passing a custom one to the `selectorIcon` property.
Select component uses the [ScrollShadow](/docs/components/scroll-shadow) under the hood to show a shadow when the select content is scrollable.
You can disable this shadow by passing using the `scrollShadowProps` property.

<CodeDemo title="Without Scroll Shadow" files={selectContent.withoutScrollShadow} />
<CodeDemo title="Without Scroll Shadow" files={selectContent.withoutScrollShadow} previewHeight="350px" />

> **Note**: You can also use the `showScrollIndicators` property to disable the scroll indicators.

### With Description

You can add a description to the select by passing the `description` property.

<CodeDemo title="With Description" files={selectContent.description} />
<CodeDemo title="With Description" files={selectContent.description} previewHeight="350px" />

### With Error Message

You can combine the `isInvalid` and `errorMessage` properties to show an invalid select.

<CodeDemo title="With Error Message" files={selectContent.errorMessage} />
<CodeDemo title="With Error Message" files={selectContent.errorMessage} previewHeight="350px"/>

### Controlled

You can use the `selectedKeys` and `onSelectionChange` / `onChange` properties to control the select value.

Using `onSelectionChange`:

<CodeDemo title="Controlled with onSelectionChange" files={selectContent.singleControlled} />
<CodeDemo title="Controlled with onSelectionChange" files={selectContent.singleControlled} previewHeight="350px"/>

Using `onChange`:

Expand All @@ -164,19 +164,19 @@ Using `onChange`:

You can control the open state of the select by using the `isOpen` and `onOpenChange` / `onClose` properties.

<CodeDemo title="Controlling the open state" files={selectContent.openState} />
<CodeDemo title="Controlling the open state" files={selectContent.openState} previewHeight="350px" />

### Custom Items

You can customize the select items by modifying the `SelectItem` children.

<CodeDemo title="Custom Items" files={selectContent.customItems} />
<CodeDemo title="Custom Items" files={selectContent.customItems} previewHeight="350px" />

### Custom Render Value

By default the select will render the selected item's text value, but you can customize this by passing a `renderValue` function.

<CodeDemo title="Custom Render Value" files={selectContent.customRenderValue} />
<CodeDemo title="Custom Render Value" files={selectContent.customRenderValue} previewHeight="350px"/>

The `renderValue` function receives the selected items as a parameter and must return a
`ReactNode`. Check the [Render Value Function](#render-value-function) section for more details.
Expand Down Expand Up @@ -217,13 +217,13 @@ import {useInfiniteScroll} from "@nextui-org/use-infinite-scroll";

You can use the `SelectSection` component to group select items.

<CodeDemo title="With Sections" files={selectContent.sections} />
<CodeDemo title="With Sections" files={selectContent.sections} previewHeight="350px" />

### Custom Sections Style

You can customize the sections style by using the `classNames` property of the `SelectSection` component.

<CodeDemo title="Custom Sections Style" files={selectContent.customSectionsStyle} />
<CodeDemo title="Custom Sections Style" files={selectContent.customSectionsStyle} previewHeight="350px" />

### Multiple Select Controlled

Expand All @@ -234,21 +234,23 @@ Using `onSelectionChange`:
<CodeDemo
title="Multiple Selection Controlled with onSelectionChange"
files={selectContent.multipleControlled}
previewHeight="350px"
/>

Using `onChange`:

<CodeDemo
title="Multiple Selection Controlled with onChange"
files={selectContent.multipleControlledOnChange}
previewHeight="350px"
/>

### Multiple With Chips

You can render any component as the select value by using the `renderValue` property. In this example we are
using the [Chip](/docs/components/chip) component to render the selected items.

<CodeDemo title="Multiple Selection with Chips" files={selectContent.multipleWithChips} />
<CodeDemo title="Multiple Selection with Chips" files={selectContent.multipleWithChips} previewHeight="350px" />

> **Note**: Make sure to pass the `isMultiline` property to the `Select` component to allow the chips to wrap.

Expand All @@ -261,7 +263,7 @@ You can customize any slot of the select by using the `classNames` property. Sel
component also provides the [popoverProps](/docs/components/popover#api) and [listboxProps](/docs/components/listbox#api) properties to customize
the popover and listbox components.

<CodeDemo title="Custom Styles" files={selectContent.customStyles} />
<CodeDemo title="Custom Styles" files={selectContent.customStyles} previewHeight="500px" />

## Slots

Expand Down
43 changes: 42 additions & 1 deletion packages/components/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {SelectProps} from "../src";

import * as React from "react";
import {render, renderHook, act} from "@testing-library/react";
import {render, renderHook, act, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {useForm} from "react-hook-form";

Expand Down Expand Up @@ -720,6 +720,47 @@ describe("Select", () => {
expect(onChange).toBeCalledTimes(1);
});
});

it("should not close when parent is scrollable", async () => {
const wrapper = render(
<div className="h-screen w-screen m-10">
<Select
aria-label="Favorite Animal"
className="fixed bottom-2"
data-testid="select"
label="Favorite Animal"
>
{itemsData.map((item) => (
<SelectItem key={item.id} value={item.label}>
{item.label}
</SelectItem>
))}
</Select>
</div>,
);

const select = wrapper.getByTestId("select");

expect(select).not.toBeNull();

// open the select listbox by clicking selector button
fireEvent.click(select);

// assert that the select listbox is open
expect(select).toHaveAttribute("aria-expanded", "true");

const listboxItems = wrapper.getAllByRole("option");

expect(listboxItems.length).toBe(13);

await act(async () => {
await user.click(listboxItems[12]);
});

fireEvent.click(select);
// asserting that the click is able to open the pop-over(not getting closed due to the scroll)
expect(select).toHaveAttribute("aria-expanded", "true");
});
});

describe("Select with React Hook Form", () => {
Expand Down
7 changes: 6 additions & 1 deletion packages/components/select/src/use-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
const triggerRef = useRef<HTMLElement>(null);
const listBoxRef = useRef<HTMLUListElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const baseRef = useRef<HTMLDivElement>(null);

let state = useMultiSelectState<T>({
...props,
Expand Down Expand Up @@ -380,6 +381,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {

const getBaseProps: PropGetter = useCallback(
(props = {}) => ({
ref: baseRef,
"data-slot": "base",
"data-filled": dataAttr(isFilled),
"data-has-value": dataAttr(hasValue),
Expand All @@ -391,7 +393,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
}),
...props,
}),
[slots, hasHelper, hasValue, hasLabel, isFilled, baseStyles],
[baseRef, slots, hasHelper, hasValue, hasLabel, isFilled, baseStyles],
);

const getTriggerProps: PropGetter = useCallback(
Expand Down Expand Up @@ -519,6 +521,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
"data-slot": "popover",
scrollRef: listBoxRef,
triggerType: "listbox",
portalContainer: baseRef.current ?? undefined,
classNames: {
content: slots.popoverContent({
class: clsx(classNames?.popoverContent, props.className),
Expand All @@ -540,6 +543,8 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
classNames?.popoverContent,
slotsProps.popoverProps,
triggerRef,
listBoxRef,
baseRef,
state,
state.selectedItems,
],
Expand Down
Loading