diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 0000000..e5b6d8d
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 0000000..7fcf973
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "restricted",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..a8f2cbc
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,55 @@
+name: Tests
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+jobs:
+ main:
+ name: Tests
+ runs-on: ubuntu-latest
+ # To use Turborepo Remote Caching, set the following environment variables for the job.
+ env:
+ CI: true
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+ TURBO_REMOTE_ONLY: true
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 2
+
+ - name: Cache turbo build setup
+ uses: actions/cache@v4
+ with:
+ path: .turbo
+ key: ${{ runner.os }}-turbo-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-turbo-
+
+ - uses: pnpm/action-setup@v4
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+
+ - name: Install Dependencies
+ run: pnpm install
+
+ - name: Build
+ run: pnpm build
+
+ - name: Lint
+ run: pnpm lint
+
+ - name: Test
+ run: pnpm test
+
+ - name: Cypress
+ run: pnpx cypress install && pnpm cypress:run
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a29c8b6..867d92e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,45 +1,38 @@
-name: Release npm package
+name: Release
on:
push:
branches:
- main
- pull_request:
jobs:
- test:
- name: 'CI'
+ release:
+ name: Publish & Deploy
runs-on: ubuntu-latest
+ env:
+ CI: true
steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - name: Checkout repo
+ uses: actions/checkout@v4
with:
- node-version: 14
- - name: Install Dependencies
- uses: bahmutov/npm-install@v1
- - name: Build
- run: yarn build
- - name: Lint
- run: yarn lint
- - name: Test
- run: yarn test
+ fetch-depth: 0
- release:
- name: Publish to NPM
- needs: test
- # publish only when merged in master on original repo, not on PR
- if: github.repository == 'roginfarrer/react-collapsed' && github.ref == 'refs/heads/main'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: actions/setup-node@v1
+ - uses: pnpm/action-setup@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+
+ - name: Install dependencies
+ run: pnpm i
+
+ - name: Create release PR or publish to npm
+ uses: changesets/action@c2918239208f2162b9d27a87f491375c51592434
with:
- node-version: 14
- - name: Install Dependencies
- uses: bahmutov/npm-install@v1
- - name: Build
- run: yarn build
- - run: npx semantic-release
+ version: pnpm run version
+ publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 37bf862..816e2ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,14 @@
+.output
+.vinxi
*.log
.DS_Store
node_modules
.cache
dist
storybook-static
+.turbo
+.parcel-cache
+.solid
+.next
+
+*storybook.log
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..b185203
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+registry = https://registry.npmjs.org
diff --git a/.nvmrc b/.nvmrc
index c2324e8..209e3ef 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-14.18.0
+20
diff --git a/example/.npmignore b/.prettierignore
similarity index 54%
rename from example/.npmignore
rename to .prettierignore
index 587e4ec..de4d1f0 100644
--- a/example/.npmignore
+++ b/.prettierignore
@@ -1,3 +1,2 @@
+dist
node_modules
-.cache
-dist
\ No newline at end of file
diff --git a/.prettierrc.json5 b/.prettierrc.json5
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/.prettierrc.json5
@@ -0,0 +1 @@
+{}
diff --git a/.storybook/main.js b/.storybook/main.js
deleted file mode 100644
index fdbfec5..0000000
--- a/.storybook/main.js
+++ /dev/null
@@ -1,8 +0,0 @@
-module.exports = {
- stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
- addons: [
- '@storybook/addon-links',
- '@storybook/addon-essentials',
- '@storybook/addon-a11y',
- ],
-}
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index c3e11ea..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,49 +0,0 @@
-Changelog has been moved to [the releases tab](https://github.com/roginfarrer/react-collapsed/releases).
-
----
-
-# 2.0.0
-
-Complete rewrite using React hooks!
-
-- Ends support for React versions < 16.8.x
-- Library now exports a custom hook in lieu of a render prop component
-- Adds support for unmounting the contents of the Collapse element when closed
-
-```js
-import React from 'react'
-import useCollapse from 'react-collapsed'
-
-function Demo() {
- const { getCollapseProps, getToggleProps, isOpen } = useCollapse()
-
- return (
- <>
-
- Collapsed content 🙈
- >
- )
-}
-```
-
-# 1.0.0
-
-Bumped to full release! :)
-
-- `duration`, `easing`, and `delay` now support taking an object with `in` and `out` keys to configure differing in-and-out transitions
-
-# 0.2.0
-
-### Breaking Changes
-
-- `getCollapsibleProps` => `getCollapseProps`. Renamed since it's easier to spell 😅
-
-### Other
-
-- Slew of Flow bug fixes
-- Improved documentation
-
-# 0.1.3
-
-- ESLINT wasn't working properly - fixed this
-- Added `files` key to package.json to improve NPM load
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e8a7e9c..13ca9ae 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,16 +2,35 @@
Thanks for wanting to make this component better!
-### Project setup
+Before proceeding with development, ensure you match one of the following criteria:
-1. Fork and clone the repo
-2. `yarn install` and `yarn dev` to install dependencies and spin up the demo site locally
-3. Create a branch for your PR
+- Fixing a small bug
+- Fixing a larger issue that has been previously discussed and agreed-upon by maintainers
+- Adding a new feature that has been previously discussed and agreed-upon by maintainers
+
+## Development
+
+For [react-collapsed](/packages/react-collapsed):
+
+1. Fork and clone the repo.
+1. `pnpm install` to install dependencies.
+1. `cd packages/react-collapsed` to get into the package directory.
+1. `pnpm storybook` to spin up the storybook.
+1. Implement your changes and tests.
+ a. Run tests with `pnpm test` and lints with `pnpm lint`
+ b. Add cypress tests with any behavior that's difficult to capture in JSDOM.
+1. Commit your work and submit a pull request for review.
+
+It's also a good idea to test server-side rendering behavior with the [next-app](/packages/next-app).
+
+### Other packages
+
+The framework-agnostic core and its adapters have varying development environments. It's best to check the packages `package.json` to see if they have a storybook or application for development.
**Tip:** Keep your main branch pointing at the original repository and make pull requests from branches on your fork. To do this, run:
```bash
-git remote add upstream https://github.com/roginfarrer/react-collapsed.git
+git remote add upstream https://github.com/roginfarrer/collapsed.git
git fetch upstream
git branch --set-upstream-to=upstream/main main
```
@@ -20,4 +39,4 @@ This will add the original repository as a "remote" called "upstream," Then fetc
### Committing and Pushing changes
-Please make sure to run the tests before you commit your changes. You can run `yarn test` to run them (or `yarn test:watch`). Make sure to add new tests for any new features or changes. All tests must pass for a pull request to be accepted.
+Please make sure to run the tests before you commit your changes. You can run `pnpm test` to run them. Make sure to add new tests for any new features or changes. All tests must pass for a pull request to be accepted.
diff --git a/LICENSE b/LICENSE
index fa36fcc..369308a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,20 @@
-MIT License
+The MIT License (MIT)
-Copyright (c) 2019-2020 Rogin Farrer
+Copyright (c) 2019-2024, Rogin Farrer
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/MIGRATION-2.x-to-3.x.md b/MIGRATION-2.x-to-3.x.md
deleted file mode 100644
index e0d1a3d..0000000
--- a/MIGRATION-2.x-to-3.x.md
+++ /dev/null
@@ -1,98 +0,0 @@
-# Migrating from 2.x to 3.x
-
-## BREAKING CHANGES
-
-- `useCollapse` has been completely rewritten in TypeScript, and now exports types.
-- `useCollapse` configuration has changed:
- - `isOpen` -> `isExpanded`
- - `defaultOpen` -> `defaultExpanded`
- - `expandStyles.transitionDuration` and `collapseStyles.transitionDuration` have been moved to a single `duration` property
- - `expandStyles.transitionTimingFunction` and `collapseStyles.transitionTimingFunction` have been moved to a single `easing` property
-- `useCollapse` output has changed:
- - `isOpen` -> `isExpanded`
- - `mountChildren` has been removed. Event hooks are now provided to recreate this feature. [See below for more](#mountChildren)
- - `toggleOpen` has been replaced with `setExpanded`, which requires a boolean that sets the expanded state, or a callback that returns a boolean.
-- The default transition duration has been changed from `500ms` to being calculated based on the height of the collapsed content. Encouraged to leave this default since it will provide more natural animations.
-- The default transition curve has been changed from `cubic-bezier(0.250, 0.460, 0.450, 0.940)` to `ease-in-out`, or `cubic-bezier(0.4, 0, 0.2, 1)`
-
-See below for more detail on the above changes.
-
-## Input
-
-The hook's property names have been changed for clarity:
-
-- `isOpen` -> `isExpanded`
-- `defaultOpen` -> `defaultExpanded`
-
-In 2.x, the customizing the transition duration and easing was done by setting `transitionDuration` and `transitionTimingFunction` in `expandStyles` or `collapseStyles`. Those have been both pulled out and promoted to top-level settings via `duration` and `easing`, respectively.
-
-The default value for `duration` is also no longer a fixed value. Instead, the duration is calculated based on the height of the collapsed content to create more natural transitions.
-
-The transition easing was also updated from a custom curve to a more basic `ease-in-out` curve.
-
-In summary:
-
-```diff
-const collapse = useCollaspse({
- collapseStyles: {},
- expandStyles: {},
- collapsedHeight: number,
-- isOpen: boolean,
-- defaultOpen: boolean,
-+ duration: number,
-+ easing: string,
-+ isExpanded: boolean,
-+ defaultExpanded: boolean,
-+ onCollapseStart() {},
-+ onCollapseEnd() {},
-+ onExpandStart() {},
-+ onExpandEnd() {},
-})
-```
-
-## Output
-
-- `isOpen` -> `isExpanded`
-- `toggleOpen` -> `setExpanded`
-- `mountChildren` has been removed.
-
-`setExpanded` now also supports an argument to set the expanded state. Previously, to toggle the expanded state, you would just call the `toggleOpen` function:
-
-```javascript
-
-```
-
-Now, you must provide a boolean or a function that returns a boolean:
-
-```javascript
-
-```
-
-### `mountChildren`
-
-`mountChildren` has been removed. In order to recreate the same functionality, you can hook into the `onExpandStart` and `onCollapseEnd` hooks:
-
-```javascript
-function Collapse() {
- const [mountChildren, setMountChildren] = useState(false)
- const { getToggleProps, getCollapseProps } = useCollapse({
- onCollapseEnd() {
- setMountChildren(false)
- },
- onExpandStart() {
- setMountChildren(true)
- },
- })
-
- return (
-
-
-
- {mountChildren &&
I will only render when expanded!
}
-
-
- )
-}
-```
diff --git a/README.md b/README.md
index 886185c..f8fe980 100644
--- a/README.md
+++ b/README.md
@@ -1,162 +1,33 @@
-# react-collapsed (useCollapse)
+# 🙈 Collapsed
-[![CI][ci-badge]][ci]
+
![npm bundle size (version)][minzipped-badge]
[![npm version][npm-badge]][npm-version]
[](https://app.netlify.com/sites/react-collapsed/deploys)
-A custom hook for creating accessible expand/collapse components in React. Animates the height using CSS transitions from `0` to `auto`.
-
-## Features
-
-- Handles the height of animations of your elements, `auto` included!
-- 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
-
-[See the demo site!](https://react-collapsed.netlify.app/)
-
-[CodeSandbox demo](https://codesandbox.io/s/magical-browser-vibv2?file=/src/App.tsx)
-
-## Installation
+Headless UI for building flexible and accessible animating expand/collapse sections or disclosures. Animates the height of elements to `auto`.
```bash
-$ yarn add react-collapsed
-# or
-$ npm i react-collapsed
-```
-
-## Usage
-
-### Simple Usage
-
-```js
-import React from 'react'
-import useCollapse from 'react-collapsed'
-
-function Demo() {
- const { getCollapseProps, getToggleProps, isExpanded } = useCollapse()
-
- return (
-
- )
-}
-```
+## [—> View installation and usage docs here! <—](/packages/react-collapsed)
-## API
+### Experimental Framework Adapters
-```js
-const { getCollapseProps, getToggleProps, isExpanded, setExpanded } =
- useCollapse({
- isExpanded: boolean,
- defaultExpanded: boolean,
- expandStyles: {},
- collapseStyles: {},
- collapsedHeight: 0,
- easing: string,
- duration: number,
- onCollapseStart: func,
- onCollapseEnd: func,
- onExpandStart: func,
- onExpandEnd: func,
- })
-```
-
-### `useCollapse` Config
-
-The following are optional properties passed into `useCollapse({ })`:
-
-| 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 |
-| onCollapseStart | function | no-op | Handler called when the collapse animation begins |
-| onCollapseEnd | function | no-op | Handler called when the collapse animation ends |
-| onExpandStart | function | no-op | Handler called when the expand animation begins |
-| onExpandEnd | function | no-op | Handler called when the expand animation ends |
-| hasDisabledAnimation | boolean | false | If true, will disable the animation |
-
-### What you get
-
-| Name | Description |
-| ---------------- | ----------------------------------------------------------------------------------------------------------- |
-| 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 |
-| isExpanded | Whether or not the collapse is expanded (if not controlled) |
-| setExpanded | Sets the hook's internal isExpanded state |
-
-## Alternative Solutions
-
-- [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.
-
-## FAQ
-
-
-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?
-
-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`.
-
-To avoid this, simply move that padding from the element to an element directly nested within in.
-
-```javascript
-// from
-
-
-// to
-
- Much better!
-
-
-```
+[react-collapsed][react-collapsed] is stable and ready to use. I've also been exploring a rewrite with a framework-agnostic core that's also available with a few different framework adapters (indicated with the `@collapsed/` namespace). Here's a breakdown to clarify what's available and their stability:
-
+| Package | Stable |
+| ---------------------------------- | ------ |
+| [react-collapsed][react-collapsed] | ✅ |
+| [@collapsed/core](packages/core) | 🚧 |
+| [@collapsed/react](packages/react) | 🚧 |
+| [@collapsed/solid](packages/solid) | 🚧 |
+| [@collapsed/lit](packages/lit) | 🚧 |
+[react-collapsed]: /packages/react-collapsed
[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%3Amain
+[npm-version]: https://npmjs.org/package/react-collapsed "View this project on npm"
[netlify]: https://app.netlify.com/sites/react-collapsed/deploys
[netlify-badge]: https://api.netlify.com/api/v1/badges/4d285ffc-aa4f-4d32-8549-eb58e00dd2d1/deploy-status
diff --git a/apps/next-app/.eslintrc.json b/apps/next-app/.eslintrc.json
new file mode 100644
index 0000000..bffb357
--- /dev/null
+++ b/apps/next-app/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/apps/next-app/.gitignore b/apps/next-app/.gitignore
new file mode 100644
index 0000000..fd3dbb5
--- /dev/null
+++ b/apps/next-app/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/apps/next-app/CHANGELOG.md b/apps/next-app/CHANGELOG.md
new file mode 100644
index 0000000..f4df05d
--- /dev/null
+++ b/apps/next-app/CHANGELOG.md
@@ -0,0 +1,16 @@
+# next-app
+
+## 0.1.2
+
+### Patch Changes
+
+- Updated dependencies [3e498c4]
+ - react-collapsed@4.2.0
+ - @collapsed/react@5.1.0
+
+## 0.1.1
+
+### Patch Changes
+
+- Updated dependencies [42793ee]
+ - @collapsed/react@5.0.0
diff --git a/apps/next-app/README.md b/apps/next-app/README.md
new file mode 100644
index 0000000..c403366
--- /dev/null
+++ b/apps/next-app/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/apps/next-app/next.config.mjs b/apps/next-app/next.config.mjs
new file mode 100644
index 0000000..4678774
--- /dev/null
+++ b/apps/next-app/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
diff --git a/apps/next-app/package.json b/apps/next-app/package.json
new file mode 100644
index 0000000..a08c876
--- /dev/null
+++ b/apps/next-app/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "next-app",
+ "version": "0.1.2",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@collapsed/react": "workspace:*",
+ "next": "14.2.3",
+ "react": "^18",
+ "react-collapsed": "workspace:*",
+ "react-dom": "^18"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "eslint-config-next": "14.2.4",
+ "typescript": "^5"
+ }
+}
diff --git a/apps/next-app/src/app/Collapse.tsx b/apps/next-app/src/app/Collapse.tsx
new file mode 100644
index 0000000..61cb084
--- /dev/null
+++ b/apps/next-app/src/app/Collapse.tsx
@@ -0,0 +1,50 @@
+"use client";
+
+import { useCollapse as useCollapsedReact } from "@collapsed/react";
+import { useCollapse as useReactCollapsed } from "react-collapsed";
+
+export function CollapsedReact() {
+ const { getCollapseProps, getToggleProps, isExpanded } = useCollapsedReact();
+
+ return (
+
- In the morning I walked down the Boulevard to the rue Soufflot for
- coffee and brioche. It was a fine morning. The horse-chestnut trees in
- the Luxembourg gardens were in bloom. There was the pleasant
- early-morning feeling of a hot day. I read the papers with the coffee
- and then smoked a cigarette. The flower-women were coming up from the
- market and arranging their daily stock. Students went by going up to the
- law school, or down to the Sorbonne. The Boulevard was busy with trams
- and people going to work.
-
-
- )
-}
-
-const App = () => {
- return (
-
-
-
- )
-}
-
-ReactDOM.render(, document.getElementById('root'))
diff --git a/example/package.json b/example/package.json
deleted file mode 100644
index a50960f..0000000
--- a/example/package.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "name": "example",
- "version": "1.0.0",
- "main": "index.js",
- "license": "MIT",
- "scripts": {
- "start": "parcel index.html",
- "build": "parcel build index.html"
- },
- "dependencies": {
- "react-app-polyfill": "^1.0.0"
- },
- "alias": {
- "react": "../node_modules/react",
- "react-dom": "../node_modules/react-dom/profiling",
- "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
- },
- "devDependencies": {
- "@types/react": "^16.9.11",
- "@types/react-dom": "^16.8.4",
- "parcel": "^1.12.3",
- "typescript": "^3.4.5"
- }
-}
diff --git a/example/tsconfig.json b/example/tsconfig.json
deleted file mode 100644
index 6d51867..0000000
--- a/example/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "compilerOptions": {
- "allowSyntheticDefaultImports": false,
- "target": "es5",
- "module": "commonjs",
- "jsx": "react",
- "moduleResolution": "node",
- "noImplicitAny": false,
- "noUnusedLocals": false,
- "noUnusedParameters": false,
- "removeComments": true,
- "strictNullChecks": true,
- "preserveConstEnums": true,
- "sourceMap": true,
- "lib": ["es2015", "es2016", "dom"],
- "baseUrl": ".",
- "types": ["node"]
- }
-}
diff --git a/internal/build/index.d.ts b/internal/build/index.d.ts
new file mode 100644
index 0000000..47eb7c3
--- /dev/null
+++ b/internal/build/index.d.ts
@@ -0,0 +1,18 @@
+import type { defineConfig } from 'tsup'
+
+export type TsupConfig = ReturnType
+
+export function getTsupConfig(
+ entry: string | string[],
+ args: {
+ packageName: string
+ packageVersion: string
+ external?: string[]
+ define?: Record
+ }
+): TsupConfig
+
+export function getPackageInfo(packageRoot: string): {
+ version: string
+ name: string
+}
diff --git a/internal/build/index.js b/internal/build/index.js
new file mode 100644
index 0000000..ce5142e
--- /dev/null
+++ b/internal/build/index.js
@@ -0,0 +1,93 @@
+import * as fs from "node:fs";
+import * as path from "node:path";
+import { defineConfig } from "tsup";
+import { postbuild } from "./postbuild.js";
+
+export function getTsupConfig(
+ entry,
+ { packageName, packageVersion, external = [] },
+) {
+ entry = Array.isArray(entry) ? entry : [entry];
+ external = ["react", "react-dom", ...external];
+ let banner = createBanner(packageName, packageVersion);
+ return defineConfig([
+ // cjs.dev.js
+ {
+ entry,
+ format: "cjs",
+ sourcemap: true,
+ outExtension: getOutExtension("dev"),
+ external,
+ banner: { js: banner },
+ define: {
+ "process.env.NODE_ENV": "true",
+ },
+ target: "es2016",
+ onSuccess: postbuild,
+ clean: true,
+ dts: { banner },
+ },
+
+ // cjs.prod.js
+ {
+ entry,
+ format: "cjs",
+ minify: true,
+ minifySyntax: true,
+ outExtension: getOutExtension("prod"),
+ external,
+ define: {
+ "process.env.NODE_ENV": "false",
+ },
+ target: "es2016",
+ dts: { banner },
+ },
+
+ // esm
+ {
+ entry,
+ dts: { banner },
+ format: "esm",
+ external,
+ banner: { js: banner },
+ define: {
+ "process.env.NODE_ENV": "true",
+ },
+ target: "es2020",
+ },
+ ]);
+}
+
+/**
+ * @param {"dev" | "prod"} env
+ */
+function getOutExtension(env) {
+ return ({ format }) => ({ js: `.${format}.${env}.js` });
+}
+
+/**
+ * @param {string} packageName
+ * @param {string} version
+ */
+function createBanner(packageName, version) {
+ return `/**
+ * ${packageName} v${version}
+ *
+ * Copyright (c) 2019-${new Date().getFullYear()}, Rogin Farrer
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.md file in the root directory of this source tree.
+ *
+ * @license MIT
+ */
+`;
+}
+
+export function getPackageInfo(packageRoot) {
+ let packageJson = fs.readFileSync(
+ path.join(packageRoot, "package.json"),
+ "utf8",
+ );
+ let { version, name } = JSON.parse(packageJson);
+ return { version, name };
+}
diff --git a/internal/build/package.json b/internal/build/package.json
new file mode 100644
index 0000000..64d0c7d
--- /dev/null
+++ b/internal/build/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@collapsed-internal/build",
+ "type": "module",
+ "private": true,
+ "version": "0.0.0",
+ "dependencies": {
+ "tsup": "^8"
+ },
+ "main": "index.js",
+ "files": [
+ "postbuild.js"
+ ],
+ "types": "index.d.ts",
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/internal/build/postbuild.js b/internal/build/postbuild.js
new file mode 100644
index 0000000..4aeb4a6
--- /dev/null
+++ b/internal/build/postbuild.js
@@ -0,0 +1,46 @@
+import * as fsp from "node:fs/promises";
+import * as path from "node:path";
+
+export async function postbuild() {
+ let fileNameBase = "index";
+ let cjsEntry = `"use strict";
+
+if (process.env.NODE_ENV === "production") {
+ module.exports = require("./${fileNameBase}.cjs.prod.js");
+} else {
+ module.exports = require("./${fileNameBase}.cjs.dev.js");
+}
+`;
+
+ await fsp.writeFile(
+ path.join(process.cwd(), "dist", `${fileNameBase}.cjs.js`),
+ cjsEntry,
+ );
+
+ await fsp.writeFile(path.join(process.cwd(), "LICENSE"), getLicenseContent());
+ console.log("🛠Done building");
+}
+
+function getLicenseContent() {
+ return `The MIT License (MIT)
+
+Copyright (c) 2019-${new Date().getFullYear()}, Rogin Farrer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+`;
+}
diff --git a/internal/build/tsconfig.json b/internal/build/tsconfig.json
new file mode 100644
index 0000000..d7f03da
--- /dev/null
+++ b/internal/build/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "extends": "@collapsed-internal/tsconfig/base.json",
+ "include": ["."]
+}
diff --git a/internal/eslint-config-collapsed/index.cjs b/internal/eslint-config-collapsed/index.cjs
new file mode 100644
index 0000000..82db1df
--- /dev/null
+++ b/internal/eslint-config-collapsed/index.cjs
@@ -0,0 +1,12 @@
+/** @type {import('eslint').ESLint.ConfigData} */
+module.exports = {
+ extends: ["plugin:@typescript-eslint/recommended"],
+ parser: "@typescript-eslint/parser",
+ plugins: ["@typescript-eslint"],
+ rules: {
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "prefer-const": "off",
+ },
+};
diff --git a/internal/eslint-config-collapsed/package.json b/internal/eslint-config-collapsed/package.json
new file mode 100644
index 0000000..6c6631f
--- /dev/null
+++ b/internal/eslint-config-collapsed/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "eslint-config-collapsed",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "^6",
+ "@typescript-eslint/parser": "^6",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-plugin-jsx-a11y": "^6.7.1",
+ "eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-react": "^7.32.2",
+ "eslint-plugin-react-hooks": "^4.6.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "main": "index.js"
+}
diff --git a/internal/eslint-config-collapsed/react.cjs b/internal/eslint-config-collapsed/react.cjs
new file mode 100644
index 0000000..1740cb3
--- /dev/null
+++ b/internal/eslint-config-collapsed/react.cjs
@@ -0,0 +1,14 @@
+module.exports = {
+ extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended'],
+ plugins: ['jsx-a11y'],
+ settings: {
+ react: {
+ version: 'detect',
+ },
+ },
+ rules: {
+ 'react/prop-types': 'off',
+ 'react/jsx-uses-react': 'off',
+ 'react/react-in-jsx-scope': 'off',
+ },
+}
diff --git a/tsconfig.json b/internal/tsconfig/base.json
similarity index 76%
rename from tsconfig.json
rename to internal/tsconfig/base.json
index 962973e..b3621d2 100644
--- a/tsconfig.json
+++ b/internal/tsconfig/base.json
@@ -1,10 +1,10 @@
{
- "include": ["src"],
- "exclude": ["src/__tests__/**/*", "src/stories/**/*"],
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "display": "Base",
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
- "lib": ["dom", "ESNext"],
+ "lib": ["dom", "ESNext", "DOM.Iterable"],
"importHelpers": true,
"sourceMap": true,
"strict": true,
@@ -18,8 +18,7 @@
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
- "moduleResolution": "node",
- "jsx": "react",
+ "moduleResolution": "Bundler",
"esModuleInterop": true,
"skipLibCheck": true
}
diff --git a/internal/tsconfig/package.json b/internal/tsconfig/package.json
new file mode 100644
index 0000000..8497493
--- /dev/null
+++ b/internal/tsconfig/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@collapsed-internal/tsconfig",
+ "version": "0.0.0",
+ "private": true,
+ "publishConfig": {
+ "access": "public"
+ },
+ "files": [
+ "base.json",
+ "react.json"
+ ]
+}
diff --git a/internal/tsconfig/react.json b/internal/tsconfig/react.json
new file mode 100644
index 0000000..a5619b3
--- /dev/null
+++ b/internal/tsconfig/react.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./base.json",
+ "display": "React",
+ "compilerOptions": {
+ "jsx": "react-jsx"
+ }
+}
diff --git a/package.json b/package.json
index f662962..c7f6e49 100644
--- a/package.json
+++ b/package.json
@@ -1,110 +1,30 @@
{
- "name": "react-collapsed",
- "version": "0.0.0-development",
- "author": "Rogin Farrer ",
- "description": "A tiny React custom-hook for creating flexible and accessible expand/collapse components.",
- "license": "MIT",
- "source": "src/index.ts",
- "main": "dist/react-collapsed.js",
- "module": "dist/react-collapsed.esm.js",
- "umd:main": "dist/react-collapsed.umd.js",
- "unpkg": "dist/react-collapsed.umd.js",
- "types": "dist/index.d.ts",
- "files": [
- "src",
- "dist"
- ],
+ "name": "collapse-workspace",
+ "version": "0.0.0",
+ "private": true,
"scripts": {
- "watch": "microbundle watch",
- "build": "microbundle",
- "test": "jest src",
- "lint": "tsc --project tsconfig.json --noEmit",
+ "preinstall": "npx only-allow pnpm",
+ "build": "turbo build",
+ "test": "turbo test",
+ "lint": "turbo lint",
+ "cypress:run": "turbo cypress:run",
"format": "prettier --write **/*.{js,ts,tsx,yml,md,md,json}",
- "storybook": "start-storybook -p 6006",
- "build-storybook": "build-storybook",
- "release": "np --no-2fa"
- },
- "peerDependencies": {
- "react": ">=16.8",
- "react-dom": ">=16.8"
- },
- "devDependencies": {
- "@babel/core": "^7.8.4",
- "@babel/eslint-parser": "^7.15.4",
- "@babel/preset-react": "^7.14.5",
- "@storybook/addon-a11y": "^6.5.11",
- "@storybook/addon-actions": "^6.5.11",
- "@storybook/addon-essentials": "^6.5.11",
- "@storybook/addon-links": "^6.5.11",
- "@storybook/react": "^6.5.11",
- "@testing-library/jest-dom": "^5.3.0",
- "@testing-library/react": "^13.4.0",
- "@types/jest": "^25.1.2",
- "@types/node": "^16.7.13",
- "@types/raf": "^3.4.0",
- "@types/react": "^18.0.18",
- "@types/react-dom": "^18.0.6",
- "@types/styled-components": "^5.0.1",
- "@typescript-eslint/eslint-plugin": "^4.31.0",
- "@typescript-eslint/parser": "^4.31.0",
- "babel-loader": "^8.2.2",
- "eslint": "^7.32.0",
- "eslint-config-airbnb": "^18.2.1",
- "eslint-config-airbnb-typescript": "^14.0.0",
- "eslint-config-prettier": "^8.3.0",
- "eslint-config-rogin": "1.0.0",
- "eslint-plugin-html": "^6.1.2",
- "eslint-plugin-import": "^2.24.2",
- "eslint-plugin-jsx-a11y": "^6.4.1",
- "eslint-plugin-prettier": "^4.0.0",
- "eslint-plugin-react": "^7.25.1",
- "eslint-plugin-react-hooks": "^4.2.0",
- "jest": "^27.1.0",
- "microbundle": "^0.13.3",
- "np": "^6.4.0",
- "prettier": "^2.3.2",
- "react": "^18.2.0",
- "react-docgen-typescript-loader": "^3.7.1",
- "react-dom": "^18.2.0",
- "semantic-release": "^18.0.0",
- "styled-components": "^5.2.0",
- "ts-jest": "^27.0.5",
- "typescript": "^4.4.2"
+ "version": "changeset version && pnpm install --lockfile-only",
+ "release": "pnpm build && changeset publish",
+ "typecheck": "turbo typecheck"
},
"dependencies": {
- "raf": "^3.4.1",
- "tiny-warning": "^1.0.3"
- },
- "jest": {
- "preset": "ts-jest",
- "testEnvironment": "jsdom",
- "setupFilesAfterEnv": [
- "/setupTests.ts"
- ],
- "globals": {
- "__DEV__": true
- },
- "testMatch": [
- "/**/*.(spec|test).{ts,tsx,js,jsx}"
- ]
- },
- "repository": {
- "type": "git",
- "url": "https://github.com/roginfarrer/react-collapsed.git"
+ "@changesets/cli": "^2.27.3",
+ "buffer": "^5.5.0",
+ "np": "^6.4.0",
+ "one-version": "^0.2.0",
+ "prettier": "^3",
+ "process": "^0.11.10",
+ "turbo": "^1.13.3"
},
- "bugs": {
- "url": "https://github.com/roginfarrer/react-collapsed/issues"
+ "alias": {
+ "process": "process/browser.js",
+ "buffer": "buffer"
},
- "keywords": [
- "collapse",
- "react",
- "collapsible",
- "animate",
- "height",
- "render",
- "expand",
- "hooks",
- "auto"
- ],
- "prettier": "eslint-config-rogin/prettier"
+ "packageManager": "pnpm@9.1.2"
}
diff --git a/packages/core/.eslintignore b/packages/core/.eslintignore
new file mode 100644
index 0000000..de4d1f0
--- /dev/null
+++ b/packages/core/.eslintignore
@@ -0,0 +1,2 @@
+dist
+node_modules
diff --git a/packages/core/.eslintrc.cjs b/packages/core/.eslintrc.cjs
new file mode 100644
index 0000000..79923ff
--- /dev/null
+++ b/packages/core/.eslintrc.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ["../../internal/eslint-config-collapsed/index.cjs"],
+};
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
new file mode 100644
index 0000000..90d63b3
--- /dev/null
+++ b/packages/core/CHANGELOG.md
@@ -0,0 +1,30 @@
+# @collapsed/core
+
+## 5.0.0
+
+### Major Changes
+
+- 42793ee: Full rewrite to narrow API to collapse animation and remove internal state. Intended for use with framework wrappers.
+
+## 4.0.1
+
+### Patch Changes
+
+- cd21fd4: Remove package.json exports
+
+## 4.0.0
+
+### Major Changes
+
+- 1ee93e8: ## BREAKING CHANGES
+
+ - Adopts React 18's `useId`, making the library incompatible with React <18
+ - Switch to `tsup` from `microbundle` for bundling library. No longer exports a UMD version, just CJS and MJS
+
+ ## Features & Bug fixes
+
+ - Refactors core functionality to vanilla JS (with a React) adapter, which I think should fix #103 and fix #100
+ - Added `onExpandedChange` option
+ - Tries to detect if `getToggleProps` is used. If the toggle element ref can be accessed, the `aria-labelledby` attribute will be added to the collapse element
+ - Added `role="region"` to collapse
+ - Updated toggle props to pass the appropriate attributes to the element, whether it's a button or not
diff --git a/packages/core/README.md b/packages/core/README.md
new file mode 100644
index 0000000..70972a6
--- /dev/null
+++ b/packages/core/README.md
@@ -0,0 +1,21 @@
+# @collapsed/core
+
+
+
+
+A framework-agnostic utility for creating accessible expand/collapse components. Animates the height using CSS transitions from `0` to `auto`.
+
+> You might be looking for the [react hook](../react)
+
+## Features
+
+- Handles the height of animations of your elements, `auto` included!
+- 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
+
+## Installation
+
+```bash
+npm install @collapsed/core
+```
diff --git a/packages/core/package.json b/packages/core/package.json
new file mode 100644
index 0000000..18c18ef
--- /dev/null
+++ b/packages/core/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@collapsed/core",
+ "version": "5.0.0",
+ "type": "module",
+ "author": "Rogin Farrer ",
+ "description": "A framework-agnostic utility for creating flexible and accessible expand/collapse elements.",
+ "license": "MIT",
+ "main": "./dist/Collapse.cjs",
+ "module": "./dist/Collapse.js",
+ "types": "./dist/Collapse.d.ts",
+ "exports": {
+ ".": {
+ "require": "./dist/Collapse.cjs",
+ "import": "./dist/Collapse.js",
+ "types": "./dist/Collapse.d.ts"
+ }
+ },
+ "files": [
+ "dist",
+ "src"
+ ],
+ "scripts": {
+ "build": "tsup",
+ "dev": "tsup --watch",
+ "test": "vitest",
+ "typecheck": "tsc --project tsconfig.json --noEmit",
+ "lint": "eslint .",
+ "format": "prettier --write **/*.{js,ts,tsx,yml,md,md,json}"
+ },
+ "devDependencies": {
+ "@collapsed-internal/build": "workspace:*",
+ "@collapsed-internal/tsconfig": "workspace:*",
+ "@types/node": "^20",
+ "eslint-config-collapsed": "workspace:*",
+ "tslib": "^2.4.1",
+ "tsup": "^8",
+ "vitest": "^1.6.0"
+ },
+ "dependencies": {
+ "tiny-warning": "^1.0.3"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/roginfarrer/collapsed.git",
+ "directory": "packages/core"
+ },
+ "keywords": [
+ "collapse",
+ "collapsible",
+ "animate",
+ "height",
+ "render",
+ "expand",
+ "auto"
+ ],
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/core/src/Collapse.ts b/packages/core/src/Collapse.ts
new file mode 100644
index 0000000..880d07d
--- /dev/null
+++ b/packages/core/src/Collapse.ts
@@ -0,0 +1,194 @@
+import {
+ Frame,
+ clearAnimationTimeout,
+ setAnimationTimeout,
+ getAutoHeightDuration,
+ paddingWarning,
+} from "./utils";
+
+let durationMap = new Map();
+function calcDuration(height: number): number {
+ if (!durationMap.has(height)) {
+ durationMap.set(height, getAutoHeightDuration(height));
+ }
+ return durationMap.get(height)!;
+}
+
+export interface CollapseOptions {
+ /** Handler called when the expanded state changes */
+ onExpandedChange?: (open: boolean) => void;
+ /**
+ * Handler called at each stage of the animation.
+ */
+ onTransitionStateChange?: (
+ state:
+ | "expandStart"
+ | "expanding"
+ | "expandEnd"
+ | "collapseStart"
+ | "collapsing"
+ | "collapseEnd",
+ ) => void;
+ getDisclosureElement: () => HTMLElement;
+ /** Timing function for the transition */
+ easing?: string;
+ /**
+ * Duration of the expand/collapse animation.
+ * If 'auto', the duration will be calculated based on the height of the collapse element
+ */
+ duration?: "auto" | number;
+ /** Height in pixels that the collapse element collapses to */
+ collapsedHeight?: number;
+}
+
+export class Collapse {
+ #options: Required;
+
+ frameId?: number;
+ endFrameId?: Frame;
+
+ constructor(options: CollapseOptions) {
+ this.#options = {
+ easing: "cubic-bezier(0.4, 0, 0.2, 1)",
+ duration: "auto",
+ collapsedHeight: 0,
+ onExpandedChange() {},
+ onTransitionStateChange() {},
+ ...options,
+ };
+ }
+
+ setOptions(opts: Partial): void {
+ this.#options = { ...this.#options, ...opts };
+ }
+
+ #getElement(): HTMLElement {
+ return this.#options.getDisclosureElement();
+ }
+
+ #getDuration(height: number): number {
+ let num =
+ this.#options.duration === "auto"
+ ? calcDuration(height)
+ : this.#options.duration;
+ return num;
+ }
+
+ #setTransitionEndTimeout = (
+ state: "open" | "close",
+ duration: number,
+ ): void => {
+ const endTransition = () => {
+ let target = this.#getElement();
+ target.style.removeProperty("transition");
+ if (state === "close") {
+ // Closed
+ this.setCollapsedStyles();
+ this.frameId = requestAnimationFrame(() => {
+ this.#options.onTransitionStateChange("collapseEnd");
+ });
+ } else {
+ target.style.removeProperty("height");
+ target.style.removeProperty("overflow");
+ target.style.removeProperty("display");
+ this.frameId = requestAnimationFrame(() => {
+ this.#options.onTransitionStateChange("expandEnd");
+ });
+ }
+ };
+ if (this.endFrameId) {
+ clearAnimationTimeout(this.endFrameId);
+ }
+ this.endFrameId = setAnimationTimeout(endTransition, duration);
+ };
+
+ public getCollapsedStyles(): Record {
+ let styles: Record = {
+ height: `${this.#options.collapsedHeight}px`,
+ overflow: "hidden",
+ display: this.#options.collapsedHeight === 0 ? "none" : "block",
+ };
+ return styles;
+ }
+
+ public setCollapsedStyles(): void {
+ let target = this.#getElement();
+ for (let [property, value] of Object.entries(this.getCollapsedStyles())) {
+ target.style.setProperty(property, value);
+ }
+ }
+
+ public unsetCollapsedStyles(): void {
+ let target = this.#getElement();
+ for (let property of Object.keys(this.getCollapsedStyles())) {
+ target.style.removeProperty(property);
+ }
+ }
+
+ public open(): void {
+ const target = this.#getElement();
+ this.#options.onExpandedChange(true);
+
+ if (this.frameId) cancelAnimationFrame(this.frameId);
+ if (this.endFrameId) clearAnimationTimeout(this.endFrameId);
+
+ if (prefersReducedMotion()) {
+ target.style.removeProperty("display");
+ target.style.removeProperty("height");
+ target.style.removeProperty("overflow");
+ return;
+ }
+
+ paddingWarning(target);
+ this.frameId = requestAnimationFrame(() => {
+ this.#options.onTransitionStateChange("expandStart");
+ target.style.setProperty("display", "block");
+ target.style.setProperty("overflow", "hidden");
+ target.style.setProperty("height", `${this.#options.collapsedHeight}px`);
+
+ this.frameId = requestAnimationFrame(() => {
+ this.#options.onTransitionStateChange("expanding");
+ const height = target.scrollHeight;
+ const duration = this.#getDuration(height);
+ this.#setTransitionEndTimeout("open", duration);
+ target.style.transition = `height ${duration}ms ${this.#options.easing}`;
+ target.style.height = `${height}px`;
+ });
+ });
+ }
+
+ public close(): void {
+ this.#options.onExpandedChange(false);
+
+ if (this.frameId) cancelAnimationFrame(this.frameId);
+ if (this.endFrameId) clearAnimationTimeout(this.endFrameId);
+
+ if (prefersReducedMotion()) {
+ this.setCollapsedStyles();
+ return;
+ }
+
+ this.#options.onTransitionStateChange("collapseStart");
+ this.frameId = requestAnimationFrame(() => {
+ const target = this.#getElement();
+
+ const height = target.scrollHeight;
+ const duration = this.#getDuration(height);
+ this.#setTransitionEndTimeout("close", duration);
+ target.style.transition = `height ${duration}ms ${this.#options.easing}`;
+ target.style.height = `${height}px`;
+
+ this.frameId = requestAnimationFrame(() => {
+ this.#options.onTransitionStateChange("collapsing");
+ target.style.overflow = "hidden";
+ target.style.height = `${this.#options.collapsedHeight}px`;
+ });
+ });
+ }
+}
+
+/** Tells if the user has enabled the "reduced motion" setting in their browser or OS. */
+export function prefersReducedMotion() {
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
+ return query.matches;
+}
diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts
new file mode 100644
index 0000000..5fb54d9
--- /dev/null
+++ b/packages/core/src/utils.ts
@@ -0,0 +1,55 @@
+import warning from "tiny-warning";
+
+// https://github.com/mui-org/material-ui/blob/da362266f7c137bf671d7e8c44c84ad5cfc0e9e2/packages/material-ui/src/styles/transitions.js#L89-L98
+export function getAutoHeightDuration(height?: number): number {
+ if (!height) {
+ return 0;
+ }
+
+ const constant = height / 36;
+
+ // 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 paddingWarning(element: HTMLElement): void {
+ if (process.env.NODE_ENV !== "production") {
+ if (window && "getComputedStyle" in window) {
+ const { paddingTop, paddingBottom } = window.getComputedStyle(element);
+ const hasPadding =
+ (paddingTop && paddingTop !== "0px") ||
+ (paddingBottom && paddingBottom !== "0px");
+
+ warning(
+ !hasPadding,
+ "Collapse: Padding applied to the collapse element will cause the animation to break and not perform as expected. To fix, apply equivalent padding to the direct descendent of the collapse element.",
+ );
+ }
+ }
+}
+
+export type Frame = {
+ id?: number;
+};
+
+export function setAnimationTimeout(callback: () => void, timeout: number) {
+ const startTime = performance.now();
+ const frame: Frame = {};
+
+ function call() {
+ frame.id = requestAnimationFrame((now) => {
+ if (now - startTime > timeout) {
+ callback();
+ } else {
+ call();
+ }
+ });
+ }
+
+ call();
+ return frame;
+}
+
+export function clearAnimationTimeout(frame: Frame) {
+ if (frame.id) cancelAnimationFrame(frame.id);
+}
diff --git a/packages/core/tests/Collapse.test.ts b/packages/core/tests/Collapse.test.ts
new file mode 100644
index 0000000..07e4eb3
--- /dev/null
+++ b/packages/core/tests/Collapse.test.ts
@@ -0,0 +1,18 @@
+import { Collapse } from "../src/Collapse";
+import { expect, test } from "vitest";
+
+test("Collapse has expected properties", () => {
+ const collapse = new Collapse({
+ // @ts-expect-error Element doesn't matter here
+ getDisclosureElement: () => {},
+ });
+
+ expect(typeof collapse.close).toBe("function");
+ expect(typeof collapse.open).toBe("function");
+ expect(typeof collapse.setCollapsedStyles).toBe("function");
+ expect(typeof collapse.setOptions).toBe("function");
+ expect(typeof collapse.unsetCollapsedStyles).toBe("function");
+ expect(collapse.getCollapsedStyles()).toMatchObject(
+ expect.objectContaining({ height: expect.stringMatching("") }),
+ );
+});
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
new file mode 100644
index 0000000..7640397
--- /dev/null
+++ b/packages/core/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "@collapsed-internal/tsconfig/base.json",
+ "include": ["."],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts
new file mode 100644
index 0000000..4d57891
--- /dev/null
+++ b/packages/core/tsup.config.ts
@@ -0,0 +1,33 @@
+import * as fs from "node:fs";
+import * as path from "node:path";
+import { defineConfig } from "tsup";
+
+let packageJson = fs.readFileSync(path.join(__dirname, "package.json"), "utf8");
+let { version, name } = JSON.parse(packageJson);
+
+/**
+ * @param {string} packageName
+ * @param {string} version
+ */
+let banner = `/**
+ * ${name} v${version}
+ *
+ * Copyright (c) 2019-${new Date().getFullYear()}, Rogin Farrer
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE.md file in the root directory of this source tree.
+ *
+ * @license MIT
+ */
+`;
+
+export default defineConfig([
+ {
+ entry: ["./src/Collapse.ts"],
+ format: ["esm", "cjs"],
+ clean: true,
+ minify: true,
+ banner: { js: banner },
+ dts: { banner },
+ },
+]);
diff --git a/packages/lit/.eslintignore b/packages/lit/.eslintignore
new file mode 100644
index 0000000..de4d1f0
--- /dev/null
+++ b/packages/lit/.eslintignore
@@ -0,0 +1,2 @@
+dist
+node_modules
diff --git a/packages/lit/.eslintrc.cjs b/packages/lit/.eslintrc.cjs
new file mode 100644
index 0000000..79923ff
--- /dev/null
+++ b/packages/lit/.eslintrc.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: ["../../internal/eslint-config-collapsed/index.cjs"],
+};
diff --git a/packages/lit/.storybook/main.ts b/packages/lit/.storybook/main.ts
new file mode 100644
index 0000000..58d2823
--- /dev/null
+++ b/packages/lit/.storybook/main.ts
@@ -0,0 +1,24 @@
+import type { StorybookConfig } from "@storybook/web-components-vite";
+
+import { join, dirname } from "path";
+
+/**
+ * This function is used to resolve the absolute path of a package.
+ * It is needed in projects that use Yarn PnP or are set up within a monorepo.
+ */
+function getAbsolutePath(value: string): any {
+ return dirname(require.resolve(join(value, "package.json")));
+}
+const config: StorybookConfig = {
+ stories: ["../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
+ addons: [
+ getAbsolutePath("@storybook/addon-links"),
+ getAbsolutePath("@storybook/addon-essentials"),
+ getAbsolutePath("@chromatic-com/storybook"),
+ ],
+ framework: {
+ name: getAbsolutePath("@storybook/web-components-vite"),
+ options: {},
+ },
+};
+export default config;
diff --git a/packages/lit/.storybook/preview.ts b/packages/lit/.storybook/preview.ts
new file mode 100644
index 0000000..0e1adde
--- /dev/null
+++ b/packages/lit/.storybook/preview.ts
@@ -0,0 +1,14 @@
+import type { Preview } from "@storybook/web-components";
+
+const preview: Preview = {
+ parameters: {
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/packages/lit/CHANGELOG.md b/packages/lit/CHANGELOG.md
new file mode 100644
index 0000000..134cea8
--- /dev/null
+++ b/packages/lit/CHANGELOG.md
@@ -0,0 +1,9 @@
+# @collapsed/lit
+
+## 0.0.1
+
+### Patch Changes
+
+- 42793ee: New implementation of collapsed as a Lit web component.
+- Updated dependencies [42793ee]
+ - @collapsed/core@5.0.0
diff --git a/packages/lit/README.md b/packages/lit/README.md
new file mode 100644
index 0000000..de39948
--- /dev/null
+++ b/packages/lit/README.md
@@ -0,0 +1,47 @@
+# @collapsed/lit
+
+
+
+
+A Lit element for creating accessible expand/collapse elements. Animates the height using CSS transitions from `0` to `auto`.
+
+## Features
+
+- Handles the height of animations of your elements, `auto` included!
+- Minimally-styled to be functional--you control the styles.
+- No animation framework required! Simply powered by CSS animations.
+
+## Installation
+
+```bash
+npm install @collapsed/lit
+```
+
+## Usage
+
+```tsx
+import { CollapsedDisclosure } from "@collapsed/lit";
+import { html } from "lit";
+
+export function App() {
+ function handleClick(evt) {
+ const collapse = document.querySelector("#disclosure");
+ const btn = event.target;
+ collapse.toggleAttribute("open");
+ btn.setAttribute("aria-expanded", collapse.hasAttribute("open").toString());
+ }
+
+ return html`
+ In the morning I walked down the Boulevard to the rue Soufflot for
+ coffee and brioche. It was a fine morning. The horse-chestnut trees
+ in the Luxembourg gardens were in bloom. There was the pleasant
+ early-morning feeling of a hot day. I read the papers with the
+ coffee and then smoked a cigarette. The flower-women were coming up
+ from the market and arranging their daily stock. Students went by
+ going up to the law school, or down to the Sorbonne. The Boulevard
+ was busy with trams and people going to work.
+
+ );
+}
+```
+
+## API
+
+```js
+const { getCollapseProps, getToggleProps, isExpanded, setExpanded } =
+ useCollapse({
+ isExpanded: boolean,
+ defaultExpanded: boolean,
+ collapsedHeight: 0,
+ easing: string,
+ duration: number,
+ onTransitionStateChange: func,
+ });
+```
+
+### `useCollapse` Config
+
+The following are optional properties passed into `useCollapse({ })`:
+
+| 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 |
+| 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 |
+| onTransitionStateChange | function | no-op | Handler called with at each stage of the transition animation |
+| hasDisabledAnimation | boolean | false | If true, will disable the animation |
+| id | string \| number | `undefined` | Unique identifier used to for associating elements appropriately for accessibility. |
+
+### What you get
+
+| Name | Description |
+| ---------------- | ----------------------------------------------------------------------------------------------------------- |
+| 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 |
+| isExpanded | Whether or not the collapse is expanded (if not controlled) |
+| setExpanded | Sets the hook's internal isExpanded state |
+
+## Alternative Solutions
+
+- [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.
+
+## FAQ
+
+
+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?
+
+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`.
+
+To avoid this, simply move that padding from the element to an element directly nested within in.
+
+```javascript
+// from
+
+
+// to
+