Skip to content

Commit

Permalink
refactor(accordion): ♻️ separate single & multi accordion (#203)
Browse files Browse the repository at this point in the history
Co-authored-by: anuraghazra <[email protected]>
  • Loading branch information
navin-moorthy and anuraghazra authored Apr 16, 2021
1 parent 4b96280 commit 747d50c
Show file tree
Hide file tree
Showing 23 changed files with 1,219 additions and 928 deletions.
50 changes: 0 additions & 50 deletions @types-tests/types.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,50 +0,0 @@
/* eslint-disable react-hooks/rules-of-hooks */
import React from "react";
import { expectError, expectType } from "tsd";
import {
MultiOverloadReturn,
SingleOverloadReturn,
} from "../src/accordion/types";
import { useAccordionState } from "../src/accordion/AccordionState";

const multi = useAccordionState({ allowMultiple: true });
expectType<(string | null)[] | undefined>(multi.selectedIds);
expectType<React.Dispatch<React.SetStateAction<(string | null)[]>>>(
multi.setSelectedIds,
);
expectType<MultiOverloadReturn>(multi);

const single = useAccordionState({ allowMultiple: false });
expectType<string | null | undefined>(single.selectedId);
expectType<React.Dispatch<React.SetStateAction<string | null>>>(
single.setSelectedId,
);
expectType<SingleOverloadReturn>(single);

expectType<MultiOverloadReturn>(
useAccordionState({
allowMultiple: true,
defaultSelectedIds: [],
}),
);

expectError(
useAccordionState({
allowMultiple: true,
defaultSelectedId: "",
}),
);

expectError(
useAccordionState({
allowMultiple: false,
defaultSelectedIds: [],
}),
);

expectType<SingleOverloadReturn>(
useAccordionState({
allowMultiple: false,
defaultSelectedId: "",
}),
);
34 changes: 17 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,32 +101,32 @@
"@react-aria/interactions": "^3.3.4",
"@react-aria/spinbutton": "^3.0.0-rc.0",
"@react-aria/utils": "3.7.0",
"date-fns": "2.20.0",
"date-fns": "2.20.2",
"reakit-system": "0.15.1",
"reakit-utils": "0.15.1",
"reakit-warning": "^0.6.1"
},
"devDependencies": {
"@babel/cli": "7.13.14",
"@babel/core": "7.13.14",
"@babel/core": "7.13.15",
"@babel/plugin-proposal-class-properties": "7.13.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.13.8",
"@babel/preset-env": "7.13.12",
"@babel/preset-env": "7.13.15",
"@babel/preset-react": "7.13.13",
"@babel/preset-typescript": "7.13.0",
"@commitlint/cli": "12.1.1",
"@commitlint/config-conventional": "12.1.1",
"@emotion/css": "11.1.3",
"@storybook/addon-a11y": "6.2.5",
"@storybook/addon-actions": "6.2.5",
"@storybook/addon-essentials": "6.2.5",
"@storybook/addon-a11y": "6.2.7",
"@storybook/addon-actions": "6.2.7",
"@storybook/addon-essentials": "6.2.7",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/react": "6.2.5",
"@storybook/react": "6.2.7",
"@testing-library/dom": "^7.28.1",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6",
"@testing-library/react-hooks": "^5.1.1",
"@testing-library/user-event": "^13.1.1",
"@testing-library/user-event": "^13.1.2",
"@textlint/markdown-to-ast": "^6.3.4",
"@types/jest": "26.0.22",
"@types/jest-axe": "3.5.1",
Expand All @@ -136,8 +136,8 @@
"@types/react-dom": "17.0.3",
"@types/react-transition-group": "4.4.1",
"@types/testing-library__jest-dom": "5.9.5",
"@typescript-eslint/eslint-plugin": "4.21.0",
"@typescript-eslint/parser": "4.21.0",
"@typescript-eslint/eslint-plugin": "4.22.0",
"@typescript-eslint/parser": "4.22.0",
"all-contributors-cli": "^6.20.0",
"ast-to-markdown": "^1.0.0",
"autoprefixer": "^10.2.5",
Expand All @@ -148,17 +148,17 @@
"babel-plugin-date-fns": "^2.0.0",
"chalk": "4.1.0",
"codesandbox": "^2.2.1",
"concurrently": "6.0.1",
"concurrently": "6.0.2",
"conventional-github-releaser": "3.1.5",
"cross-env": "7.0.3",
"eslint": "7.23.0",
"eslint": "7.24.0",
"eslint-config-prettier": "8.1.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-flowtype": "5.6.0",
"eslint-plugin-flowtype": "5.7.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "3.3.1",
"eslint-plugin-react": "7.23.1",
"eslint-plugin-react": "7.23.2",
"eslint-plugin-react-hooks": "4.2.0",
"gacp": "2.10.2",
"glob": "^7.1.6",
Expand All @@ -176,15 +176,15 @@
"mockdate": "^3.0.5",
"outdent": "^0.8.0",
"patch-package": "^6.4.7",
"postcss": "^8.2.9",
"postcss": "^8.2.10",
"postcss-import": "^14.0.1",
"postcss-scopify": "^0.1.9",
"prettier": "2.2.1",
"raw-loader": "^4.0.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-hook-form": "7.0.4",
"react-spring": "9.0.0",
"react-hook-form": "7.0.7",
"react-spring": "9.1.1",
"react-test-renderer": "17.0.2",
"react-transition-group": "4.4.1",
"react-virtual": "^2.6.1",
Expand Down
48 changes: 48 additions & 0 deletions src/accordion/AccordionBaseState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
CompositeState,
CompositeActions,
useCompositeState,
CompositeInitialState,
} from "reakit";

export function useAccordionBaseState(
props: AccordionBaseInitialState = {},
): AccordionBaseStateReturn {
const composite = useCompositeState({
orientation: "vertical",
...props,
});

const panels = useCompositeState();

return {
panels: panels.items,
registerPanel: panels.registerItem,
unregisterPanel: panels.unregisterItem,
...composite,
};
}

export type AccordionBaseState = CompositeState & {
/**
* Lists all the panels.
*/
panels: CompositeState["items"];
};

export type AccordionBaseActions = CompositeActions & {
/**
* Registers a accordion panel.
*/
registerPanel: CompositeActions["registerItem"];

/**
* Unregisters a accordion panel.
*/
unregisterPanel: CompositeActions["unregisterItem"];
};

export type AccordionBaseInitialState = CompositeInitialState;

export type AccordionBaseStateReturn = AccordionBaseState &
AccordionBaseActions;
112 changes: 112 additions & 0 deletions src/accordion/AccordionMultiState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import * as React from "react";
import { Dispatch, SetStateAction } from "react";
import { useControllableState } from "@chakra-ui/hooks";

import {
AccordionBaseState,
AccordionBaseActions,
useAccordionBaseState,
AccordionBaseInitialState,
} from "./AccordionBaseState";

export function useAccordionMultiState(
props: AccordionMultiInitialState = {},
): AccordionMultiStateReturn {
const { manual = true } = props;

const { move, ...baseState } = useAccordionBaseState(props);

const [selectedIds, setSelectedIds] = useControllableState({
defaultValue: props?.defaultSelectedIds || [],
value: props?.selectedIds,
onChange: props?.onSelectedIdsChange,
});

const select = React.useCallback(
(id: string) => {
move(id);

if (selectedIds.includes(id)) {
setSelectedIds(prevIds => prevIds?.filter(pId => pId !== id));

return;
}

setSelectedIds(prevIds => [...prevIds, id]);
},

[move, selectedIds, setSelectedIds],
);

return {
selectedIds,
setSelectedIds,
select,
manual,
allowToggle: true,
allowMultiple: true,
move,
...baseState,
};
}

export type AccordionMultiState = AccordionBaseState & {
/**
* The current selected accordion's `id`.
*/
selectedIds: string[];

/**
* Allow to toggle accordion items
* @default false
*/
allowToggle: boolean;

/**
* Allow to open multiple accordion items
*/
allowMultiple: boolean;

/**
* Whether the accodion selection should be manual.
* @default true
*/
manual: boolean;
};

export type AccordionMultiActions = AccordionBaseActions & {
/**
* Sets the value.
*/
setSelectedIds: Dispatch<SetStateAction<string[]>>;

/**
* Moves into and selects an accordion by its `id`.
*/
select: (id: string) => void;
};

export type AccordionMultiInitialState = Pick<
Partial<AccordionMultiState>,
"manual" | "selectedIds"
> &
AccordionBaseInitialState & {
/**
* The initial value to be used, in uncontrolled mode
* @default []
*/
defaultSelectedIds?: string[] | (() => string[]);

/**
* The callback fired when the value changes
*/
onSelectedIdsChange?: (value: string[]) => void;

/**
* The function that determines if the state should be updated
*/
shouldUpdate?: (prev: string[], next: string[]) => boolean;
};

export type AccordionMultiStateReturn = AccordionMultiState &
AccordionMultiActions;
19 changes: 11 additions & 8 deletions src/accordion/AccordionPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { createHook, createComponent } from "reakit-system";
import { ACCORDION_PANEL_KEYS } from "./__keys";
import { AccordionStateReturn } from "./AccordionState";
import { getAccordionId, isPanelVisible } from "./helpers";
import { AccordionMultiStateReturn } from "./AccordionMultiState";

export const useAccordionPanel = createHook<
AccordionPanelOptions,
Expand All @@ -24,11 +25,12 @@ export const useAccordionPanel = createHook<

useProps(options, { ref: htmlRef, ...htmlProps }) {
const ref = React.useRef<HTMLElement>(null);
const accordionId = getAccordionId(options);
const { id, registerPanel, unregisterPanel } = options;
const accordionId = getAccordionId(options);

React.useLayoutEffect(() => {
if (!id) return undefined;
if (!id) return;

registerPanel?.({ id, ref, groupId: accordionId });

return () => {
Expand All @@ -38,6 +40,7 @@ export const useAccordionPanel = createHook<

return {
ref: useForkRef(ref, htmlRef),
role: "region",
"aria-labelledby": accordionId,
...htmlProps,
};
Expand Down Expand Up @@ -66,14 +69,14 @@ export type AccordionPanelOptions = {
unstable_IdOptions &
Pick<
AccordionStateReturn,
| "registerPanel"
| "unregisterPanel"
| "panels"
| "items"
| "allowMultiple"
| "panels"
| "selectedId"
| "selectedIds"
>;
| "allowMultiple"
| "registerPanel"
| "unregisterPanel"
> &
Pick<AccordionMultiStateReturn, "selectedIds">;

export type AccordionPanelHTMLProps = DisclosureContentHTMLProps &
unstable_IdHTMLProps;
Expand Down
Loading

1 comment on commit 747d50c

@vercel
Copy link

@vercel vercel bot commented on 747d50c Apr 16, 2021

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.