Skip to content

Commit

Permalink
Add the portalContainerId in Select Future release (#4328)
Browse files Browse the repository at this point in the history
* feat(Select): Add the portalContainerId prop

* docs(Select): update future docs on portals

* docs(Select): update future docs following refactor



Co-authored-by: Geoffrey Chong <[email protected]>
  • Loading branch information
mcwinter07 and gyfchong authored Nov 22, 2023
1 parent 06cb427 commit a374c07
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/happy-tips-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kaizen/components": minor
---

Add portalContainerId prop the future Select to allow portals to other DOM elements.
80 changes: 78 additions & 2 deletions packages/components/src/__future__/Select/Select.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react"
import { render, waitFor } from "@testing-library/react"
import { render, waitFor, screen, within } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { Select, SelectProps } from "./Select"
import { singleMockItems } from "./_docs/mockData"
Expand All @@ -17,7 +17,6 @@ const SelectWrapper = ({
)
return (
<Select
id="id--select"
label="Mock Label"
items={items}
description="This is a description"
Expand Down Expand Up @@ -356,4 +355,81 @@ describe("<Select />", () => {
})
})
})

describe("Popover portal", () => {
it("has accessible trigger controls", async () => {
render(<SelectWrapper isOpen />)

const trigger = screen.getByRole("combobox", {
name: "Mock Label",
})

await waitFor(() => {
expect(trigger).toHaveAttribute("aria-controls")
})
})

it("will portal to the document body by default", async () => {
render(<SelectWrapper selectedKey="batch-brew" isOpen />)

const popover = screen.getByRole("dialog")
// expected div that FocusOn adds to the popover
const popoverFocusWrapper = popover.parentNode

await waitFor(() => {
const expectedBodyTag = popoverFocusWrapper?.parentNode
expect(expectedBodyTag?.nodeName).toEqual("BODY")
})
})

it("will render as a descendant of the element matching the id", async () => {
const SelectWithPortal = (): JSX.Element => {
const portalContainerId = "id--portal-container"
return (
<>
<div
id={portalContainerId}
data-testid="id--portal-container-test"
></div>
<SelectWrapper
selectedKey="batch-brew"
isOpen
portalContainerId={portalContainerId}
/>
</>
)
}
render(<SelectWithPortal />)

await waitFor(() => {
const newPortalRegion = screen.getByTestId("id--portal-container-test")
const popover = within(newPortalRegion).getByRole("dialog")

expect(popover).toBeInTheDocument()
})
})

it("will portal to the document body if the id does not match", async () => {
const SelectWithPortal = (): JSX.Element => {
const expectedContainerId = "id--portal-container"
return (
<>
<div id="id--wrong-id"></div>
<SelectWrapper
selectedKey="batch-brew"
isOpen
portalContainerId={expectedContainerId}
/>
</>
)
}
render(<SelectWithPortal />)

await waitFor(() => {
const popover = within(document.body).getByRole("dialog")

expect(popover).toBeInTheDocument()
})
})
})
})
20 changes: 18 additions & 2 deletions packages/components/src/__future__/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useId } from "react"
import React, { useEffect, useId, useState } from "react"
import { UseFloatingReturn } from "@floating-ui/react-dom"
import { useButton } from "@react-aria/button"
import { HiddenSelect, useSelect } from "@react-aria/select"
Expand Down Expand Up @@ -67,6 +67,10 @@ export type SelectProps<Option extends SelectOption = SelectOption> = {
* @deprecated: Either define `disabled` in your `Option` (in `items`), or use `disabledKeys`
*/
disabledValues?: Key[]
/**
* Creates a portal for the Popover to the matching element id
*/
portalContainerId?: string
} & OverrideClassName<Omit<AriaSelectProps<Option>, OmittedAriaSelectProps>>

/**
Expand All @@ -89,13 +93,14 @@ export const Select = <Option extends SelectOption = SelectOption>({
description,
placeholder,
isDisabled,
portalContainerId,
...restProps
}: SelectProps<Option>): JSX.Element => {
const { refs } = useFloating<HTMLButtonElement>()
const triggerRef = refs.reference

const id = propsId ?? useId()
const descriptionId = `${id}--description`
const popoverId = `${id}--popover`

const disabledKeys = getDisabledKeysFromItems(items)

Expand Down Expand Up @@ -151,6 +156,15 @@ export const Select = <Option extends SelectOption = SelectOption>({
ref: refs.setReference,
}

const [portalContainer, setPortalContainer] = useState<HTMLElement>()

useEffect(() => {
if (portalContainerId) {
const portalElement = document.getElementById(portalContainerId)
portalElement && setPortalContainer(portalElement)
}
}, [])

return (
<div
className={classnames(
Expand All @@ -173,6 +187,8 @@ export const Select = <Option extends SelectOption = SelectOption>({
)}
{state.isOpen && (
<Popover
id={popoverId}
portalContainer={portalContainer}
refs={refs}
focusOnProps={{
onEscapeKey: state.close,
Expand Down
8 changes: 8 additions & 0 deletions packages/components/src/__future__/Select/_docs/Select.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ Add validation messages using `status` and `validationMessage`.

Set `isFullWidth` to `true` to have the Select span the full width of its container.
<Canvas of={SelectStories.FullWidth} />

### Portals

By default, the Select's popover will attach itself to the `body` of the document using React's `createPortal`.

You can change the default behaviour by providing a `portalContainerId` to attach this to different element in the DOM. This can help to resolve issues that may arise with `z-index` or having a Select in a modal.

<Canvas of={SelectStories.PortalContainer} />
29 changes: 29 additions & 0 deletions packages/components/src/__future__/Select/_docs/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,32 @@ export const Validation: Story = {
export const FullWidth: Story = {
args: { isFullWidth: true },
}

export const PortalContainer: Story = {
render: args => {
const portalContainerId = "id--portal-container"
return (
<>
<div
id={portalContainerId}
className="flex gap-24 bg-gray-200 p-12 overflow-hidden h-[200px] relative"
>
<Select
{...args}
label="Default"
selectedKey="batch-brew"
id="id--select-default"
/>
<Select
{...args}
label="Inner portal"
selectedKey="batch-brew"
id="id--select-inner"
portalContainerId={portalContainerId}
/>
</div>
</>
)
},
parameters: { docs: { source: { type: "code" } } },
}

0 comments on commit a374c07

Please sign in to comment.