Skip to content

Commit

Permalink
feat(divider): Update useVerticalDividerHeight to support any HTMLEle…
Browse files Browse the repository at this point in the history
…ment
  • Loading branch information
mlaursen committed Jan 18, 2022
1 parent 7ccd0a6 commit edd9287
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 149 deletions.
70 changes: 11 additions & 59 deletions packages/divider/src/VerticalDivider.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,13 @@
import { forwardRef, HTMLAttributes, Ref, useCallback, useState } from "react";
import { applyRef } from "@react-md/utils";
import { forwardRef, HTMLAttributes } from "react";

import { Divider } from "./Divider";
import { useVerticalDividerHeight } from "./useVerticalDividerHeight";

export interface VerticalDividerProps extends HTMLAttributes<HTMLDivElement> {
/**
* The max height for the vertical divider. This number **must** be greater
* than 0 to work correctly.
*
* When the value is between 0 and 1, it will be used as a multiplier with the
* parent element's height. When the value is greater than 1, it will be used
* in `Math.min(parentElementHeight, maxHeight)`.
*/
/** {@inheritDoc VerticalDividerHookOptions.maxHeight} */
maxHeight?: number;
}

interface VerticalDividerHeight {
ref: (instance: HTMLDivElement | null) => void;
height: number | undefined;
}

/**
* This is a small hook that is used to automatically create a vertical divider
* based on the computed height of its parent element.
*
* @param maxHeight - The max height for the vertical divider. When the value is
* between 0 and 1, it will be used as a percentage. Otherwise the smaller value
* of parent element height and this will be used.
*/
export function useVerticalDividerHeight(
maxHeight: number,
forwardedRef?: Ref<HTMLDivElement | null> | undefined
): VerticalDividerHeight {
if (process.env.NODE_ENV !== "production" && maxHeight < 0) {
throw new Error(
"The `maxHeight` for a vertical divider height must be greater than 0"
);
}

const [height, setHeight] = useState<number | undefined>(undefined);
const ref = useCallback(
(instance: HTMLDivElement | null) => {
applyRef(instance, forwardedRef);
if (!instance || !instance.parentElement) {
return;
}

const height = instance.parentElement.offsetHeight;
if (maxHeight <= 1) {
setHeight(height * maxHeight);
} else {
setHeight(Math.min(height, maxHeight));
}
},
[maxHeight, forwardedRef]
);

return { ref, height };
}

/**
* This component is used to create a vertical divider based on a parent
* element's height. This is really only needed when the parent element **has no
Expand All @@ -68,10 +17,13 @@ export function useVerticalDividerHeight(
* the time) when it is not set on a parent element.
*/
export const VerticalDivider = forwardRef<HTMLDivElement, VerticalDividerProps>(
function VerticalDivider({ style, maxHeight = 1, ...props }, forwardedRef) {
const { ref, height } = useVerticalDividerHeight(maxHeight, forwardedRef);
return (
<Divider {...props} style={{ ...style, height }} ref={ref} vertical />
);
function VerticalDivider({ style, maxHeight = 1, ...props }, ref) {
const heightProps = useVerticalDividerHeight({
ref,
style,
maxHeight,
});

return <Divider {...props} {...heightProps} vertical />;
}
);
91 changes: 2 additions & 89 deletions packages/divider/src/__tests__/VerticalDivider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { render } from "@testing-library/react";
import { act, renderHook } from "@testing-library/react-hooks";

import { useVerticalDividerHeight, VerticalDivider } from "../VerticalDivider";
import { VerticalDivider } from "../VerticalDivider";

describe("VerticalDivider", () => {
it("should render as a div with the vertical divider class names", () => {
Expand All @@ -10,92 +9,6 @@ describe("VerticalDivider", () => {

expect(divider.tagName).toBe("DIV");
expect(divider.className).toBe("rmd-divider rmd-divider--vertical");
});

describe("useVerticalDividerHeight", () => {
it("should throw an error if the maxHeight is less than 0", () => {
// can't use renderHook for this since the error will be caught in the ErrorBoundary
const Test = () => {
useVerticalDividerHeight(-1);
return null;
};

const consoleError = jest.spyOn(console, "error");
// hide React uncaught error message
consoleError.mockImplementation();

expect(() => render(<Test />)).toThrowError(
"The `maxHeight` for a vertical divider height must be greater than 0"
);
});

it("should provide a ref callback and a height", () => {
let { result } = renderHook(() => useVerticalDividerHeight(5));
expect(result.current).toEqual({
ref: expect.any(Function),
height: undefined,
});

({ result } = renderHook(() => useVerticalDividerHeight(0)));
expect(result.current).toEqual({
ref: expect.any(Function),
height: undefined,
});

({ result } = renderHook(() => useVerticalDividerHeight(1)));
expect(result.current).toEqual({
ref: expect.any(Function),
height: undefined,
});
});

it("should update the height value after the ref is called with an element", () => {
const div = document.createElement("div");
const parentDiv = document.createElement("div");
parentDiv.appendChild(div);
Object.defineProperty(parentDiv, "offsetHeight", { value: 100 });

const { result } = renderHook(() => useVerticalDividerHeight(1));
expect(result.current.height).toBeUndefined();

act(() => result.current.ref(div));
expect(result.current.height).toBe(100);
});

it("should use the maxHeight as a multiplier if it is less than 1", () => {
const div = document.createElement("div");
const parentDiv = document.createElement("div");
parentDiv.appendChild(div);
Object.defineProperty(parentDiv, "offsetHeight", { value: 100 });

const { result } = renderHook(() => useVerticalDividerHeight(0.6));
expect(result.current.height).toBeUndefined();

act(() => result.current.ref(div));
expect(result.current.height).toBe(60);
});

it("should use the maxHeight as a pixel value if it is greater than 1", () => {
const div = document.createElement("div");
const parentDiv = document.createElement("div");
parentDiv.appendChild(div);
Object.defineProperty(parentDiv, "offsetHeight", {
value: 100,
writable: true,
});

const { result } = renderHook(() => useVerticalDividerHeight(80));
expect(result.current.height).toBeUndefined();

act(() => result.current.ref(div));
expect(result.current.height).toBe(80);

Object.defineProperty(parentDiv, "offsetHeight", {
value: 40,
writable: true,
});
act(() => result.current.ref(div));
expect(result.current.height).toBe(40);
});
expect(container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VerticalDivider should render as a div with the vertical divider class names 1`] = `
<div>
<div
class="rmd-divider rmd-divider--vertical"
role="separator"
style="height: 0px;"
/>
</div>
`;
60 changes: 60 additions & 0 deletions packages/divider/src/__tests__/useVerticalDivider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { act, renderHook } from "@testing-library/react-hooks";

import { useVerticalDividerHeight } from "../useVerticalDividerHeight";

describe("useVerticalDivider", () => {
it("should update the height value after the ref is called with an element", () => {
const div = document.createElement("div");
const parentDiv = document.createElement("div");
parentDiv.appendChild(div);
Object.defineProperty(parentDiv, "offsetHeight", { value: 100 });

const { result } = renderHook(() =>
useVerticalDividerHeight({ maxHeight: 1 })
);
expect(result.current.style?.height).toBeUndefined();

act(() => result.current.ref(div));
expect(result.current.style?.height).toBe(100);
});

it("should use the maxHeight as a multiplier if it is less than 1", () => {
const div = document.createElement("div");
const parentDiv = document.createElement("div");
parentDiv.appendChild(div);
Object.defineProperty(parentDiv, "offsetHeight", { value: 100 });

const { result } = renderHook(() =>
useVerticalDividerHeight({ maxHeight: 0.6 })
);
expect(result.current.style?.height).toBeUndefined();

act(() => result.current.ref(div));
expect(result.current.style?.height).toBe(60);
});

it("should use the maxHeight as a pixel value if it is greater than 1", () => {
const div = document.createElement("div");
const parentDiv = document.createElement("div");
parentDiv.appendChild(div);
Object.defineProperty(parentDiv, "offsetHeight", {
value: 100,
writable: true,
});

const { result } = renderHook(() =>
useVerticalDividerHeight({ maxHeight: 80 })
);
expect(result.current.style?.height).toBeUndefined();

act(() => result.current.ref(div));
expect(result.current.style?.height).toBe(80);

Object.defineProperty(parentDiv, "offsetHeight", {
value: 40,
writable: true,
});
act(() => result.current.ref(div));
expect(result.current.style?.height).toBe(40);
});
});
2 changes: 1 addition & 1 deletion packages/divider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
* @module @react-md/divider
*/
export * from "./Divider";

export * from "./useVerticalDividerHeight";
export * from "./VerticalDivider";
70 changes: 70 additions & 0 deletions packages/divider/src/useVerticalDividerHeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { CSSProperties, Ref, RefCallback, useCallback, useState } from "react";
import { applyRef } from "@react-md/utils";

/** @remarks \@since 5.0.0 */
export interface VerticalDividerHookOptions<E extends HTMLElement> {
/**
* An optional ref to merge with the returned ref.
*/
ref?: Ref<E>;

/**
* An optional style object to merge with the divider's height style.
*/
style?: CSSProperties;

/**
* The max height for the vertical divider. When this is `<= 0`, the hook will
* be disabled.
*
* When the value is between 0 and 1, it will be used as a multiplier with the
* parent element's height. When the value is greater than 1, it will be used
* in `Math.min(parentElementHeight, maxHeight)`.
*/
maxHeight: number;
}

/** @remarks \@since 5.0.0 */
export interface VerticalDividerHeight<E extends HTMLElement> {
ref: RefCallback<E>;
style: CSSProperties | undefined;
}

/**
* This is a small hook that is used to automatically create a vertical divider
* based on the computed height of its parent element.
*
* @param maxHeight - The max height for the vertical divider. When the value is
* between 0 and 1, it will be used as a percentage. Otherwise the smaller value
* of parent element height and this will be used.
* @remarks \@since 5.0.0 The hook accepts an object instead of using multiple
* params and uses a generic for the HTMLElement type.
*/
export function useVerticalDividerHeight<E extends HTMLElement>({
ref,
style,
maxHeight,
}: VerticalDividerHookOptions<E>): VerticalDividerHeight<E> {
const [height, setHeight] = useState<number | undefined>(undefined);
const refCallback = useCallback(
(instance: E | null) => {
applyRef(instance, ref);
if (!instance || !instance.parentElement || maxHeight === 0) {
return;
}

const height = instance.parentElement.offsetHeight;
if (maxHeight <= 1) {
setHeight(height * maxHeight);
} else {
setHeight(Math.min(height, maxHeight));
}
},
[maxHeight, ref]
);

return {
ref: refCallback,
style: maxHeight <= 0 ? style : { ...style, height },
};
}

1 comment on commit edd9287

@vercel
Copy link

@vercel vercel bot commented on edd9287 Jan 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.