Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
save
  • Loading branch information
roginfarrer committed Jun 7, 2024
commit 69d3022ffe8643a731b88fa5b00bb8d51da14b7b
2 changes: 1 addition & 1 deletion internal/tsconfig/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["dom", "ESNext"],
"lib": ["dom", "ESNext", "DOM.Iterable"],
"importHelpers": true,
"sourceMap": true,
"strict": true,
Expand Down
4 changes: 4 additions & 0 deletions packages/react/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ module.exports = {
},

docs: {},

typescript: {
reactDocgen: "react-docgen-typescript",
},
};

function getAbsolutePath(value) {
Expand Down
3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@
"tslib": "^2.4.1",
"tsup": "^8",
"typescript": "^5.4",
"vite": "^4"
"vite": "^4.5.3"
},
"dependencies": {
"@collapsed/core": "workspace:*",
"web-component": "workspace:*",
"tiny-warning": "^1.0.3"
},
"repository": {
Expand Down
60 changes: 5 additions & 55 deletions packages/react/src/stories/basic.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,32 @@ import { useCollapse } from "..";
import { Toggle, Collapse, excerpt } from "./components";

export const Uncontrolled = () => {
const toggleRef = React.useRef();
const collapseRef = React.useRef();
const { isExpanded, setExpanded } = useCollapse({
getCollapseElement: () => collapseRef.current,
getToggleElement: () => toggleRef.current,
});
const { getToggleProps, getCollapseProps, isExpanded } = useCollapse();

return (
<div>
<button onClick={() => setExpanded((x) => !x)}>
{isExpanded ? "Close" : "Open"}
</button>
<Toggle ref={toggleRef}>{isExpanded ? "Close" : "Open"}</Toggle>
<Collapse ref={collapseRef}>{excerpt}</Collapse>
<Toggle {...getToggleProps()}>{isExpanded ? "Close" : "Open"}</Toggle>
<Collapse {...getCollapseProps()}>{excerpt}</Collapse>
</div>
);
};

export const Controlled = () => {
const [isExpanded, setOpen] = React.useState<boolean>(true);
const collapseRef = React.useRef();
const { setExpanded } = useCollapse({
getCollapseElement: () => collapseRef.current,
const { getCollapseProps } = useCollapse({
isExpanded,
onExpandedChange: setOpen,
});

return (
<div>
<Toggle onClick={() => setExpanded((x) => !x)}>
{isExpanded ? "Close" : "Open"}
</Toggle>
<Toggle onClick={() => setOpen((x) => !x)}>
{isExpanded ? "Close" : "Open"}
</Toggle>
<Collapse ref={collapseRef}>{excerpt}</Collapse>
<Collapse {...getCollapseProps({})}>{excerpt}</Collapse>
</div>
);
};

function useReduceMotion() {
const [matches, setMatch] = React.useState(
window.matchMedia("(prefers-reduced-motion: reduce)").matches,
);
React.useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
const handleChange = () => {
setMatch(mq.matches);
};
handleChange();
mq.addEventListener("change", handleChange);
return () => {
mq.removeEventListener("change", handleChange);
};
}, []);
return matches;
}

// export const PrefersReducedMotion = () => {
// const reduceMotion = useReduceMotion();
// const [isExpanded, setOpen] = React.useState<boolean>(true);
// const { getToggleProps, getCollapseProps } = useCollapse({
// isExpanded,
// hasDisabledAnimation: reduceMotion,
// });

// return (
// <div>
// <Toggle {...getToggleProps({ onClick: () => setOpen((old) => !old) })}>
// {isExpanded ? "Close" : "Open"}
// </Toggle>
// <Collapse {...getCollapseProps()}>{excerpt}</Collapse>
// </div>
// );
// };

export default {
title: "Basic Usage",
};
217 changes: 182 additions & 35 deletions packages/react/src/useCollapse.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,197 @@
import * as React from "react";
import { Collapse, CollapseParams } from "@collapsed/core";
import {
callAll,
mergeRefs,
useControlledState,
useLayoutEffect,
} from "./utils";
CollapseAnimation,
CollapseOptions,
} from "../../web-component/src/collapsed";
//import { Collapse, CollapseParams } from "@collapsed/core";
import { AssignableRef, mergeRefs, useControlledState } from "./utils";

export interface UseCollapseParams extends CollapseParams {
export interface UseCollapseParams
extends Omit<CollapseOptions, "getDisclosureElement"> {
isExpanded?: boolean;
onExpandedChange: (state: boolean) => void;
onExpandedChange?: (state: boolean) => void;
initialExpanded?: boolean;
}

export function useCollapse(options: UseCollapseParams) {
const {
isExpanded: propExpanded,
initialExpanded: propDefaultExpanded,
export function useCollapse({
isExpanded: propExpanded,
initialExpanded: propDefaultExpanded = false,
onExpandedChange,
easing = "cubic-bezier(0.4, 0, 0.2, 1)",
duration = "auto",
collapsedHeight = 0,
onStateChange = () => {},
onTransitionStateChange = () => {},
disableAnimation = false,
}: UseCollapseParams = {}) {
const id = React.useId();
const [isExpanded, setExpanded] = useControlledState(
propExpanded,
propDefaultExpanded,
onExpandedChange,
...opts
} = options;
);
let prevState = React.useRef(isExpanded);
const [hasMounted, setHasMounted] = React.useState(false);
const [toggleEl, setToggleEl] = React.useState<HTMLElement | null>(null);
const collapseElRef = React.useRef<HTMLElement | null>(null);
// const instance = React.useRef<CollapseAnimation>();
const [instance] = React.useState(
() =>
new CollapseAnimation({
easing,
duration,
collapsedHeight,
getDisclosureElement: () => collapseElRef.current!,
onStateChange: setExpanded,
}),
);

const id = React.useId();
instance.setOptions({
easing,
duration,
collapsedHeight,
getDisclosureElement: () => collapseElRef.current!,
onStateChange: setExpanded,
});

const resolvedOptions: CollapseParams = {
id,
isExpanded: propExpanded,
initialExpanded: propExpanded ?? propDefaultExpanded,
...opts,
};
const assignCollapseRef = React.useCallback(
(node: HTMLElement | null) => {
if (node !== collapseElRef.current) {
instance.cleanup();
}
if (node) {
console.log("setup", node, collapseElRef.current);
collapseElRef.current = node;
instance.setup();
}
},
[instance],
);

const [instance] = React.useState(() => new Collapse(resolvedOptions));
const [state, setState] = React.useState(() => instance.initialState);
React.useEffect(() => {
if (isExpanded !== prevState.current) {
if (isExpanded) {
instance.open();
} else {
instance.close();
}
prevState.current = isExpanded;
}
}, [instance, isExpanded]);

useLayoutEffect(() => {
instance.setOptions((prev) => ({
...prev,
isExpanded: propExpanded ?? state,
onExpandedChange(state) {
setState(state);
resolvedOptions.onExpandedChange?.(state);
},
}));
});
const disclosureId = `collapsed-disclosure-${id}`;

// const resolvedOptions: CollapseParams = {
// id,
// isExpanded: propExpanded,
// initialExpanded: propExpanded ?? propDefaultExpanded,
// ...opts,
// };

// const [instance] = React.useState(() => new Collapse(resolvedOptions));
// const [state, setState] = React.useState(() => instance.initialState);

// useLayoutEffect(() => {
// instance.setOptions((prev) => ({
// ...prev,
// isExpanded: propExpanded ?? state,
// onExpandedChange(state) {
// setState(state);
// resolvedOptions.onExpandedChange?.(state);
// },
// }));
// });

return {
isExpanded: state,
setExpanded: setState,
isExpanded: isExpanded,
setExpanded: setExpanded,
getToggleProps<
Args extends React.ComponentPropsWithoutRef<"button"> & {
[k: string]: unknown;
},
RefKey extends string | undefined = "ref",
>(
args?: Args & {
/**
* Sets the key of the prop that the component uses for ref assignment
* @default 'ref'
*/
refKey?: RefKey;
},
): {
[K in RefKey extends string ? RefKey : "ref"]: AssignableRef<any>;
} & React.ComponentPropsWithoutRef<"button"> {
const { disabled, onClick, refKey, ...rest } = {
refKey: "ref",
onClick() {},
disabled: false,
...args,
};

const isButton = toggleEl ? toggleEl.tagName === "BUTTON" : undefined;

const theirRef: any = args?.[refKey || "ref"];

const props: any = {
"aria-controls": `react-collapsed-panel-${id}`,
"aria-expanded": isExpanded,
onClick(evt: any) {
if (disabled) return;
onClick?.(evt);
setExpanded((n) => !n);
},
[refKey || "ref"]: mergeRefs(theirRef, setToggleEl),
};

const buttonProps = {
type: "button",
disabled: disabled ? true : undefined,
};
const fakeButtonProps = {
"aria-disabled": disabled ? true : undefined,
role: "button",
tabIndex: disabled ? -1 : 0,
};

if (isButton === false) {
return { ...props, ...fakeButtonProps, ...rest };
} else if (isButton === true) {
return { ...props, ...buttonProps, ...rest };
} else {
return {
...props,
...buttonProps,
...fakeButtonProps,
...rest,
};
}
},

getCollapseProps<
Args extends { style?: React.CSSProperties; [k: string]: unknown },
RefKey extends string | undefined = "ref",
>(
args?: Args & {
/**
* Sets the key of the prop that the component uses for ref assignment
* @default 'ref'
*/
refKey?: RefKey;
},
): {
[K in RefKey extends string ? RefKey : "ref"]: AssignableRef<any>;
} & {
id: string;
} {
const { refKey } = { refKey: "ref", ...args };
const theirRef: any = args?.[refKey || "ref"];
console.log({ hasMounted, isExpanded });
return {
id: disclosureId,
...args,
style: !hasMounted && !isExpanded ? { display: "none" } : undefined,
[refKey || "ref"]: mergeRefs(assignCollapseRef, theirRef),
} as any;
},
};
}
42 changes: 42 additions & 0 deletions packages/web-component/app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!doctype html>

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><my-element> ⌲ Home</title>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600|Roboto+Mono"
/>
<script type="module" src="../dist/collapsed-disclosure.js"></script>
<script type="module" src="../dist/collapsed-toggle.js"></script>
<script type="module" src="../dist/collapsed-group.js"></script>
</head>
<body>
<button id="btn" aria-controls="foo" aria-expanded="false">Toggle</button>
<collapsed-disclosure id="foo">
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
<p>hello there</p>
</collapsed-disclosure>
<script type="module">
let disclosure = document.querySelector("#foo");
let open = disclosure.open;
let btn = document.querySelector("#btn");
btn.addEventListener("click", () => {
open = !open;
disclosure.open = open;
btn.setAttribute("aria-expanded", open.toString());
});
</script>
</body>
</html>
Loading