-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(multi-select): add Popover subcomponent (#4044)
* 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
1 parent
e1770fb
commit 209ef69
Showing
10 changed files
with
339 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--- | ||
--- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
packages/components/src/MultiSelect/subcomponents/Popover/Popover.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
59 changes: 59 additions & 0 deletions
59
packages/components/src/MultiSelect/subcomponents/Popover/Popover.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
}) | ||
}) | ||
}) | ||
}) |
86 changes: 86 additions & 0 deletions
86
packages/components/src/MultiSelect/subcomponents/Popover/Popover.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
94 changes: 94 additions & 0 deletions
94
...s/components/src/MultiSelect/subcomponents/Popover/_docs/Popover.stickersheet.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" }, | ||
} |
83 changes: 83 additions & 0 deletions
83
packages/components/src/MultiSelect/subcomponents/Popover/_docs/Popover.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}, | ||
}, | ||
}, | ||
} |
2 changes: 2 additions & 0 deletions
2
packages/components/src/MultiSelect/subcomponents/Popover/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./Popover" | ||
export { useFloating } from "@floating-ui/react-dom" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters