diff --git a/.changeset/rotten-jobs-pull.md b/.changeset/rotten-jobs-pull.md new file mode 100644 index 0000000000..c467de7d62 --- /dev/null +++ b/.changeset/rotten-jobs-pull.md @@ -0,0 +1,6 @@ +--- +"@nextui-org/use-aria-multiselect": patch +"@nextui-org/select": patch +--- + +add hideEmptyContent API to select diff --git a/apps/docs/content/docs/components/select.mdx b/apps/docs/content/docs/components/select.mdx index 570cfa86ca..7d05d3190d 100644 --- a/apps/docs/content/docs/components/select.mdx +++ b/apps/docs/content/docs/components/select.mdx @@ -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: "-" }, @@ -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" }, @@ -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", diff --git a/packages/components/select/__tests__/select.test.tsx b/packages/components/select/__tests__/select.test.tsx index 8f90aa7678..470cd130ef 100644 --- a/packages/components/select/__tests__/select.test.tsx +++ b/packages/components/select/__tests__/select.test.tsx @@ -828,6 +828,57 @@ describe("Select", () => { "Invalid value", ); }); + + it("should not open dropdown when hideEmptyContent is true", async () => { + const wrapper = render( + + {[]} + , + ); + + 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( + + {[]} + , + ); + + 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", () => { diff --git a/packages/components/select/src/use-select.ts b/packages/components/select/src/use-select.ts index 9fd67a73c5..6cc12c628e 100644 --- a/packages/components/select/src/use-select.ts +++ b/packages/components/select/src/use-select.ts @@ -166,6 +166,11 @@ export type UseSelectProps = 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(originalProps: UseSelectProps) { @@ -209,6 +214,7 @@ export function useSelect(originalProps: UseSelectProps) { onClose, className, classNames, + hideEmptyContent = false, ...otherProps } = props; @@ -263,6 +269,7 @@ export function useSelect(originalProps: UseSelectProps) { isDisabled: originalProps.isDisabled, isInvalid: originalProps.isInvalid, defaultOpen, + hideEmptyContent, onOpenChange: (open) => { onOpenChange?.(open); if (!open) { diff --git a/packages/components/select/stories/select.stories.tsx b/packages/components/select/stories/select.stories.tsx index f768f58e49..70920b4247 100644 --- a/packages/components/select/stories/select.stories.tsx +++ b/packages/components/select/stories/select.stories.tsx @@ -411,6 +411,31 @@ const StartContentTemplate = ({color, variant, ...args}: SelectProps) => ( ); +const EmptyTemplate = ({color, variant, ...args}: SelectProps) => ( + + + {[]} + + + {[]} + + +); + const CustomItemsTemplate = ({color, variant, ...args}: SelectProps) => ( * @default true */ shouldFlip?: boolean; + /** + * Whether the menu should be hidden when there are no items. + */ + hideEmptyContent?: boolean; } export interface MultiSelectState @@ -82,6 +86,8 @@ export function useMultiSelectState(props: MultiSelectProps): M value: listState.selectedKeys, }); + const shouldHideContent = listState.collection.size === 0 && props.hideEmptyContent; + return { ...validationState, ...listState, @@ -91,18 +97,17 @@ export function useMultiSelectState(props: MultiSelectProps): 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,