Skip to content

Commit

Permalink
feat(multi-select): add Popover subcomponent (#4044)
Browse files Browse the repository at this point in the history
* chore(deps): add @floating-ui/react-dom

* chore(deps): add devDep @types/react-dom

* feat(multi-select): add Popover subcomponent

---------

Co-authored-by: Cassandra Tam <cassandra.tam@cultureamp.com>
Co-authored-by: Michael Winter <mcwinter07@gmail.com>
  • Loading branch information
3 people authored Sep 7, 2023
1 parent e1770fb commit 209ef69
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .changeset/popular-rocks-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 2 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"dependencies": {
"@cultureamp/i18n-react-intl": "^1.7.0",
"@floating-ui/react-dom": "^2.0.2",
"@kaizen/a11y": "^1.7.11",
"@kaizen/brand": "^1.5.8",
"@kaizen/date-picker": "^6.3.1",
Expand Down Expand Up @@ -81,6 +82,7 @@
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-typescript": "^11.1.3",
"@types/react-dom": "^18.2.7",
"autoprefixer": "^10.4.15",
"concat-cli": "^4.0.0",
"esbuild": "^0.18.17",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import "~@kaizen/design-tokens/sass/border";
@import "~@kaizen/design-tokens/sass/color";
@import "~@kaizen/design-tokens/sass/shadow";

.popover {
border-radius: $border-solid-border-radius;
box-shadow: $shadow-large-box-shadow;
background: $color-white;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useEffect, useRef, useState } from "react"
import { render, waitFor } from "@testing-library/react"
import { Popover, PopoverProps, useFloating } from "./"

const PopoverWrapper = (customProps?: Partial<PopoverProps>): JSX.Element => {
const { refs } = useFloating()
return (
<Popover {...customProps} refs={refs}>
Hello
</Popover>
)
}

describe("<Popover />", () => {
describe("Portals", () => {
const PopoverWrapperWithPortal = ({
shouldUsePortal = false,
}: {
shouldUsePortal?: boolean
}): JSX.Element => {
const portalRef = useRef<HTMLDivElement>(null)
const [portalContainer, setPortalContainer] = useState<HTMLDivElement>()

useEffect(() => {
if (portalRef.current !== null) {
setPortalContainer(portalRef.current)
}
}, [])

return (
<>
<div ref={portalRef} data-testid="portal-container" />
<PopoverWrapper
portalContainer={shouldUsePortal ? portalContainer : undefined}
/>
</>
)
}

it("renders within portal container", async () => {
const { getByTestId } = render(
<PopoverWrapperWithPortal shouldUsePortal />
)

await waitFor(() => {
expect(getByTestId("portal-container")).toHaveTextContent("Hello")
})
})

it("renders in document.body by default", async () => {
const { getByTestId } = render(<PopoverWrapperWithPortal />)

await waitFor(() => {
expect(document.body).toHaveTextContent("Hello")
expect(getByTestId("portal-container")).not.toHaveTextContent("Hello")
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { HTMLAttributes } from "react"
import { createPortal } from "react-dom"
import {
useFloating,
offset,
UseFloatingReturn,
UseFloatingOptions,
autoPlacement,
autoUpdate,
} from "@floating-ui/react-dom"
import classnames from "classnames"
import { FocusOn } from "react-focus-on"
import { ReactFocusOnProps } from "react-focus-on/dist/es5/types"
import { OverrideClassName } from "~types/OverrideClassName"
import styles from "./Popover.module.scss"

export type PopoverProps = {
children: React.ReactNode
refs: UseFloatingReturn["refs"]
/**
* passes in additional options / override for https://floating-ui.com/docs/tooltip#usefloating-hook
*/
floatingOptions?: Omit<UseFloatingOptions, "elements">
focusOnProps?: Omit<ReactFocusOnProps, "children">
portalContainer?: Element | DocumentFragment
} & OverrideClassName<HTMLAttributes<HTMLDivElement>>

export const Popover = ({
children,
refs,
floatingOptions,
focusOnProps,
portalContainer,
classNameOverride,
...restProps
}: PopoverProps): JSX.Element => {
const { floatingStyles } = useFloating({
elements: {
reference: refs.reference.current,
floating: refs.floating.current,
},
placement: "bottom-start",
middleware: [
offset(15),
autoPlacement(state =>
state.platform.isRTL?.(state.elements.reference)
? {
allowedPlacements: [
"bottom-end",
"bottom-start",
"top-end",
"top-start",
],
}
: {
allowedPlacements: [
"bottom-start",
"bottom-end",
"top-start",
"top-end",
],
}
),
],
whileElementsMounted: autoUpdate,
...floatingOptions,
})

return createPortal(
<FocusOn scrollLock={false} {...focusOnProps}>
<div
ref={refs.setFloating}
style={floatingStyles}
className={classnames(styles.popover, classNameOverride)}
role="dialog"
aria-modal="true"
{...restProps}
>
{children}
</div>
</FocusOn>,
portalContainer ?? document.body
)
}

Popover.displayName = "Popover"
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { useEffect, useRef, useState } from "react"
import { Meta } from "@storybook/react"
import { Heading } from "@kaizen/typography"
import { StickerSheetStory } from "~storybook/components/StickerSheet"
import { Popover, PopoverProps, useFloating } from "../index"

export default {
title: "Components/MultiSelect/Popover",
parameters: {
chromatic: { disable: false },
controls: { disable: true },
},
} satisfies Meta

const PopoverTemplate = (
args: Partial<Omit<PopoverProps, "refs">>
): JSX.Element => {
const [isOpen, setIsOpen] = useState<boolean>(true)
const { refs } = useFloating()

return (
<div>
<button
ref={refs.setReference}
type="button"
onClick={() => setIsOpen(!isOpen)}
>
Pancakes!
</button>
{isOpen && (
<Popover refs={refs} aria-label="Pancakes!" {...args}>
<div className="p-16">Content here!</div>
</Popover>
)}
</div>
)
}

const StickerSheetTemplate: StickerSheetStory = {
render: (_, { parameters }) => {
const portalRef = useRef<HTMLDivElement>(null)
const [portalContainer, setPortalContainer] = useState<HTMLDivElement>()

useEffect(() => {
if (portalRef.current !== null) {
setPortalContainer(portalRef.current)
}
}, [])

return (
<div
ref={portalRef}
id="stickersheet__popover"
// overflow-hidden is added so we can ensure the last row autoplaces above
className="relative flex flex-col justify-between gap-160 h-[500px] overflow-hidden"
>
<div>
<Heading variant="heading-3" tag="div" classNameOverride="!mb-16">
{parameters.textDirection === "rtl"
? "Default alignment to bottom-right; align to left when no space on left"
: "Default alignment to bottom-left; align to right when no space on right"}
</Heading>
<div className="flex justify-between w-[100%]">
<PopoverTemplate />
<PopoverTemplate />
<PopoverTemplate />
</div>
</div>

<div>
<Heading variant="heading-3" tag="div" classNameOverride="!mb-64">
Flips to top when no space below
</Heading>
<div>Content below popover</div>
<div className="flex justify-between w-[100%]">
<PopoverTemplate portalContainer={portalContainer} />
<PopoverTemplate portalContainer={portalContainer} />
</div>
</div>
</div>
)
},
}

export const StickerSheetDefault: StickerSheetStory = {
...StickerSheetTemplate,
name: "Sticker Sheet (Default)",
}

export const StickerSheetRTL: StickerSheetStory = {
...StickerSheetTemplate,
name: "Sticker Sheet (RTL)",
parameters: { textDirection: "rtl" },
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useState } from "react"
import { Meta, StoryObj } from "@storybook/react"
import { classNameOverrideArgType } from "~storybook/argTypes"
import { Popover, useFloating } from "../index"

const meta = {
title: "Components/MultiSelect/Popover",
component: Popover,
argTypes: { ...classNameOverrideArgType },
args: {
refs: undefined,
children: (
<div className="p-16">
<button type="button">Waffles</button>
<button type="button">Pikelets</button>
<button type="button">Crumpets</button>
</div>
),
},
decorators: [
Story => (
<div className="overflow-scroll max-w-[200px] max-h-[200px] border-solid">
<div
id="testing-ground"
className="relative flex justify-center items-center h-[500px] w-[500px]"
>
<Story />
</div>
</div>
),
],
} satisfies Meta<typeof Popover>

export default meta

type Story = StoryObj<typeof meta>

const PopoverTemplate: Story = {
render: args => {
const [isOpen, setIsOpen] = useState<boolean>(false)
const { refs } = useFloating()
const handleClose = (): void => setIsOpen(false)

return (
<div>
<button
ref={refs.setReference}
type="button"
onClick={() => setIsOpen(!isOpen)}
>
Pancakes!
</button>
<button type="button">Flapjacks</button>

{isOpen && (
<Popover
{...args}
portalContainer={
document.getElementById("testing-ground") ?? undefined
}
refs={refs}
focusOnProps={{
onClickOutside: handleClose,
onEscapeKey: handleClose,
shards: [refs.reference],
}}
/>
)}
</div>
)
},
}

export const Playground: Story = {
...PopoverTemplate,
parameters: {
docs: {
canvas: {
sourceState: "shown",
},
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./Popover"
export { useFloating } from "@floating-ui/react-dom"
2 changes: 1 addition & 1 deletion packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export * from "./FilterDatePicker"
export * from "./FilterDateRangePicker"
export * from "./FilterMultiSelect"
export * from "./FilterSelect"
export * from "./Icons"
export * from "./InputSearch"
export * from "./KaizenProvider"
export * from "./Workflow"
export * from "./Icons"
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2085,7 +2085,7 @@
"@floating-ui/core" "^1.4.1"
"@floating-ui/utils" "^0.1.1"

"@floating-ui/react-dom@^2.0.0":
"@floating-ui/react-dom@^2.0.0", "@floating-ui/react-dom@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.2.tgz#fab244d64db08e6bed7be4b5fcce65315ef44d20"
integrity sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==
Expand Down

0 comments on commit 209ef69

Please sign in to comment.