From 0bc4936619c398f92ff1cec57cae172b37aa4d11 Mon Sep 17 00:00:00 2001 From: Geoffrey Chong Date: Thu, 26 Oct 2023 13:46:04 +1100 Subject: [PATCH] KDS-1756-migrate-skirt-content-to-kaio-2 (#4211) * add Content + Container * add Skirt + SkirtCard * fix lint * add changeset * Update tough-beers-carry.md * Update packages/components/src/Container/_docs/Container.mdx Co-authored-by: Cassandra * Remove duplicate to save some money --------- Co-authored-by: Cassandra --- .changeset/tough-beers-carry.md | 5 ++ packages/components/package.json | 1 + .../src/Container/Container.module.scss | 13 ++++ .../components/src/Container/Container.tsx | 23 ++++++ .../src/Container/_docs/Container.mdx | 32 ++++++++ .../src/Container/_docs/Container.stories.tsx | 65 ++++++++++++++++ packages/components/src/Container/index.ts | 1 + .../src/Content/Content.module.scss | 12 +++ packages/components/src/Content/Content.tsx | 52 +++++++++++++ .../components/src/Content/_docs/Content.mdx | 31 ++++++++ .../src/Content/_docs/Content.stories.tsx | 63 +++++++++++++++ .../components/src/Content/_variables.scss | 3 + packages/components/src/Content/index.ts | 1 + .../components/src/Skirt/Skirt.module.scss | 32 ++++++++ packages/components/src/Skirt/Skirt.tsx | 76 +++++++++++++++++++ packages/components/src/Skirt/_docs/Skirt.mdx | 28 +++++++ .../src/Skirt/_docs/Skirt.stories.tsx | 50 ++++++++++++ packages/components/src/Skirt/index.ts | 1 + .../SkirtCard/SkirtCard.module.scss | 17 +++++ .../subcomponents/SkirtCard/SkirtCard.tsx | 18 +++++ .../Skirt/subcomponents/SkirtCard/index.ts | 1 + packages/components/src/index.ts | 3 + .../src/utils/useResizeObserver.spec.tsx | 49 ++++++++++++ .../components/src/utils/useResizeObserver.ts | 75 ++++++++++++++++++ 24 files changed, 652 insertions(+) create mode 100644 .changeset/tough-beers-carry.md create mode 100644 packages/components/src/Container/Container.module.scss create mode 100644 packages/components/src/Container/Container.tsx create mode 100644 packages/components/src/Container/_docs/Container.mdx create mode 100644 packages/components/src/Container/_docs/Container.stories.tsx create mode 100644 packages/components/src/Container/index.ts create mode 100644 packages/components/src/Content/Content.module.scss create mode 100644 packages/components/src/Content/Content.tsx create mode 100644 packages/components/src/Content/_docs/Content.mdx create mode 100644 packages/components/src/Content/_docs/Content.stories.tsx create mode 100644 packages/components/src/Content/_variables.scss create mode 100644 packages/components/src/Content/index.ts create mode 100644 packages/components/src/Skirt/Skirt.module.scss create mode 100644 packages/components/src/Skirt/Skirt.tsx create mode 100644 packages/components/src/Skirt/_docs/Skirt.mdx create mode 100644 packages/components/src/Skirt/_docs/Skirt.stories.tsx create mode 100644 packages/components/src/Skirt/index.ts create mode 100644 packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.module.scss create mode 100644 packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.tsx create mode 100644 packages/components/src/Skirt/subcomponents/SkirtCard/index.ts create mode 100644 packages/components/src/utils/useResizeObserver.spec.tsx create mode 100644 packages/components/src/utils/useResizeObserver.ts diff --git a/.changeset/tough-beers-carry.md b/.changeset/tough-beers-carry.md new file mode 100644 index 00000000000..d0f9615a281 --- /dev/null +++ b/.changeset/tough-beers-carry.md @@ -0,0 +1,5 @@ +--- +"@kaizen/components": minor +--- + +Migrate Skirt, Content, Container from `kaizen-legacy` diff --git a/packages/components/package.json b/packages/components/package.json index e0e16138c57..9d9ff505c9d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -83,6 +83,7 @@ "react-popper": "^2.3.0", "react-select": "^5.7.7", "react-textfit": "^1.1.1", + "resize-observer-polyfill": "^1.5.1", "tslib": "^2.6.2", "use-debounce": "^9.0.4", "uuid": "^9.0.1" diff --git a/packages/components/src/Container/Container.module.scss b/packages/components/src/Container/Container.module.scss new file mode 100644 index 00000000000..69ea25208e1 --- /dev/null +++ b/packages/components/src/Container/Container.module.scss @@ -0,0 +1,13 @@ +.container { + display: flex; + width: 100%; + justify-content: center; + + * { + &, + &::after, + &::before { + box-sizing: border-box; + } + } +} diff --git a/packages/components/src/Container/Container.tsx b/packages/components/src/Container/Container.tsx new file mode 100644 index 00000000000..22f418c343e --- /dev/null +++ b/packages/components/src/Container/Container.tsx @@ -0,0 +1,23 @@ +import React from "react" +import classnames from "classnames" +import { ContentProps } from "~components/Content" +import styles from "./Container.module.scss" + +/** + * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3086156812/Layout Guidance} | + * {@link https://cultureamp.design/?path=/docs/components-content--docs Storybook} + */ +export const Container = React.forwardRef( + ({ children, style, classNameOverride, ...restProps }, ref) => ( +
+ {children} +
+ ) +) + +Container.displayName = "Container" diff --git a/packages/components/src/Container/_docs/Container.mdx b/packages/components/src/Container/_docs/Container.mdx new file mode 100644 index 00000000000..29996367560 --- /dev/null +++ b/packages/components/src/Container/_docs/Container.mdx @@ -0,0 +1,32 @@ +import { Canvas, Controls, Meta } from "@storybook/blocks" +import { ResourceLinks, KaioNotification, Installation } from "~storybook/components" +import { LinkTo } from "~storybook/components/LinkTo" +import * as ContainerStories from "./Container.stories" + + + +# Container + + + + + + + +## Overview + +Wraps your entire page. + + + + +## Use Case +Usually wraps a Content component. + diff --git a/packages/components/src/Container/_docs/Container.stories.tsx b/packages/components/src/Container/_docs/Container.stories.tsx new file mode 100644 index 00000000000..26cd7db5506 --- /dev/null +++ b/packages/components/src/Container/_docs/Container.stories.tsx @@ -0,0 +1,65 @@ +import React from "react" +import { Meta, StoryObj } from "@storybook/react" +import { Content } from "~components/Content" +import { Text } from "~components/Text" +import { Container } from "../index" + +const meta = { + title: "Pages/Container", + component: Container, + args: { + children: ( +
+ + Bacon ipsum dolor amet andouille buffalo beef boudin kielbasa + drumstick fatback cow tongue ground round chicken. Jowl cow short + ribs, ham tongue turducken spare ribs pig drumstick chuck meatball. + Buffalo turducken pancetta tail salami chicken. Bresaola venison + pastrami beef. + + + Porchetta shankle ribeye, ground round beef filet mignon fatback + chislic boudin. Boudin burgdoggen spare ribs, meatloaf pastrami pork + loin meatball short ribs tenderloin ribeye cupim venison short loin + pork chop tongue. Andouille landjaeger bacon, picanha filet mignon + short ribs hamburger shank shoulder porchetta. Pork chop ground round + tenderloin, biltong kevin corned beef chuck frankfurter spare ribs + pork meatball pastrami fatback. Strip steak beef ribs pork loin kevin, + biltong fatback tongue salami brisket capicola flank tenderloin. + +
+ ), + }, + argTypes: { + children: { controls: "disabled" }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = { + parameters: { + docs: { + canvas: { + sourceState: "shown", + }, + }, + }, +} + +export const Example: Story = { + render: args => ( + + + + ), + parameters: { + docs: { + canvas: { + sourceState: "shown", + }, + }, + }, +} diff --git a/packages/components/src/Container/index.ts b/packages/components/src/Container/index.ts new file mode 100644 index 00000000000..74d4d9e41cb --- /dev/null +++ b/packages/components/src/Container/index.ts @@ -0,0 +1 @@ +export * from "./Container" diff --git a/packages/components/src/Content/Content.module.scss b/packages/components/src/Content/Content.module.scss new file mode 100644 index 00000000000..ecccbdbf258 --- /dev/null +++ b/packages/components/src/Content/Content.module.scss @@ -0,0 +1,12 @@ +@import "~@kaizen/design-tokens/sass/layout"; +@import "./variables"; + +.content { + max-width: $layout-content-max-width; + margin: 0 $layout-content-side-margin; + width: 100%; + + @media (max-width: calc(#{$layout-breakpoints-large} - 1px)) { + margin: 0 $content-margin-width-on-medium-and-small; + } +} diff --git a/packages/components/src/Content/Content.tsx b/packages/components/src/Content/Content.tsx new file mode 100644 index 00000000000..fb5d1fe6084 --- /dev/null +++ b/packages/components/src/Content/Content.tsx @@ -0,0 +1,52 @@ +import React, { HTMLAttributes } from "react" +import classnames from "classnames" +import { OverrideClassName } from "~types/OverrideClassName" +import styles from "./Content.module.scss" + +export type ContentProps = { + children?: React.ReactNode + /** + * Not recommended. A short-circuit for dynamically overriding layout in a pinch + */ + style?: Pick< + React.CSSProperties, + | "bottom" + | "left" + | "margin" + | "marginBottom" + | "marginLeft" + | "marginRight" + | "marginTop" + | "padding" + | "paddingBottom" + | "paddingLeft" + | "paddingRight" + | "paddingTop" + | "position" + | "right" + | "top" + | "transform" + | "transformBox" + | "transformOrigin" + | "transformStyle" + > +} & OverrideClassName> + +/** + * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3086156812/Layout Guidance} | + * {@link https://cultureamp.design/?path=/docs/components-content--docs Storybook} + */ +export const Content = React.forwardRef( + ({ children, style, classNameOverride, ...restProps }, ref) => ( +
+ {children} +
+ ) +) + +Content.displayName = "Content" diff --git a/packages/components/src/Content/_docs/Content.mdx b/packages/components/src/Content/_docs/Content.mdx new file mode 100644 index 00000000000..ed16e7cf15d --- /dev/null +++ b/packages/components/src/Content/_docs/Content.mdx @@ -0,0 +1,31 @@ +import { Canvas, Meta } from "@storybook/blocks" +import { ResourceLinks, KaioNotification, Installation } from "~storybook/components" +import { LinkTo } from "~storybook/components/LinkTo" +import * as ContentStories from "./Content.stories" + + + +# Content + + + + + + + +## Overview + +Wraps your content at a **page level** in the standard minimum width and margins. + + + +## Use Case +Usually wrapped in a Container + diff --git a/packages/components/src/Content/_docs/Content.stories.tsx b/packages/components/src/Content/_docs/Content.stories.tsx new file mode 100644 index 00000000000..1eff846f5dd --- /dev/null +++ b/packages/components/src/Content/_docs/Content.stories.tsx @@ -0,0 +1,63 @@ +import React from "react" +import { Meta, StoryObj } from "@storybook/react" +import { Container } from "~components/Container" +import { Text } from "~components/Text" +import { Content } from "../index" + +const meta = { + title: "Pages/Content", + component: Content, + args: { + children: ( + <> + + Bacon ipsum dolor amet andouille buffalo beef boudin kielbasa + drumstick fatback cow tongue ground round chicken. Jowl cow short + ribs, ham tongue turducken spare ribs pig drumstick chuck meatball. + Buffalo turducken pancetta tail salami chicken. Bresaola venison + pastrami beef. + + + Porchetta shankle ribeye, ground round beef filet mignon fatback + chislic boudin. Boudin burgdoggen spare ribs, meatloaf pastrami pork + loin meatball short ribs tenderloin ribeye cupim venison short loin + pork chop tongue. Andouille landjaeger bacon, picanha filet mignon + short ribs hamburger shank shoulder porchetta. Pork chop ground round + tenderloin, biltong kevin corned beef chuck frankfurter spare ribs + pork meatball pastrami fatback. Strip steak beef ribs pork loin kevin, + biltong fatback tongue salami brisket capicola flank tenderloin. + + + ), + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = { + parameters: { + docs: { + canvas: { + sourceState: "shown", + }, + }, + }, +} + +export const Example: Story = { + render: args => ( + + + + ), + parameters: { + chromatic: { disable: false }, + docs: { + canvas: { + sourceState: "shown", + }, + }, + }, +} diff --git a/packages/components/src/Content/_variables.scss b/packages/components/src/Content/_variables.scss new file mode 100644 index 00000000000..a4ea8cb54d1 --- /dev/null +++ b/packages/components/src/Content/_variables.scss @@ -0,0 +1,3 @@ +// This replicates the layout max-width logic in +// draft-packages/title-block-zen/KaizenDraft/TitleBlockZen/TitleBlockZen.scss +$content-margin-width-on-medium-and-small: 12px; diff --git a/packages/components/src/Content/index.ts b/packages/components/src/Content/index.ts new file mode 100644 index 00000000000..08d3aa4845a --- /dev/null +++ b/packages/components/src/Content/index.ts @@ -0,0 +1 @@ +export * from "./Content" diff --git a/packages/components/src/Skirt/Skirt.module.scss b/packages/components/src/Skirt/Skirt.module.scss new file mode 100644 index 00000000000..d0075011399 --- /dev/null +++ b/packages/components/src/Skirt/Skirt.module.scss @@ -0,0 +1,32 @@ +@import "~@kaizen/design-tokens/sass/color"; + +$dt-color-background-color-default: $color-purple-600; +$dt-color-background-color-education: $color-blue-100; + +.container { + position: relative; +} + +// Actual height is determined in JavaScript +.underlay { + box-sizing: content-box; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + height: 100%; +} + +.defaultVariant { + background-color: $dt-color-background-color-default; +} + +.educationVariant { + background-color: $dt-color-background-color-education; +} + +.content { + position: relative; + z-index: 2; +} diff --git a/packages/components/src/Skirt/Skirt.tsx b/packages/components/src/Skirt/Skirt.tsx new file mode 100644 index 00000000000..9c89b88dda5 --- /dev/null +++ b/packages/components/src/Skirt/Skirt.tsx @@ -0,0 +1,76 @@ +import React from "react" +import classnames from "classnames" +import { Container } from "~components/Container" +import { Content, ContentProps } from "~components/Content" +import { useResizeObserver } from "~utils/useResizeObserver" +import styles from "./Skirt.module.scss" + +const spacing = 24 +const maxHeight = 222 +const fallbackPercentage = 0.8 + +type Variant = "default" | "education" + +export type SkirtProps = { + children: React.ReactNode + variant?: Variant + titleBlockHasNavigation?: boolean +} & ContentProps + +export const Skirt = ({ + children, + variant = "default", + titleBlockHasNavigation = true, + classNameOverride, + ...restProps +}: SkirtProps): JSX.Element => { + const [ref, skirtHeight] = useResizeObserver( + entry => { + if (entry.contentRect) { + return deriveSkirtHeight(entry.contentRect, titleBlockHasNavigation) + } + return undefined + } + ) + + return ( + +
+ {children} + + ) +} + +Skirt.displayName = "Skirt" + +const deriveSkirtHeight = ( + rect: DOMRectReadOnly, + titleBlockHasNavigation: boolean +): number => { + const { height, width } = rect + let responsiveOffset: number = 0 + if (width > 768) { + responsiveOffset = 2.8125 * 16 + } + + // This ensures the maximum height of the skirt is consistent between pages + // where the title block has/doesn’t have navigation + const derivedMaxHeight = titleBlockHasNavigation + ? maxHeight + : maxHeight + responsiveOffset + const maxHeightWithSpacing = derivedMaxHeight + spacing + + return Math.max( + spacing, + height > maxHeightWithSpacing + ? derivedMaxHeight + : height * fallbackPercentage - spacing + ) +} diff --git a/packages/components/src/Skirt/_docs/Skirt.mdx b/packages/components/src/Skirt/_docs/Skirt.mdx new file mode 100644 index 00000000000..611c0c6f804 --- /dev/null +++ b/packages/components/src/Skirt/_docs/Skirt.mdx @@ -0,0 +1,28 @@ +import { Canvas, Controls, Meta } from "@storybook/blocks" +import { ResourceLinks, KaioNotification, Installation } from "~storybook/components" +import * as SkirtStories from "./Skirt.stories" + + + +# Skirt + + + + + + + +## Overview + +A coloured shade that extends behind content. + + + diff --git a/packages/components/src/Skirt/_docs/Skirt.stories.tsx b/packages/components/src/Skirt/_docs/Skirt.stories.tsx new file mode 100644 index 00000000000..d9ccedc4364 --- /dev/null +++ b/packages/components/src/Skirt/_docs/Skirt.stories.tsx @@ -0,0 +1,50 @@ +import React from "react" +import { Meta, StoryObj } from "@storybook/react" +import { Text } from "~components/Text" +import { Skirt } from "../index" +import { SkirtCard } from "../subcomponents/SkirtCard" + +const meta = { + title: "Components/Skirt", + component: Skirt, + args: { + children: ( + + + Bacon ipsum dolor amet andouille buffalo beef boudin kielbasa + drumstick fatback cow tongue ground round chicken. Jowl cow short + ribs, ham tongue turducken spare ribs pig drumstick chuck meatball. + Buffalo turducken pancetta tail salami chicken. Bresaola venison + pastrami beef. + + + Porchetta shankle ribeye, ground round beef filet mignon fatback + chislic boudin. Boudin burgdoggen spare ribs, meatloaf pastrami pork + loin meatball short ribs tenderloin ribeye cupim venison short loin + pork chop tongue. Andouille landjaeger bacon, picanha filet mignon + short ribs hamburger shank shoulder porchetta. Pork chop ground round + tenderloin, biltong kevin corned beef chuck frankfurter spare ribs + pork meatball pastrami fatback. Strip steak beef ribs pork loin kevin, + biltong fatback tongue salami brisket capicola flank tenderloin. + + + ), + }, + argTypes: { + children: { control: "disabled" }, + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = { + parameters: { + docs: { + canvas: { + sourceState: "shown", + }, + }, + }, +} diff --git a/packages/components/src/Skirt/index.ts b/packages/components/src/Skirt/index.ts new file mode 100644 index 00000000000..25ad577969d --- /dev/null +++ b/packages/components/src/Skirt/index.ts @@ -0,0 +1 @@ +export * from "./Skirt" diff --git a/packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.module.scss b/packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.module.scss new file mode 100644 index 00000000000..a3880dadad5 --- /dev/null +++ b/packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.module.scss @@ -0,0 +1,17 @@ +@import "~@kaizen/design-tokens/sass/layout"; +@import "../../../Content/variables"; + +.wrapper { + @media (max-width: $layout-breakpoints-large) { + margin-left: $content-margin-width-on-medium-and-small * -1; + margin-right: $content-margin-width-on-medium-and-small * -1; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + @media (max-width: $layout-breakpoints-medium) { + border-radius: 0; + margin-left: $content-margin-width-on-medium-and-small * -1; + margin-right: $content-margin-width-on-medium-and-small * -1; + } +} diff --git a/packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.tsx b/packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.tsx new file mode 100644 index 00000000000..057cd040fb3 --- /dev/null +++ b/packages/components/src/Skirt/subcomponents/SkirtCard/SkirtCard.tsx @@ -0,0 +1,18 @@ +import React from "react" +import classnames from "classnames" +import { Card, CardProps } from "~components/Card" +import styles from "./SkirtCard.module.scss" + +export type SkirtCardProps = CardProps + +export const SkirtCard = (props: SkirtCardProps): JSX.Element => { + const { classNameOverride, ...restProps } = props + return ( + + ) +} + +SkirtCard.displayName = "SkirtCard" diff --git a/packages/components/src/Skirt/subcomponents/SkirtCard/index.ts b/packages/components/src/Skirt/subcomponents/SkirtCard/index.ts new file mode 100644 index 00000000000..a0698b975c4 --- /dev/null +++ b/packages/components/src/Skirt/subcomponents/SkirtCard/index.ts @@ -0,0 +1 @@ +export * from "./SkirtCard" diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 53465a4a570..d3272812199 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -10,6 +10,8 @@ export * from "./Card" export * from "./Checkbox" export * from "./ClearButton" export * from "./Collapsible" +export * from "./Container" +export * from "./Content" export * from "./DateInput" export * from "./DatePicker" export * from "./DateRangePicker" @@ -37,6 +39,7 @@ export * from "./Popover" export * from "./Radio" export * from "./SearchField" export * from "./Select" +export * from "./Skirt" export * from "./Slider" export * from "./Text" export * from "./Workflow" diff --git a/packages/components/src/utils/useResizeObserver.spec.tsx b/packages/components/src/utils/useResizeObserver.spec.tsx new file mode 100644 index 00000000000..d80836abd0d --- /dev/null +++ b/packages/components/src/utils/useResizeObserver.spec.tsx @@ -0,0 +1,49 @@ +import { setImmediate } from "timers" +import { renderHook, act } from "@testing-library/react-hooks" +import { useResizeObserver } from "./useResizeObserver" + +function MockResizeObserver(callback: unknown): void { + // @ts-ignore + this.callback = callback +} + +MockResizeObserver.prototype.observe = async function observe(): Promise { + this.callback(["first"]) + await new Promise(r => setImmediate(r)) + this.callback(["second"]) + await new Promise(r => setImmediate(r)) + this.callback(["third"]) +} + +const disconnect = jest.fn() +MockResizeObserver.prototype.disconnect = disconnect + +jest.mock("resize-observer-polyfill", () => ({ + __esModule: true, + default: MockResizeObserver, +})) + +describe("useResizeObserver", () => { + it("Calls the callback with the expected entries", async () => { + const callback = jest.fn().mockImplementation(value => value) + const { result, waitForNextUpdate, unmount } = renderHook(() => + useResizeObserver(callback) + ) + await act(async () => { + // @ts-ignore + result.current[0]("node") + expect(result.current[1]).toBe(undefined) + await waitForNextUpdate() + expect(callback).toBeCalledTimes(1) + expect(result.current[1]).toBe("first") + await waitForNextUpdate() + expect(callback).toBeCalledTimes(2) + expect(result.current[1]).toBe("second") + await waitForNextUpdate() + expect(callback).toBeCalledTimes(3) + expect(result.current[1]).toBe("third") + }) + unmount() + expect(disconnect).toBeCalled() + }) +}) diff --git a/packages/components/src/utils/useResizeObserver.ts b/packages/components/src/utils/useResizeObserver.ts new file mode 100644 index 00000000000..4a383228bd1 --- /dev/null +++ b/packages/components/src/utils/useResizeObserver.ts @@ -0,0 +1,75 @@ +import React, { Ref, useCallback, useEffect, useRef, useState } from "react" +import ResizeObserver from "resize-observer-polyfill" + +const defaultCallback = (entry: ResizeObserverEntry): ResizeObserverEntry => + entry + +export interface DOMRectReadOnly { + readonly x: number + readonly y: number + readonly width: number + readonly height: number + readonly top: number + readonly right: number + readonly bottom: number + readonly left: number +} + +/** + * Hook for observing changes to a DOM element via ResizeObserver. + * + * @param {resolveEntryCallback} resolveEntry - Callback for resolving the + * desired value from each ResizeObserverEntry emitted by ResizeObserver + * @return {Array} An array containing a ref for binding to the observed DOM + * element, and the current value of the callback-resolved ResizeObserverEntry + * @callback resolveEntryCallback + */ +export const useResizeObserver = ( + resolveEntry: (entry: ResizeObserverEntry) => any = defaultCallback +): [Ref, T | undefined] => { + const destroyResizeObserverRef: React.MutableRefObject< + undefined | (() => void) + > = useRef(undefined) + const [dimensions, setDimensions] = useState(undefined) + const resolveEntryRef: React.MutableRefObject< + (entry: ResizeObserverEntry) => any + > = useRef(resolveEntry) + + const ref: Ref = useCallback( + (node: E) => { + if (node) { + const resizeObserver = new ResizeObserver( + (entries: ResizeObserverEntry[]) => { + for (const entry of entries) { + const value = resolveEntryRef.current(entry) + if (value) { + setDimensions(value) + } + } + } + ) + resizeObserver.observe(node) + destroyResizeObserverRef.current = (): void => { + resizeObserver.disconnect() + } + } + }, + [destroyResizeObserverRef, setDimensions, resolveEntryRef] + ) + + // Ensure the resolveEntryRef has the latest value + useEffect(() => { + resolveEntryRef.current = resolveEntry + }, [resolveEntry]) + + // Destroy the observer on unmount + useEffect( + () => () => { + if (destroyResizeObserverRef.current) { + destroyResizeObserverRef.current() + } + }, + [] + ) + return [ref, dimensions] +}