Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .changeset/rotten-jobs-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@nextui-org/use-aria-multiselect": patch
"@nextui-org/select": patch
---

add hideEmptyContent API to select
10 changes: 8 additions & 2 deletions apps/docs/content/docs/components/select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ the popover and listbox components.
},
{
attribute: "endContent",
type: "ReactNode",
type: "ReactNode",
description: "Element to be rendered in the right side of the select.",
default: "-"
},
Expand Down Expand Up @@ -515,7 +515,7 @@ the popover and listbox components.
},
{
attribute: "itemHeight",
type: "number",
type: "number",
description: "The fixed height of each item in pixels. Required when using virtualization.",
default: "32"
},
Expand Down Expand Up @@ -603,6 +603,12 @@ the popover and listbox components.
description: "Whether the select should disable the rotation of the selector icon.",
default: "false"
},
{
attribute: "hideEmptyContent",
type: "boolean",
description: "Whether the listbox will be prevented from opening when there are no items.",
default: "false"
},
{
attribute: "popoverProps",
type: "PopoverProps",
Expand Down
51 changes: 51 additions & 0 deletions packages/components/select/__tests__/select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,57 @@ describe("Select", () => {
"Invalid value",
);
});

it("should not open dropdown when hideEmptyContent is true", async () => {
const wrapper = render(
<Select
hideEmptyContent
aria-label="Favorite Animal"
data-testid="hide-empty-content-true-test"
label="Favorite Animal"
>
{[]}
</Select>,
);

const select = wrapper.getByTestId("hide-empty-content-true-test");

// open the select dropdown
await user.click(select);

// assert that the select is not open
expect(select).not.toHaveAttribute("aria-expanded", "true");
// assert that the listbox is not rendered
expect(wrapper.queryByRole("listbox")).not.toBeInTheDocument();
});

it("should open dropdown when hideEmptyContent is false", async () => {
const wrapper = render(
<Select
aria-label="Favorite Animal"
data-testid="hide-empty-content-false-test"
hideEmptyContent={false}
label="Favorite Animal"
>
{[]}
</Select>,
);

const select = wrapper.getByTestId("hide-empty-content-false-test");

// open the select dropdown
await user.click(select);

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

const listbox = wrapper.getByRole("listbox");

// assert that the listbox is rendered
expect(listbox).toBeInTheDocument();
// assert that the listbox items are not rendered
expect(wrapper.queryByRole("option")).not.toBeInTheDocument();
});
});

describe("Select virtualization tests", () => {
Expand Down
7 changes: 7 additions & 0 deletions packages/components/select/src/use-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ export type UseSelectProps<T> = Omit<
* @default undefined
*/
isVirtualized?: boolean;
/**
* Whether the listbox will be prevented from opening when there are no items.
* @default false
*/
hideEmptyContent?: boolean;
};

export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
Expand Down Expand Up @@ -209,6 +214,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
onClose,
className,
classNames,
hideEmptyContent = false,
...otherProps
} = props;

Expand Down Expand Up @@ -263,6 +269,7 @@ export function useSelect<T extends object>(originalProps: UseSelectProps<T>) {
isDisabled: originalProps.isDisabled,
isInvalid: originalProps.isInvalid,
defaultOpen,
hideEmptyContent,
onOpenChange: (open) => {
onOpenChange?.(open);
if (!open) {
Expand Down
33 changes: 33 additions & 0 deletions packages/components/select/stories/select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,31 @@ const StartContentTemplate = ({color, variant, ...args}: SelectProps) => (
</Select>
);

const EmptyTemplate = ({color, variant, ...args}: SelectProps) => (
<div className="w-full justify-center flex gap-2">
<Select
hideEmptyContent
className="max-w-xs"
color={color}
label="Hide empty content"
variant={variant}
{...args}
>
{[]}
</Select>
<Select
className="max-w-xs"
color={color}
hideEmptyContent={false}
label="Show empty content"
variant={variant}
{...args}
>
{[]}
</Select>
</div>
);

const CustomItemsTemplate = ({color, variant, ...args}: SelectProps<User>) => (
<div className="w-full justify-center flex gap-2">
<Select
Expand Down Expand Up @@ -864,6 +889,14 @@ export const StartContent = {
},
};

export const EmptyContent = {
render: EmptyTemplate,

args: {
...defaultProps,
},
};

export const WithDescription = {
render: MirrorTemplate,

Expand Down
25 changes: 15 additions & 10 deletions packages/hooks/use-aria-multiselect/src/use-multiselect-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface MultiSelectProps<T>
* @default true
*/
shouldFlip?: boolean;
/**
* Whether the menu should be hidden when there are no items.
*/
hideEmptyContent?: boolean;
}

export interface MultiSelectState<T>
Expand Down Expand Up @@ -82,6 +86,8 @@ export function useMultiSelectState<T extends {}>(props: MultiSelectProps<T>): M
value: listState.selectedKeys,
});

const shouldHideContent = listState.collection.size === 0 && props.hideEmptyContent;

return {
...validationState,
...listState,
Expand All @@ -91,18 +97,17 @@ export function useMultiSelectState<T extends {}>(props: MultiSelectProps<T>): M
triggerState.close();
},
open(focusStrategy: FocusStrategy | null = null) {
// Don't open if the collection is empty.
if (listState.collection.size !== 0) {
setFocusStrategy(focusStrategy);
triggerState.open();
}
if (shouldHideContent) return;

setFocusStrategy(focusStrategy);
triggerState.open();
},
toggle(focusStrategy: FocusStrategy | null = null) {
if (listState.collection.size !== 0) {
setFocusStrategy(focusStrategy);
triggerState.toggle();
validationState.commitValidation();
}
if (shouldHideContent) return;

setFocusStrategy(focusStrategy);
triggerState.toggle();
validationState.commitValidation();
},
isFocused,
setFocused,
Expand Down