From 29aee67f5ac8969afdab7d86f586d728be2acb74 Mon Sep 17 00:00:00 2001 From: Rogin Farrer Date: Sun, 12 Apr 2020 16:19:12 -0400 Subject: [PATCH] Update readme, API (#46) * Update readme, API * fix details * fix details again * fix details again * fix details again * mostly name fixes * fix ref issue --- README.md | 88 +++++++++++++++++++++++------------- example/index.tsx | 8 ++-- src/__tests__/hooks.test.tsx | 28 ++++++------ src/__tests__/index.test.tsx | 44 +++++++++++------- src/hooks.ts | 34 ++++++++------ src/index.ts | 44 +++++++++--------- src/types.ts | 47 +++++++++++++------ src/utils.ts | 46 +++++++++++++++---- stories/basic.stories.tsx | 14 +++--- stories/div.stories.tsx | 6 +-- stories/nested.stories.tsx | 10 ++-- stories/unmount.stories.tsx | 6 +-- 12 files changed, 231 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 3717adf..87cee04 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![npm version][npm-badge]][npm-version] [![Documentation Netlify Status][netlify-badge]][netlify] -A custom hook for creating flexible and accessible expand/collapse components in React. +A custom hook for creating accessible expand/collapse components in React. Animates the height using CSS transitions from `0` to `auto`. ## v3 @@ -14,9 +14,10 @@ This master branch now reflects the development of the next major release of thi ## Features - Handles the height of animations of your elements, `auto` included! -- You control the UI - `useCollapse` provides the necessary props, you control everything else. -- Built with accessibility in mind - no need to worry if your collapse/expand component is accessible, since this takes care of it for you! +- You control the UI - `useCollapse` provides the necessary props, you control the styles and the elements. +- Accessible out of the box - no need to worry if your collapse/expand component is accessible, since this takes care of it for you! - No animation framework required! Simply powered by CSS animations +- Written in TypeScript ## Demo @@ -39,13 +40,15 @@ import React from 'react'; import useCollapse from 'react-collapsed'; function Demo() { - const { getCollapseProps, getToggleProps, isOpen } = useCollapse(); + const { getCollapseProps, getToggleProps, isExpanded } = useCollapse(); return ( - - +
+
Collapsed content 🙈
- +
); } ``` @@ -57,20 +60,20 @@ import React, { useState } from 'react'; import useCollapse from 'react-collapsed'; function Demo() { - const [isOpen, setOpen] = useState(false); - const { getCollapseProps, getToggleProps } = useCollapse({ isOpen }); + const [isExpanded, setOpen] = useState(false); + const { getCollapseProps, getToggleProps } = useCollapse({ isExpanded }); return ( - +
Collapsed content 🙈
- +
); } ``` @@ -81,15 +84,17 @@ function Demo() { const { getCollapseProps, getToggleProps, - isOpen, + isExpanded, toggleOpen, mountChildren, } = useCollapse({ - isOpen: boolean, - defaultOpen: boolean, + isExpanded: boolean, + defaultExpanded: boolean, expandStyles: {}, collapseStyles: {}, collapsedHeight: 0, + easing: string, + duration: number, }); ``` @@ -97,13 +102,15 @@ const { The following are optional properties passed into `useCollapse({ })`: -| Prop | Type | Default | Description | -| --------------- | ------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| isOpen | boolean | `undefined` | If true, the Collapse is expanded | -| defaultOpen | boolean | `false` | If true, the Collapse will be expanded when mounted | -| expandStyles | object | `{transitionDuration: '500ms', transitionTimingFunction: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)'}` | Style object applied to the collapse panel when it expands | -| collapseStyles | object | `{transitionDuration: '500ms', transitionTimingFunction: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)'}` | Style object applied to the collapse panel when it collapses | -| collapsedHeight | number | `0` | The height of the content when collapsed | +| Prop | Type | Default | Description | +| --------------- | ------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| isExpanded | boolean | `undefined` | If true, the Collapse is expanded | +| defaultExpanded | boolean | `false` | If true, the Collapse will be expanded when mounted | +| expandStyles | object | `{}` | Style object applied to the collapse panel when it expands | +| collapseStyles | object | `{}` | Style object applied to the collapse panel when it collapses | +| collapsedHeight | number | `0` | The height of the content when collapsed | +| easing | string | `cubic-bezier(0.4, 0, 0.2, 1)` | The transition timing function for the animation | +| duration | number | `undefined` | The duration of the animation in milliseconds. By default, the duration is programmatically calculated based on the height of the collapsed element | ### What you get @@ -111,27 +118,44 @@ The following are optional properties passed into `useCollapse({ })`: | ---------------- | ----------------------------------------------------------------------------------------------------------- | | getCollapseProps | Function that returns a prop object, which should be spread onto the collapse element | | getToggleProps | Function that returns a prop object, which should be spread onto an element that toggles the collapse panel | -| isOpen | Whether or not the collapse is open (if not controlled) | -| toggleOpen | Function that will toggle the state of the collapse panel | +| isExpanded | Whether or not the collapse is expanded (if not controlled) | +| toggleExpanded | Function that will toggle the expanded state of the collapse panel | | mountChildren | Whether or not the collapse panel content should be visible | ## Alternative Solutions -- [react-spring](https://www.react-spring.io/) - JavaScript animation based library that can potentially have smoother animations. +- [react-spring](https://www.react-spring.io/) - JavaScript animation based library that can potentially have smoother animations. Requires a bit more work to create an accessible collapse component. +- [react-animate-height](https://github.com/Stanko/react-animate-height/) - Another library that uses CSS transitions to animate to any height. It provides components, not a hook. -## Possible Issues +## FAQ -- Applying padding to the collapse block (the element receiving `getCollapseProps`) can lead to infinite animations and state updates. [14](https://github.com/roginfarrer/react-collapsed/issues/14) +
+When I apply vertical padding to the component that gets getCollapseProps, the animation is janky and it doesn't collapse all the way. What gives? - **Solution:** Apply the padding to a child element instead. +The collapse works by manipulating the `height` property. If an element has vertical padding, that padding expandes the size of the element, even if it has `height: 0; overflow: hidden`. -[minzipped-badge]: https://img.shields.io/bundlephobia/minzip/react-collapsed/latest +To avoid this, simply move that padding from the element to an element directly nested within in. -[npm-badge]: http://img.shields.io/npm/v/react-collapsed.svg?style=flat -[npm-version]: https://npmjs.org/package/react-collapsed "View this project on npm" +```javascript +// from +
+ +// to +
+ Much better! +
+
+``` +
+ +[minzipped-badge]: https://img.shields.io/bundlephobia/minzip/react-collapsed/latest +[npm-badge]: http://img.shields.io/npm/v/react-collapsed.svg?style=flat +[npm-version]: https://npmjs.org/package/react-collapsed 'View this project on npm' [ci-badge]: https://github.com/roginfarrer/react-collapsed/workflows/CI/badge.svg [ci]: https://github.com/roginfarrer/react-collapsed/actions?query=workflow%3ACI+branch%3Amaster - [netlify]: https://app.netlify.com/sites/react-collapsed-next/deploys [netlify-badge]: https://api.netlify.com/api/v1/badges/4d285ffc-aa4f-4d32-8549-eb58e00dd2d1/deploy-status diff --git a/example/index.tsx b/example/index.tsx index df004e8..117fa05 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -6,15 +6,15 @@ import useCollapse from '../src'; const collapseStyles = { background: 'blue', color: 'white' }; export const Uncontrolled = () => { - const { getCollapseProps, getToggleProps, isOpen } = useCollapse({ - defaultOpen: true, - isOpen: false, + const { getCollapseProps, getToggleProps, isExpanded } = useCollapse({ + defaultExpanded: true, + isExpanded: false, }); return (
{ let hookReturn: [boolean, () => void]; function UseControlledState({ - defaultOpen, - isOpen, + defaultExpanded, + isExpanded, }: { - defaultOpen?: boolean; - isOpen?: boolean; + defaultExpanded?: boolean; + isExpanded?: boolean; }) { - const result = useControlledState({ defaultOpen, isOpen }); + const result = useControlledState({ defaultExpanded, isExpanded }); hookReturn = result; @@ -47,13 +47,13 @@ describe('useControlledState', () => { }); it('returns the defaultValue value', () => { - render(); + render(); expect(hookReturn[0]).toBe(true); }); it('setter toggles the value', () => { - render(); + render(); expect(hookReturn[0]).toBe(true); @@ -77,29 +77,29 @@ describe('useControlledState', () => { consoleOutput = []; }); - function Foo({ isOpen }: { isOpen?: boolean }) { - useControlledState({ isOpen }); + function Foo({ isExpanded }: { isExpanded?: boolean }) { + useControlledState({ isExpanded }); return
; } it('warns about changing from uncontrolled to controlled', () => { const { rerender } = render(); - rerender(); + rerender(); expect(consoleOutput[0]).toMatchInlineSnapshot( - `"Warning: useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the \`isOpen\` prop."` + `"Warning: useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the \`isExpanded\` prop."` ); expect(consoleOutput.length).toBe(1); }); it('warns about changing from controlled to uncontrolled', () => { // Initially control the value - const { rerender } = render(); + const { rerender } = render(); // Then re-render without controlling it - rerender(); + rerender(); expect(consoleOutput[0]).toMatchInlineSnapshot( - `"Warning: useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the \`isOpen\` prop."` + `"Warning: useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the \`isExpanded\` prop."` ); expect(consoleOutput.length).toBe(1); }); diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 5bf11c1..a3f8c6b 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -5,17 +5,17 @@ import { mocked } from 'ts-jest/utils'; import useCollapse from '../'; import { getElementHeight } from '../utils'; import { - GetTogglePropsShape, - GetCollapsePropsShape, - CollapseConfig, + GetTogglePropsInput, + GetCollapsePropsInput, + UseCollapseInput, } from '../types'; const mockedGetElementHeight = mocked(getElementHeight, true); const Collapse: React.FC<{ - toggleProps?: GetTogglePropsShape; - collapseProps?: GetCollapsePropsShape; - props?: CollapseConfig; + toggleProps?: GetTogglePropsInput; + collapseProps?: GetCollapsePropsInput; + props?: UseCollapseInput; unmountChildren?: boolean; }> = ({ toggleProps, collapseProps, props, unmountChildren = false }) => { const { getCollapseProps, getToggleProps, mountChildren } = useCollapse( @@ -43,9 +43,9 @@ test('does not throw', () => { test('returns expected constants', () => { const { result } = renderHook(useCollapse); - expect(result.current.isOpen).toStrictEqual(false); + expect(result.current.isExpanded).toStrictEqual(false); expect(result.current.mountChildren).toStrictEqual(false); - expect(typeof result.current.toggleOpen).toBe('function'); + expect(typeof result.current.toggleExpanded).toBe('function'); expect(typeof result.current.getToggleProps()).toBe('object'); expect(typeof result.current.getCollapseProps()).toBe('object'); }); @@ -60,7 +60,9 @@ test('Toggle has expected props when closed (default)', () => { }); test('Toggle has expected props when collapse is open', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const toggle = getByTestId('toggle'); expect(toggle.getAttribute('aria-expanded')).toBe('true'); }); @@ -79,7 +81,9 @@ test('Collapse has expected props when closed (default)', () => { }); test('Collapse has expected props when open', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const collapse = getByTestId('collapse'); expect(collapse).toHaveAttribute('id'); expect(collapse).toHaveAttribute('aria-hidden', 'false'); @@ -105,7 +109,7 @@ test('Re-render does not modify id', () => { const collapse = getByTestId('collapse'); const collapseId = collapse.getAttribute('id'); - rerender(); + rerender(); expect(collapseId).toEqual(collapse.getAttribute('id')); }); @@ -126,7 +130,9 @@ test.skip('clicking the toggle closes the collapse', () => { // Mocked since ref element sizes = :( in jsdom mockedGetElementHeight.mockReturnValue(0); - const { getByTestId } = render(); + const { getByTestId } = render( + + ); const toggle = getByTestId('toggle'); const collapse = getByTestId('collapse'); @@ -136,10 +142,10 @@ test.skip('clicking the toggle closes the collapse', () => { expect(collapse.style.height).toBe('0px'); }); -test('toggle click calls onClick argument with isOpen', () => { +test('toggle click calls onClick argument with isExpanded', () => { const onClick = jest.fn(); const { getByTestId } = render( - + ); const toggle = getByTestId('toggle'); @@ -156,7 +162,7 @@ describe('mountChildren', () => { it('children rendered when mounted open', () => { const { queryByText } = render( - + ); expect(queryByText('content')).toBeInTheDocument(); }); @@ -172,7 +178,7 @@ test('warns if using padding on collapse', () => { render( ); @@ -183,3 +189,9 @@ test('warns if using padding on collapse', () => { console.warn = originalWarn; }); + +test('permits access to the collapse ref', () => { + let cb = jest.fn(); + const { queryByTestId } = render(); + expect(cb).toHaveBeenCalledWith(queryByTestId('collapse')); +}); diff --git a/src/hooks.ts b/src/hooks.ts index 36df789..3420679 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -2,33 +2,37 @@ import { RefObject, useState, useRef, useEffect, useCallback } from 'react'; import warning from 'tiny-warning'; export function useControlledState({ - isOpen, - defaultOpen, + isExpanded, + defaultExpanded, }: { - isOpen?: boolean; - defaultOpen?: boolean; + isExpanded?: boolean; + defaultExpanded?: boolean; }): [boolean, () => void] { - const [stateIsOpen, setStateIsOpen] = useState(defaultOpen || false); - const initiallyControlled = useRef(isOpen != null); - const open = initiallyControlled.current ? isOpen || false : stateIsOpen; - const toggleOpen = useCallback(() => { + const [stateExpanded, setStateExpanded] = useState( + defaultExpanded || false + ); + const initiallyControlled = useRef(isExpanded != null); + const expanded = initiallyControlled.current + ? isExpanded || false + : stateExpanded; + const toggleExpanded = useCallback(() => { if (!initiallyControlled.current) { - setStateIsOpen(oldOpen => !oldOpen); + setStateExpanded(oldExpanded => !oldExpanded); } }, []); useEffect(() => { warning( - !(initiallyControlled.current && isOpen == null), - 'useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isOpen` prop.' + !(initiallyControlled.current && isExpanded == null), + 'useCollapse is changing from controlled to uncontrolled. useCollapse should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isExpanded` prop.' ); warning( - !(!initiallyControlled.current && isOpen != null), - 'useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isOpen` prop.' + !(!initiallyControlled.current && isExpanded != null), + 'useCollapse is changing from uncontrolled to controlled. useCollapse should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled collapse for the lifetime of the component. Check the `isExpanded` prop.' ); - }, [isOpen]); + }, [isExpanded]); - return [open, toggleOpen]; + return [expanded, toggleExpanded]; } export function useEffectAfterMount( diff --git a/src/index.ts b/src/index.ts index 340523c..303e9ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { callAll, getElementHeight, getAutoHeightDuration, + mergeRefs, } from './utils'; import { usePaddingWarning, @@ -12,12 +13,12 @@ import { useControlledState, } from './hooks'; import { - CollapseConfig, - CollapseAPI, - GetCollapsePropsAPI, - GetCollapsePropsShape, - GetTogglePropsAPI, - GetTogglePropsShape, + UseCollapseInput, + UseCollapseOutput, + GetCollapsePropsOutput, + GetCollapsePropsInput, + GetTogglePropsOutput, + GetTogglePropsInput, } from './types'; import raf from 'raf'; @@ -29,8 +30,8 @@ export default function useCollapse({ collapseStyles = {}, expandStyles = {}, ...initialConfig -}: CollapseConfig = {}): CollapseAPI { - const [isOpen, toggleOpen] = useControlledState(initialConfig); +}: UseCollapseInput = {}): UseCollapseOutput { + const [isExpanded, toggleExpanded] = useControlledState(initialConfig); const uniqueId = useUniqueId(); const el = useRef(null); usePaddingWarning(el); @@ -41,9 +42,9 @@ export default function useCollapse({ overflow: 'hidden', }; const [styles, setStyles] = useState( - isOpen ? {} : collapsedStyles + isExpanded ? {} : collapsedStyles ); - const [mountChildren, setMountChildren] = useState(isOpen); + const [mountChildren, setMountChildren] = useState(isExpanded); const mergeStyles = (newStyles: {}): void => { setStyles(oldStyles => ({ ...oldStyles, ...newStyles })); }; @@ -58,7 +59,7 @@ export default function useCollapse({ } useEffectAfterMount(() => { - if (isOpen) { + if (isExpanded) { raf(() => { setMountChildren(true); mergeStyles({ @@ -92,7 +93,7 @@ export default function useCollapse({ }); }); } - }, [isOpen]); + }, [isExpanded]); const handleTransitionEnd = (e: TransitionEvent): void => { // Sometimes onTransitionEnd is triggered by another transition, @@ -108,7 +109,7 @@ export default function useCollapse({ // transitioning the other direction // The conditions give us the opportunity to bail out, // which will prevent the collapsed content from flashing on the screen - if (isOpen) { + if (isExpanded) { const height = getElementHeight(el); // If the height at the end of the transition @@ -133,17 +134,17 @@ export default function useCollapse({ disabled = false, onClick = noop, ...rest - }: GetTogglePropsShape = {}): GetTogglePropsAPI { + }: GetTogglePropsInput = {}): GetTogglePropsOutput { return { type: 'button', role: 'button', id: `react-collapsed-toggle-${uniqueId}`, 'aria-controls': `react-collapsed-panel-${uniqueId}`, - 'aria-expanded': isOpen, + 'aria-expanded': isExpanded, tabIndex: 0, disabled, ...rest, - onClick: disabled ? noop : callAll(onClick, toggleOpen), + onClick: disabled ? noop : callAll(onClick, toggleExpanded), }; } @@ -152,12 +153,13 @@ export default function useCollapse({ onTransitionEnd = noop, refKey = 'ref', ...rest - }: GetCollapsePropsShape = {}): GetCollapsePropsAPI { + }: GetCollapsePropsInput = {}): GetCollapsePropsOutput { + const theirRef: any = rest[refKey]; return { id: `react-collapsed-panel-${uniqueId}`, - 'aria-hidden': !isOpen, + 'aria-hidden': !isExpanded, ...rest, - [refKey]: el, + [refKey]: mergeRefs(el, theirRef), onTransitionEnd: callAll(handleTransitionEnd, onTransitionEnd), style: { boxSizing: 'border-box', @@ -172,8 +174,8 @@ export default function useCollapse({ return { getToggleProps, getCollapseProps, - isOpen, - toggleOpen, + isExpanded, + toggleExpanded, mountChildren, }; } diff --git a/src/types.ts b/src/types.ts index 64535d9..2140bac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,10 @@ -import { CSSProperties, TransitionEvent, MouseEvent } from 'react'; +import { + ReactNode, + CSSProperties, + TransitionEvent, + MouseEvent, + MutableRefObject, +} from 'react'; type Dispatch = (value: A) => void; type SetStateAction = S | ((prevState: S) => S); @@ -7,17 +13,29 @@ export type StateSetter = Dispatch>; type ButtonType = 'submit' | 'reset' | 'button'; type AriaBoolean = boolean | 'true' | 'false'; -export interface CollapseConfig { - isOpen?: boolean; - defaultOpen?: boolean; +/** + * React.Ref uses the readonly type `React.RefObject` instead of + * `React.MutableRefObject`, We pretty much always assume ref objects are + * mutable (at least when we create them), so this type is a workaround so some + * of the weird mechanics of using refs with TS. + */ +export type AssignableRef = + | { + bivarianceHack(instance: ValueType | null): void; + }['bivarianceHack'] + | MutableRefObject; + +export interface UseCollapseInput { + isExpanded?: boolean; + defaultExpanded?: boolean; collapsedHeight?: number; expandStyles?: {}; collapseStyles?: {}; - easing?: string | { expand?: string; collapse?: string }; + easing?: string; duration?: number; } -export interface GetTogglePropsAPI { +export interface GetTogglePropsOutput { disabled: boolean; type: ButtonType; role: string; @@ -28,30 +46,31 @@ export interface GetTogglePropsAPI { onClick: (e: MouseEvent) => void; } -export interface GetTogglePropsShape { +export interface GetTogglePropsInput { [key: string]: unknown; disabled?: boolean; onClick?: (e: MouseEvent) => void; } -export interface GetCollapsePropsAPI { +export interface GetCollapsePropsOutput { id: string; onTransitionEnd: (e: TransitionEvent) => void; style: CSSProperties; 'aria-hidden': AriaBoolean; } -export interface GetCollapsePropsShape { +export interface GetCollapsePropsInput { [key: string]: unknown; style?: CSSProperties; onTransitionEnd?: (e: TransitionEvent) => void; refKey?: string; + ref?: (node: ReactNode) => void | null | undefined; } -export interface CollapseAPI { - getCollapseProps: (config?: GetCollapsePropsShape) => GetCollapsePropsAPI; - getToggleProps: (config?: GetTogglePropsShape) => GetTogglePropsAPI; - isOpen: boolean; +export interface UseCollapseOutput { + getCollapseProps: (config?: GetCollapsePropsInput) => GetCollapsePropsOutput; + getToggleProps: (config?: GetTogglePropsInput) => GetTogglePropsOutput; + isExpanded: boolean; mountChildren: boolean; - toggleOpen: () => void; + toggleExpanded: () => void; } diff --git a/src/utils.ts b/src/utils.ts index 2020fd0..3437f01 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { RefObject } from 'react'; import warning from 'tiny-warning'; +import { AssignableRef } from './types'; type AnyFunction = (...args: any[]) => unknown; @@ -25,15 +26,6 @@ export function getElementHeight( export const callAll = (...fns: AnyFunction[]) => (...args: any[]): void => fns.forEach(fn => fn && fn(...args)); -export function joinTransitionProperties(string?: string): string { - if (string) { - const styles = ['height']; - styles.push(...string.split(', ')); - return styles.join(', '); - } - return 'height'; -} - // https://github.com/mui-org/material-ui/blob/da362266f7c137bf671d7e8c44c84ad5cfc0e9e2/packages/material-ui/src/styles/transitions.js#L89-L98 export function getAutoHeightDuration(height: number | string): number { if (!height || typeof height === 'string') { @@ -45,3 +37,39 @@ export function getAutoHeightDuration(height: number | string): number { // https://www.wolframalpha.com/input/?i=(4+%2B+15+*+(x+%2F+36+)+**+0.25+%2B+(x+%2F+36)+%2F+5)+*+10 return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10); } + +export function assignRef( + ref: AssignableRef | null | undefined, + value: any +) { + if (ref == null) return; + if (typeof ref === 'function') { + ref(value); + } else { + try { + ref.current = value; + } catch (error) { + throw new Error(`Cannot assign value "${value}" to ref "${ref}"`); + } + } +} + +/** + * Passes or assigns a value to multiple refs (typically a DOM node). Useful for + * dealing with components that need an explicit ref for DOM calculations but + * also forwards refs assigned by an app. + * + * @param refs Refs to fork + */ +export function mergeRefs( + ...refs: (AssignableRef | null | undefined)[] +) { + if (refs.every(ref => ref == null)) { + return null; + } + return (node: any) => { + refs.forEach(ref => { + assignRef(ref, node); + }); + }; +} diff --git a/stories/basic.stories.tsx b/stories/basic.stories.tsx index 8be1ac6..4607370 100644 --- a/stories/basic.stories.tsx +++ b/stories/basic.stories.tsx @@ -4,30 +4,28 @@ import { Toggle, Collapse, excerpt } from './components'; import { withA11y } from '@storybook/addon-a11y'; export const Uncontrolled = () => { - const { getCollapseProps, getToggleProps, isOpen } = useCollapse({ - defaultOpen: true, - duration: 1000, + const { getCollapseProps, getToggleProps, isExpanded } = useCollapse({ + defaultExpanded: true, }); return (
- {isOpen ? 'Close' : 'Open'} + {isExpanded ? 'Close' : 'Open'} {excerpt} -

adding something here

); }; export const Controlled = () => { - const [isOpen, setOpen] = React.useState(true); + const [isExpanded, setOpen] = React.useState(true); const { getCollapseProps, getToggleProps } = useCollapse({ - isOpen, + isExpanded, }); return (
setOpen(old => !old) })}> - {isOpen ? 'Close' : 'Open'} + {isExpanded ? 'Close' : 'Open'} {excerpt}
diff --git a/stories/div.stories.tsx b/stories/div.stories.tsx index a22b5ae..ac27e51 100644 --- a/stories/div.stories.tsx +++ b/stories/div.stories.tsx @@ -9,14 +9,14 @@ export default { }; export const Div = () => { - const { getCollapseProps, getToggleProps, isOpen } = useCollapse({ - defaultOpen: true, + const { getCollapseProps, getToggleProps, isExpanded } = useCollapse({ + defaultExpanded: true, }); return (
- {isOpen ? 'Close' : 'Open'} + {isExpanded ? 'Close' : 'Open'} {excerpt}
diff --git a/stories/nested.stories.tsx b/stories/nested.stories.tsx index 0106617..5272fa4 100644 --- a/stories/nested.stories.tsx +++ b/stories/nested.stories.tsx @@ -9,7 +9,7 @@ export default { }; function InnerCollapse() { - const { getCollapseProps, getToggleProps, isOpen } = useCollapse(); + const { getCollapseProps, getToggleProps, isExpanded } = useCollapse(); return ( <> @@ -58,20 +58,20 @@ function InnerCollapse() { - {isOpen ? 'Click to collapse' : 'Read more?'} + {isExpanded ? 'Click to collapse' : 'Read more?'} ); } export function Nested() { - const { getCollapseProps, getToggleProps, isOpen } = useCollapse({ - defaultOpen: true, + const { getCollapseProps, getToggleProps, isExpanded } = useCollapse({ + defaultExpanded: true, }); return ( - {isOpen ? 'Close' : 'Expand'} + {isExpanded ? 'Close' : 'Expand'}
diff --git a/stories/unmount.stories.tsx b/stories/unmount.stories.tsx index f05c901..b915b34 100644 --- a/stories/unmount.stories.tsx +++ b/stories/unmount.stories.tsx @@ -12,15 +12,15 @@ export function Unmount() { const { getCollapseProps, getToggleProps, - isOpen, + isExpanded, mountChildren, } = useCollapse({ - defaultOpen: true, + defaultExpanded: true, }); return ( - {isOpen ? 'Close' : 'Open'} + {isExpanded ? 'Close' : 'Open'}
{mountChildren && {excerpt}}