├── .changeset
├── README.md
└── config.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── docs.yml
│ ├── publish.yml
│ ├── snapshot.yml
│ └── verify.yml
├── .gitignore
├── .npmrc
├── .yarnrc.yml
├── CONTRIBUTING.md
├── LICENSE
├── examples
├── basic
│ ├── index.html
│ ├── package.json
│ ├── readme.MD
│ ├── src
│ │ ├── data.ts
│ │ ├── main.tsx
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.js
├── comprehensive
│ ├── index.html
│ ├── package.json
│ ├── readme.MD
│ ├── src
│ │ ├── data.ts
│ │ ├── main.tsx
│ │ ├── style.css
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.js
└── react-compiler
│ ├── index.html
│ ├── package.json
│ ├── readme.MD
│ ├── src
│ ├── data.ts
│ ├── main.tsx
│ ├── style.css
│ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.js
├── homepagedata.json
├── ideas.md
├── lerna.json
├── nx.json
├── package.json
├── packages
├── core
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── core
│ │ │ ├── build-proxified-instance.ts
│ │ │ ├── build-static-instance.ts
│ │ │ ├── core.spec.ts
│ │ │ └── create-tree.ts
│ │ ├── features
│ │ │ ├── async-data-loader
│ │ │ │ ├── async-data-loader.spec.ts
│ │ │ │ ├── feature.ts
│ │ │ │ └── types.ts
│ │ │ ├── drag-and-drop
│ │ │ │ ├── drag-and-drop.spec.ts
│ │ │ │ ├── feature.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils.ts
│ │ │ ├── expand-all
│ │ │ │ ├── expand-all.spec.ts
│ │ │ │ ├── feature.ts
│ │ │ │ └── types.ts
│ │ │ ├── hotkeys-core
│ │ │ │ ├── feature.ts
│ │ │ │ └── types.ts
│ │ │ ├── keyboard-drag-and-drop
│ │ │ │ ├── feature.ts
│ │ │ │ ├── keyboard-drag-and-drop.spec.ts
│ │ │ │ └── types.ts
│ │ │ ├── main
│ │ │ │ └── types.ts
│ │ │ ├── prop-memoization
│ │ │ │ ├── feature.ts
│ │ │ │ ├── prop-memoization.spec.ts
│ │ │ │ └── types.ts
│ │ │ ├── renaming
│ │ │ │ ├── feature.ts
│ │ │ │ ├── renaming.spec.ts
│ │ │ │ └── types.ts
│ │ │ ├── search
│ │ │ │ ├── feature.ts
│ │ │ │ ├── search.spec.ts
│ │ │ │ └── types.ts
│ │ │ ├── selection
│ │ │ │ ├── feature.ts
│ │ │ │ ├── selection.spec.ts
│ │ │ │ └── types.ts
│ │ │ ├── sync-data-loader
│ │ │ │ ├── feature.ts
│ │ │ │ └── types.ts
│ │ │ └── tree
│ │ │ │ ├── feature.ts
│ │ │ │ ├── tree.spec.ts
│ │ │ │ └── types.ts
│ │ ├── index.ts
│ │ ├── mddocs-entry.ts
│ │ ├── test-utils
│ │ │ ├── test-tree-do.ts
│ │ │ ├── test-tree-expect.ts
│ │ │ └── test-tree.ts
│ │ ├── types
│ │ │ ├── core.ts
│ │ │ └── deep-merge.ts
│ │ ├── utilities
│ │ │ ├── create-on-drop-handler.ts
│ │ │ ├── errors.ts
│ │ │ ├── insert-items-at-target.ts
│ │ │ └── remove-items-from-parents.ts
│ │ ├── utils.spec.ts
│ │ └── utils.ts
│ ├── tsconfig.json
│ ├── typedoc.json
│ └── vitest.config.ts
├── docs
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── README.md
│ ├── babel.config.js
│ ├── docs
│ │ ├── 0-root
│ │ │ └── getstarted.mdx
│ │ ├── 1-guides
│ │ │ ├── 0-state.mdx
│ │ │ ├── 1-hotkeys.mdx
│ │ │ ├── 2-accessibility.mdx
│ │ │ ├── 3-styling.mdx
│ │ │ ├── 99-rct-migration.mdx
│ │ │ └── _category_.json
│ │ ├── 2-features
│ │ │ ├── 00-overview.mdx
│ │ │ ├── 01-tree.mdx
│ │ │ ├── 02-sync-dataloader.mdx
│ │ │ ├── 03-async-dataloader.mdx
│ │ │ ├── 04-selection.mdx
│ │ │ ├── 05-dnd.mdx
│ │ │ ├── 05-kdnd.mdx
│ │ │ ├── 07-hotkeys.mdx
│ │ │ ├── 08-search.mdx
│ │ │ ├── 09-renaming.mdx
│ │ │ ├── 10-expandall.mdx
│ │ │ ├── 11-prop-memoization.mdx
│ │ │ ├── 99-main.mdx
│ │ │ └── _category_.json
│ │ ├── 3-dnd
│ │ │ ├── 1-overview.mdx
│ │ │ ├── 2-foreign-dnd.mdx
│ │ │ ├── 3-customizability.mdx
│ │ │ ├── 4-behavior.mdx
│ │ │ └── _category_.json
│ │ ├── 4-recipes
│ │ │ ├── 0-external-state-updates.mdx
│ │ │ ├── 1-handling-expensive-components.mdx
│ │ │ ├── 2-virtualization.mdx
│ │ │ ├── 3-proxy-instances.mdx
│ │ │ ├── 4-plugins.mdx
│ │ │ ├── 5-click-behavior.mdx
│ │ │ └── _category_.json
│ │ ├── 5-contributing
│ │ │ ├── 2-tests.mdx
│ │ │ └── _category_.json
│ │ ├── 6-changelog
│ │ │ └── _category_.json
│ │ └── 7-demos
│ │ │ ├── 0-demos.mdx
│ │ │ └── _category_.json
│ ├── docusaurus.config.ts
│ ├── package.json
│ ├── sidebars.ts
│ ├── src
│ │ ├── components
│ │ │ ├── demo
│ │ │ │ ├── demo-box.module.css
│ │ │ │ ├── demo-box.tsx
│ │ │ │ └── use-cleaned-code.ts
│ │ │ ├── docs-page
│ │ │ │ ├── docs-page-header.tsx
│ │ │ │ ├── feature-page-header.tsx
│ │ │ │ ├── link-row.tsx
│ │ │ │ └── styles.module.css
│ │ │ └── home
│ │ │ │ ├── demo-grid.module.css
│ │ │ │ ├── demo-grid.tsx
│ │ │ │ ├── home-notes.module.css
│ │ │ │ └── home-notes.tsx
│ │ ├── css
│ │ │ └── custom.css
│ │ ├── pages
│ │ │ ├── index.module.css
│ │ │ ├── index.tsx
│ │ │ └── markdown-page.md
│ │ └── util
│ │ │ ├── use-all-stories.ts
│ │ │ ├── use-stories-by-tags.ts
│ │ │ ├── use-stories.ts
│ │ │ └── use-story.ts
│ ├── static
│ │ ├── .nojekyll
│ │ └── img
│ │ │ ├── banner-1.png
│ │ │ ├── banner-github.png
│ │ │ ├── docusaurus.png
│ │ │ ├── favicon.ico
│ │ │ ├── ht-dnd-no-reordering.gif
│ │ │ ├── ht-dnd-reparenting.gif
│ │ │ ├── ht-homeend.gif
│ │ │ ├── ht-kdnd-foreign-in.gif
│ │ │ ├── ht-kdnd-foreign-out.gif
│ │ │ ├── ht-kdnd-navigation.gif
│ │ │ ├── ht-keyselect.gif
│ │ │ ├── ht-leftright.gif
│ │ │ ├── ht-rename.gif
│ │ │ ├── ht-search.gif
│ │ │ ├── ht-selections.gif
│ │ │ └── logo.svg
│ ├── storybook-plugin.ts
│ └── tsconfig.json
├── react
│ ├── .babelrc.json
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── assistive-tree-description.tsx
│ │ ├── index.ts
│ │ └── use-tree.tsx
│ ├── tsconfig.json
│ └── typedoc.json
└── sb-react
│ ├── .babelrc.json
│ ├── .storybook
│ ├── main.ts
│ ├── preview.ts
│ └── style.css
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ ├── argtypes.ts
│ ├── async-data-loading.stories.tsx
│ ├── async
│ │ ├── async-get-children-with-data.stories.tsx
│ │ ├── async-loading-state.stories.tsx
│ │ └── async-optimistic-invalidation.stories.tsx
│ ├── dnd
│ │ ├── basic.stories.tsx
│ │ ├── can-drag.stories.tsx
│ │ ├── can-drop.stories.tsx
│ │ ├── cannot-drop-inbetween.stories.tsx
│ │ ├── comprehensive.stories.tsx
│ │ ├── drag-inside.stories.tsx
│ │ ├── drag-line.stories.tsx
│ │ ├── drag-outside.stories.tsx
│ │ ├── kitchensink.stories.tsx
│ │ ├── minimal-dragline-styling.stories.tsx
│ │ ├── on-drop-handler.stories.tsx
│ │ └── visible-assistive-text.stories.tsx
│ ├── expand-all
│ │ ├── async-data.stories.tsx
│ │ └── basic.stories.tsx
│ ├── general
│ │ ├── basic-styling.stories.tsx
│ │ ├── comprehensive-sample.stories.tsx
│ │ ├── example.stories.tsx
│ │ ├── item-data-objects.stories.tsx
│ │ ├── recursive-datastructure.stories.tsx
│ │ └── simple.stories.tsx
│ ├── guides
│ │ ├── click-behavior
│ │ │ ├── expand-on-arrow-click.css
│ │ │ ├── expand-on-arrow-click.stories.tsx
│ │ │ └── expand-on-double-click.stories.tsx
│ │ ├── multiple-trees-advanced.stories.tsx
│ │ ├── multiple-trees.stories.tsx
│ │ ├── overwriting-internals.stories.tsx
│ │ └── render-performance
│ │ │ ├── memoized-slow-item-renderers.stories.tsx
│ │ │ └── slow-item-renderers.stories.tsx
│ ├── hotkeys
│ │ ├── custom-hotkeys.stories.tsx
│ │ ├── overwriting-hotkeys.stories.tsx
│ │ └── visible-hotkeys.stories.tsx
│ ├── plugins
│ │ ├── simple-plugin.stories.tsx
│ │ └── transform-props.stories.tsx
│ ├── renaming
│ │ ├── basic.stories.tsx
│ │ └── can-rename.stories.tsx
│ ├── scalability
│ │ ├── many-features.stories.tsx
│ │ ├── scalability.stories.tsx
│ │ ├── virtualization-dynamic-height.stories.tsx
│ │ └── virtualization.stories.tsx
│ ├── search
│ │ ├── async.stories.tsx
│ │ ├── basic.stories.tsx
│ │ ├── custom-matcher.stories.tsx
│ │ └── scroll-behaviour.stories.tsx
│ ├── state
│ │ ├── distinct-state-handlers.stories.tsx
│ │ ├── external-state.stories.tsx
│ │ └── internal-state.stories.tsx
│ └── utils
│ │ ├── data.ts
│ │ ├── hotkey-debugger.stories.tsx
│ │ ├── unit-test-async.stories.tsx
│ │ └── unit-test-sync.stories.tsx
│ └── tsconfig.json
├── readme.md
├── scripts
├── examples-data-template.ts.tpl
├── generate-llmtxt.mjs
├── prepare.mjs
└── version.mjs
├── tsconfig.json
├── tsconfig.lint.json
├── typedoc.base.json
├── typedoc.json
└── yarn.lock
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [
6 | ["@headless-tree/core", "@headless-tree/react"]
7 | ],
8 | "linked": [],
9 | "access": "public",
10 | "baseBranch": "main",
11 | "updateInternalDependencies": "patch",
12 | "ignore": []
13 | }
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: lukasbach
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior. Ideally this includes a sandbox link that reproduces the issue. There
14 | are some starter sandboxes provided for Headless Tree for Codesandbox or Stackblitz, that you can use
15 | to create a reproduction: https://headless-tree.lukasbach.com/examples
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem. A gif or screencast can also help understand your problem.
22 |
23 | **Additional context**
24 | You can help by providing additional details that are available to you, such as
25 |
26 | - Device if mobile
27 | - Operating System, Browser
28 | - Version of the Library or tool for which you report the bug
29 |
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Discord Chat
4 | url: https://discord.gg/KuZ6EezzVw
5 | about: An official discord server to discuss issues and features
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: lukasbach
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: 'npm'
9 | directory: '/'
10 | open-pull-requests-limit: 5
11 | schedule:
12 | interval: 'daily'
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: read
10 | pages: write
11 | deployments: write
12 | id-token: write
13 |
14 | jobs:
15 | docs:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - uses: actions/cache@v3
20 | with:
21 | path: |
22 | **/node_modules
23 | ./.yarn/cache
24 | key: ${{ runner.os }}-deps-${{ hashFiles('**/yarn.lock') }}
25 | - uses: volta-cli/action@v4.1.1
26 | - run: yarn
27 | - run: yarn lint:test
28 | - run: yarn build:core
29 | - run: yarn build:sb
30 | - run: yarn llmtxt
31 | - run: yarn build:web
32 | - run: |
33 | mkdir -p ./packages/docs/build/storybook
34 | cp -r ./packages/sb-react/storybook-static ./packages/docs/build/storybook/react
35 | - uses: actions/upload-pages-artifact@v3
36 | with:
37 | path: ./packages/docs/build
38 | - uses: actions/deploy-pages@v4
39 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | branches:
5 | - main
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | publish:
14 | runs-on: ubuntu-latest
15 | name: Publish
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | persist-credentials: false
20 | fetch-depth: 0
21 |
22 | - uses: actions/cache@v3
23 | with:
24 | path: |
25 | **/node_modules
26 | ./.yarn/cache
27 | key: ${{ runner.os }}-deps-${{ hashFiles('**/yarn.lock') }}
28 | - uses: volta-cli/action@v4.1.1
29 | with:
30 | registry-url: "https://registry.npmjs.org"
31 |
32 | - run: yarn
33 | - run: yarn lint:test
34 | - run: yarn test
35 | - run: yarn build:core
36 | - run: yarn build:web
37 | - name: Copy readme.md to packages
38 | run: |
39 | for dir in $(find packages -type d -maxdepth 1); do
40 | cp readme.md $dir/readme.md
41 | done
42 |
43 | - name: Create Release Pull Request or Publish to npm
44 | id: changesets
45 | uses: changesets/action@v1
46 | with:
47 | version: npx zx ./scripts/version.mjs
48 | publish: yarn changeset publish
49 | commit: "chore: release"
50 | env:
51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
53 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/snapshot.yml:
--------------------------------------------------------------------------------
1 | name: Snapshot Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 | snapshot:
14 | runs-on: ubuntu-latest
15 | name: Publish
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | persist-credentials: false
20 | fetch-depth: 0
21 |
22 | - uses: actions/cache@v3
23 | with:
24 | path: |
25 | **/node_modules
26 | ./.yarn/cache
27 | key: ${{ runner.os }}-deps-${{ hashFiles('**/yarn.lock') }}
28 | - uses: volta-cli/action@v4.1.1
29 | with:
30 | registry-url: "https://registry.npmjs.org"
31 |
32 | - run: yarn
33 | - run: yarn lint:test
34 | - run: yarn test
35 | - run: yarn build:core
36 | - run: yarn build:web
37 |
38 | - name: Snapshot Versioning
39 | run: yarn changeset version --snapshot
40 |
41 | - name: Snapshot Release
42 | run: |
43 | if git diff --quiet; then
44 | echo "No changes detected"
45 | exit 0
46 | else
47 | yarn changeset publish --no-git-tag --tag snapshot
48 | fi
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
51 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
52 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
53 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yml:
--------------------------------------------------------------------------------
1 | name: Verify
2 | on:
3 | push:
4 | pull_request:
5 | workflow_dispatch:
6 |
7 | jobs:
8 | verify:
9 | runs-on: ubuntu-latest
10 | name: Verify
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/cache@v3
14 | with:
15 | path: |
16 | **/node_modules
17 | ./.yarn/cache
18 | key: ${{ runner.os }}-deps-${{ hashFiles('**/yarn.lock') }}
19 | - uses: volta-cli/action@v4.1.1
20 | - run: yarn
21 | - run: yarn lint:test
22 | - run: yarn test
23 | - run: yarn build:core
24 | - run: yarn build:web
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .pnp.*
2 | .yarn/*
3 | !.yarn/patches
4 | !.yarn/plugins
5 | !.yarn/releases
6 | !.yarn/sdks
7 | !.yarn/versions
8 | node_modules
9 |
10 | .nx
11 |
12 | .idea
13 | /docs
14 | lib
15 |
16 | tsconfig.tsbuildinfo
17 |
18 | storybook-static
19 | /packages/docs/apidocs
20 | /packages/docs/.cache
21 | /packages/docs/public
22 | /packages/docs/docs/md-test
23 | /packages/docs/docs/0-root/examples.mdx
24 | /examples/*/dist
25 | /packages/*/readme.md
26 | /packages/readme.md
27 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | // registry.npmjs.org/:_authToken=${NPM_TOKEN}
2 | registry-url=https://registry.npmjs.org/
3 | always-auth=true
4 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | compressionLevel: mixed
2 |
3 | enableGlobalCache: false
4 |
5 | nodeLinker: node-modules
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | To run locally, you need to install NodeJS and Yarn. Locally installing
4 | Volta is recommended, since it will make sure to use the correct versions
5 | of NodeJS and Yarn.
6 |
7 | To develop locally, run ``yarn`` to install dependencies, then
8 |
9 | - ``yarn start`` to build the library in watch mode and start the storybook instance
10 | - ``yarn start:docs`` to work on the documentation page. You might need to run
11 | ``yarn build`` first. If you just want to edit documentation pages, you can also
12 | just directly edit the markdown files.
13 | - ``yarn verify`` to run the linter, tests and build the library
14 |
15 | When proposing a new change, please document the changes and whether
16 | they are breaking or not by running ``yarn changeset`` prior to committing.
17 |
18 | Before proposing your changes, please check if you those of the following steps
19 | which make sense in the scope of your change have been completed:
20 |
21 | - Changes to the usage of the library are documented in the Docs Pages
22 | - Potential visual features are reproducible in a Storybook story
23 | - Unit Tests verifying the changes have been implemented
24 | - Linter and Unit Tests run successfully
25 |
26 | Feel free to reach out at any time during your contribution process if you
27 | have any questions or need help. You can also collaborate in our Discord
28 | community to get help or discuss your ideas with other contributors:
29 | - https://discord.gg/WN85eMY3
30 |
31 | There are some more detailed documentations available on contributing implementation
32 | changes to Headless Tree at the HT website:
33 | - https://headless-tree.lukasbach.com/contributing/overview
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Lukas Bach
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@headless-tree/example-basic",
3 | "description": "Basic example of Headless Tree with React",
4 | "private": true,
5 | "version": "0.0.0",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "start": "vite",
10 | "build": "tsc -b && vite build"
11 | },
12 | "dependencies": {
13 | "@headless-tree/core": "^1.0.0",
14 | "@headless-tree/react": "^1.0.0",
15 | "classnames": "^2.3.2",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^19.0.10",
21 | "@types/react-dom": "^19.0.4",
22 | "@vitejs/plugin-react": "^4.3.4",
23 | "globals": "^16.0.0",
24 | "typescript": "~5.7.2",
25 | "vite": "^6.3.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/basic/readme.MD:
--------------------------------------------------------------------------------
1 | # Basic example of Headless Tree with React
2 |
3 | To run this example:
4 |
5 | - `npm install` to install dependencies
6 | - `npm start` to start the dev server
7 |
8 | You can run this sample from [Stackblitz](https://stackblitz.com/github/lukasbach/headless-tree/tree/main/examples/basic?preset=node&file=src/main.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/lukasbach/headless-tree/tree/main/examples/basic?file=src/main.tsx). The source code is available on [GitHub](https://github.com/lukasbach/headless-tree/tree/main/examples/basic).
9 |
10 |
--------------------------------------------------------------------------------
/examples/basic/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import "./style.css";
4 | import {
5 | asyncDataLoaderFeature,
6 | createOnDropHandler,
7 | dragAndDropFeature,
8 | hotkeysCoreFeature,
9 | keyboardDragAndDropFeature,
10 | selectionFeature,
11 | } from "@headless-tree/core";
12 | import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
13 | import cn from "classnames";
14 | import { DemoItem, asyncDataLoader, data } from "./data";
15 |
16 | export const Tree = () => {
17 | const tree = useTree({
18 | initialState: {
19 | expandedItems: ["fruit"],
20 | selectedItems: ["banana", "orange"],
21 | },
22 | rootItemId: "root",
23 | getItemName: (item) => item.getItemData()?.name,
24 | isItemFolder: (item) => !!item.getItemData()?.children,
25 | canReorder: true,
26 | onDrop: createOnDropHandler((item, newChildren) => {
27 | data[item.getId()].children = newChildren;
28 | }),
29 | indent: 20,
30 | dataLoader: asyncDataLoader,
31 | features: [
32 | asyncDataLoaderFeature,
33 | selectionFeature,
34 | hotkeysCoreFeature,
35 | dragAndDropFeature,
36 | keyboardDragAndDropFeature,
37 | ],
38 | });
39 |
40 | return (
41 |
42 |
43 | {tree.getItems().map((item) => (
44 |
61 | ))}
62 |
63 |
64 | );
65 | };
66 |
67 | createRoot(document.getElementById("root")!).render(
68 |
69 |
70 |
71 |
72 | ,
73 | );
74 |
--------------------------------------------------------------------------------
/examples/basic/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module "*.css";
3 |
--------------------------------------------------------------------------------
/examples/basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "isolatedModules": true,
11 | "moduleDetection": "force",
12 | "noEmit": true,
13 | "jsx": "react-jsx",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noUncheckedSideEffectImports": true
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/basic/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | });
7 |
--------------------------------------------------------------------------------
/examples/comprehensive/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/comprehensive/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@headless-tree/example-comprehensive",
3 | "description": "Integration of Headless Tree with React with most HT features enabled",
4 | "private": true,
5 | "version": "0.0.0",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "start": "vite",
10 | "build": "tsc -b && vite build"
11 | },
12 | "dependencies": {
13 | "@headless-tree/core": "^1.0.0",
14 | "@headless-tree/react": "^1.0.0",
15 | "classnames": "^2.3.2",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^19.0.10",
21 | "@types/react-dom": "^19.0.4",
22 | "@vitejs/plugin-react": "^4.3.4",
23 | "globals": "^16.0.0",
24 | "typescript": "~5.7.2",
25 | "vite": "^6.3.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/comprehensive/readme.MD:
--------------------------------------------------------------------------------
1 | # Integration of Headless Tree with React with most HT features enabled
2 |
3 | To run this example:
4 |
5 | - `npm install` to install dependencies
6 | - `npm start` to start the dev server
7 |
8 | You can run this sample from [Stackblitz](https://stackblitz.com/github/lukasbach/headless-tree/tree/main/examples/comprehensive?preset=node&file=src/main.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/lukasbach/headless-tree/tree/main/examples/comprehensive?file=src/main.tsx). The source code is available on [GitHub](https://github.com/lukasbach/headless-tree/tree/main/examples/comprehensive).
9 |
10 |
--------------------------------------------------------------------------------
/examples/comprehensive/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module "*.css";
3 |
--------------------------------------------------------------------------------
/examples/comprehensive/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "isolatedModules": true,
11 | "moduleDetection": "force",
12 | "noEmit": true,
13 | "jsx": "react-jsx",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noUncheckedSideEffectImports": true
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/comprehensive/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | plugins: [react()],
6 | });
7 |
--------------------------------------------------------------------------------
/examples/react-compiler/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/examples/react-compiler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@headless-tree/example-react-compiler",
3 | "description": "Headless Tree with React Compiler enabled",
4 | "private": true,
5 | "version": "0.0.0",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "start": "vite",
10 | "build": "tsc -b && vite build"
11 | },
12 | "dependencies": {
13 | "@headless-tree/core": "^1.0.0",
14 | "@headless-tree/react": "^1.0.0",
15 | "classnames": "^2.3.2",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^19.0.10",
21 | "@types/react-dom": "^19.0.4",
22 | "@vitejs/plugin-react": "^4.3.4",
23 | "babel-plugin-react-compiler": "^19.1.0-rc.1",
24 | "globals": "^16.0.0",
25 | "typescript": "~5.7.2",
26 | "vite": "^6.3.1"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/react-compiler/readme.MD:
--------------------------------------------------------------------------------
1 | # Headless Tree with React Compiler enabled
2 |
3 | To run this example:
4 |
5 | - `npm install` to install dependencies
6 | - `npm start` to start the dev server
7 |
8 | You can run this sample from [Stackblitz](https://stackblitz.com/github/lukasbach/headless-tree/tree/main/examples/react-compiler?preset=node&file=src/main.tsx) or [CodeSandbox](https://codesandbox.io/p/devbox/github/lukasbach/headless-tree/tree/main/examples/react-compiler?file=src/main.tsx). The source code is available on [GitHub](https://github.com/lukasbach/headless-tree/tree/main/examples/react-compiler).
9 |
10 |
--------------------------------------------------------------------------------
/examples/react-compiler/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module "*.css";
3 |
--------------------------------------------------------------------------------
/examples/react-compiler/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "isolatedModules": true,
11 | "moduleDetection": "force",
12 | "noEmit": true,
13 | "jsx": "react-jsx",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "noUncheckedSideEffectImports": true
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/examples/react-compiler/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | plugins: [
6 | react({
7 | babel: {
8 | plugins: [
9 | ["babel-plugin-react-compiler", {
10 | target: "19"
11 | }],
12 | ],
13 | },
14 | }),
15 | ],
16 | });
17 |
--------------------------------------------------------------------------------
/homepagedata.json:
--------------------------------------------------------------------------------
1 | {
2 | "repo": "headless-tree",
3 | "title": "Headless Tree",
4 | "category": "library",
5 | "created_at": "2025-04-15T00:00:00.000Z",
6 | "highlight": true,
7 | "docs": "https://headless-tree.lukasbach.com",
8 | "npm": "@headless-tree/core @headless-tree/react"
9 | }
10 |
--------------------------------------------------------------------------------
/ideas.md:
--------------------------------------------------------------------------------
1 | - Auto-open folders when dragging onto them for 1 second
2 | - Drag item to bottom most location shows dragline at 0
3 | - Dragging items out of the tree (or otherwise removing them), they remain selected. Now ctrl+selecting a new item and dragging that out as well will throw an error (in comprehensive demo)
4 | - Styling guide
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json",
3 | "version": "0.0.0"
4 | }
5 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasksRunnerOptions": {
3 | "default": {
4 | "runner": "nx-cloud",
5 | "options": {
6 | "cacheableOperations": [
7 | "build:cjs",
8 | "build:esm",
9 | "build:docs",
10 | "typecheck",
11 | "build:sb",
12 | "lint:test"
13 | ],
14 | "accessToken": "YmU4OTg0NzYtZTIzYy00ZTdmLWJhM2ItMGRmYTUyZGE0YzJkfHJlYWQtb25seQ=="
15 | }
16 | }
17 | },
18 | "targetDefaults": {
19 | "build:cjs": {
20 | "outputs": [
21 | "{projectRoot}/lib/cjs"
22 | ]
23 | },
24 | "build:esm": {
25 | "outputs": [
26 | "{projectRoot}/lib/esm"
27 | ]
28 | },
29 | "build:docs": {
30 | "outputs": [
31 | "{projectRoot}/public",
32 | "{projectRoot}/build",
33 | "{projectRoot}/.docusaurus"
34 | ]
35 | },
36 | "build:sb": {
37 | "outputs": [
38 | "{projectRoot}/storybook-static"
39 | ]
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "root",
3 | "private": true,
4 | "workspaces": [
5 | "packages/*",
6 | "examples/*"
7 | ],
8 | "scripts": {
9 | "lint": "eslint . --fix",
10 | "lint:test": "eslint .",
11 | "start": "lerna run start,start:sb --parallel --scope @headless-tree/core --scope @headless-tree/sb-react",
12 | "start:docs": "lerna run start --parallel --scope @headless-tree/docs",
13 | "build": "lerna run build,build:cjs,build:esm,build:docs,build:sb --concurrency 1",
14 | "build:core": "lerna run build,build:cjs,build:esm --concurrency 1",
15 | "build:web": "lerna run build:docs,build:sb --concurrency 1",
16 | "test": "lerna run test --stream",
17 | "postinstall": "zx scripts/prepare.mjs",
18 | "llmtxt": "zx scripts/generate-llmtxt.mjs",
19 | "verify": "yarn lint && lerna run build,build:esm,test --stream"
20 | },
21 | "dependencies": {
22 | "@changesets/cli": "^2.27.5",
23 | "@lukasbach/eslint-config-deps": "^1.0.7",
24 | "@types/node": "22.10.7",
25 | "@types/react": "18.2.66",
26 | "@types/react-dom": "18.2.22",
27 | "eslint": "^8.57.0",
28 | "front-matter": "^4.0.2",
29 | "lerna": "^8.1.3",
30 | "typedoc": "^0.25.13",
31 | "typedoc-plugin-markdown": "^3.17.1",
32 | "zx": "^8.3.2"
33 | },
34 | "volta": {
35 | "node": "23.6.1",
36 | "yarn": "4.1.1"
37 | },
38 | "eslintConfig": {
39 | "extends": "@lukasbach/base/react",
40 | "parserOptions": {
41 | "project": "./tsconfig.lint.json"
42 | },
43 | "rules": {
44 | "no-param-reassign": "off",
45 | "no-nested-ternary": "warn",
46 | "@typescript-eslint/no-shadow": "off",
47 | "@typescript-eslint/no-unused-vars": "warn",
48 | "no-empty-pattern": "off",
49 | "react/button-has-type": "off",
50 | "react/no-unescaped-entities": "warn",
51 | "no-alert": "off",
52 | "react/destructuring-assignment": "off"
53 | },
54 | "ignorePatterns": [
55 | "lib",
56 | "*.js"
57 | ]
58 | },
59 | "devDependencies": {
60 | "@nrwl/nx-cloud": "latest"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/core/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @headless-tree/core
2 |
3 | ## 1.1.0
4 |
5 | ### Minor Changes
6 |
7 | - 64d8e2a: add getChildrenWithData method to data loader to support fetching all children of an item at once
8 | - 35260e3: fixed hotkey issues where releasing modifier keys (like shift) before normal keys can cause issues with subsequent keydown events
9 |
10 | ### Patch Changes
11 |
12 | - 29b2c64: improved key-handling behavior for hotkeys while input elements are focused (#98)
13 | - da1e757: fixed a bug where alt-tabbing out of browser will break hotkeys feature
14 | - c283f52: add feature to allow async data invalidation without triggering rerenders with `invalidateItemData(optimistic: true)` (#95)
15 | - 29b2c64: added option to completely ignore hotkey events while input elements are focused (`ignoreHotkeysOnInput`) (#98)
16 | - cd5b27c: add position:absolute to default styles of getDragLineStyle()
17 |
18 | ## 1.0.1
19 |
20 | ### Patch Changes
21 |
22 | - c9f9932: fixed tree.focusNextItem() and tree.focusPreviousItem() throwing if no item is currently focused
23 | - 6ed84b4: recursive item references are filtered out when rendering (#89)
24 | - 4bef2f2: fixed a bug where hotkeys involving shift may not work properly depending on the order of shift and other key inputs (#98)
25 |
26 | ## 1.0.0
27 |
28 | ### Minor Changes
29 |
30 | - 9e5027b: The propMemoization feature now memoizes all prop-generation related functions, including searchinput and renameinput related props
31 |
32 | ## 0.0.15
33 |
34 | ### Patch Changes
35 |
36 | - 2af5668: Bug fix: Mutations to expanded tree items from outside will now trigger a rebuild of the tree structure (#65)
37 | - 617faea: Support for keyboard-controlled drag-and-drop events
38 |
39 | ## 0.0.14
40 |
41 | ### Patch Changes
42 |
43 | - 7e702fb: dev release
44 |
45 | ## 0.0.13
46 |
47 | ### Patch Changes
48 |
49 | - fdaefbc: dev release
50 |
51 | ## 0.0.12
52 |
53 | ### Patch Changes
54 |
55 | - 7236907: dev release
56 |
57 | ## 0.0.11
58 |
59 | ### Patch Changes
60 |
61 | - 7ed33ac: dev release
62 |
63 | ## 0.0.10
64 |
65 | ### Patch Changes
66 |
67 | - 520ec27: test release
68 |
69 | ## 0.0.9
70 |
71 | ### Patch Changes
72 |
73 | - ab2a124: dev release
74 |
75 | ## 0.0.8
76 |
77 | ### Patch Changes
78 |
79 | - 076dfc5: dev release
80 |
81 | ## 0.0.7
82 |
83 | ### Patch Changes
84 |
85 | - 6ec53b3: dev release
86 |
87 | ## 0.0.6
88 |
89 | ### Patch Changes
90 |
91 | - bc9c446: dev release
92 |
93 | ## 0.0.5
94 |
95 | ### Patch Changes
96 |
97 | - 751682a: dev release
98 |
99 | ## 0.0.4
100 |
101 | ### Patch Changes
102 |
103 | - dc6d813: tag pushing
104 |
105 | ## 0.0.3
106 |
107 | ### Patch Changes
108 |
109 | - 6460368: tree shaking
110 |
111 | ## 0.0.2
112 |
113 | ### Patch Changes
114 |
115 | - 05e24de: release test
116 |
--------------------------------------------------------------------------------
/packages/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@headless-tree/core",
3 | "version": "1.1.0",
4 | "type": "module",
5 | "main": "lib/cjs/index.js",
6 | "module": "lib/esm/index.js",
7 | "types": "lib/esm/index.d.ts",
8 | "sideEffects": false,
9 | "scripts": {
10 | "build:cjs": "tsc -m commonjs --outDir lib/cjs",
11 | "build:esm": "tsc",
12 | "start": "tsc -w",
13 | "test": "vitest run"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git@github.com:lukasbach/headless-tree.git",
18 | "directory": "packages/core"
19 | },
20 | "author": "Lukas Bach ",
21 | "license": "MIT",
22 | "devDependencies": {
23 | "jsdom": "^26.0.0",
24 | "typescript": "^5.7.2",
25 | "vitest": "^3.0.3"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/core/src/core/build-static-instance.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-continue,no-labels,no-extra-label */
2 |
3 | import { InstanceBuilder } from "../features/main/types";
4 |
5 | export const buildStaticInstance: InstanceBuilder = (
6 | features,
7 | instanceType,
8 | buildOpts,
9 | ) => {
10 | const instance: any = {};
11 | const finalize = () => {
12 | const opts = buildOpts(instance);
13 | featureLoop: for (let i = 0; i < features.length; i++) {
14 | // Loop goes in forward order, each features overwrite previous ones and wraps those in a prev() fn
15 | const definition = features[i][instanceType];
16 | if (!definition) continue featureLoop;
17 | methodLoop: for (const [key, method] of Object.entries(definition)) {
18 | if (!method) continue methodLoop;
19 | const prev = instance[key];
20 | instance[key] = (...args: any[]) => {
21 | return method({ ...opts, prev }, ...args);
22 | };
23 | }
24 | }
25 | };
26 | return [instance as any, finalize];
27 | };
28 |
--------------------------------------------------------------------------------
/packages/core/src/features/async-data-loader/types.ts:
--------------------------------------------------------------------------------
1 | import { SetStateFn } from "../../types/core";
2 | import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
3 |
4 | export interface AsyncDataLoaderDataRef {
5 | itemData: Record;
6 | childrenIds: Record;
7 | }
8 |
9 | /**
10 | * @category Async Data Loader/General
11 | * */
12 | export type AsyncDataLoaderFeatureDef = {
13 | state: {
14 | loadingItemData: string[];
15 | loadingItemChildrens: string[];
16 | };
17 | config: {
18 | rootItemId: string;
19 |
20 | /** Will be called when HT retrieves item data for an item whose item data is asynchronously being loaded.
21 | * Can be used to create placeholder data to use for rendering the tree item while it is loaded. If not defined,
22 | * the tree item data will be null. */
23 | createLoadingItemData?: () => T;
24 |
25 | setLoadingItemData?: SetStateFn;
26 | setLoadingItemChildrens?: SetStateFn;
27 | onLoadedItem?: (itemId: string, item: T) => void;
28 | onLoadedChildren?: (itemId: string, childrenIds: string[]) => void;
29 | };
30 | treeInstance: SyncDataLoaderFeatureDef["treeInstance"] & {
31 | /** @deprecated use loadItemData instead */
32 | waitForItemDataLoaded: (itemId: string) => Promise;
33 | /** @deprecated use loadChildrenIds instead */
34 | waitForItemChildrenLoaded: (itemId: string) => Promise;
35 | loadItemData: (itemId: string) => Promise;
36 | loadChildrenIds: (itemId: string) => Promise;
37 | };
38 | itemInstance: SyncDataLoaderFeatureDef["itemInstance"] & {
39 | /** Invalidate fetched data for item, and triggers a refetch and subsequent rerender if the item is visible
40 | * @param optimistic If true, the item will not trigger a state update on `loadingItemData`, and
41 | * the tree will continue to display the old data until the new data has loaded. */
42 | invalidateItemData: (optimistic?: boolean) => Promise;
43 |
44 | /** Invalidate fetched children ids for item, and triggers a refetch and subsequent rerender if the item is visible
45 | * @param optimistic If true, the item will not trigger a state update on `loadingItemChildrens`, and
46 | * the tree will continue to display the old data until the new data has loaded. */
47 | invalidateChildrenIds: (optimistic?: boolean) => Promise;
48 |
49 | updateCachedChildrenIds: (childrenIds: string[]) => void;
50 | isLoading: () => boolean;
51 | };
52 | hotkeys: SyncDataLoaderFeatureDef["hotkeys"];
53 | };
54 |
--------------------------------------------------------------------------------
/packages/core/src/features/drag-and-drop/types.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance, SetStateFn } from "../../types/core";
2 |
3 | export interface DndDataRef {
4 | lastDragCode?: string;
5 | lastAllowDrop?: boolean;
6 | }
7 |
8 | export interface DndState {
9 | draggedItems?: ItemInstance[];
10 | draggingOverItem?: ItemInstance;
11 | dragTarget?: DragTarget;
12 | }
13 |
14 | export interface DragLineData {
15 | indent: number;
16 | top: number;
17 | left: number;
18 | width: number;
19 | }
20 |
21 | export type DragTarget =
22 | | {
23 | item: ItemInstance;
24 | childIndex: number;
25 | insertionIndex: number;
26 | dragLineIndex: number;
27 | dragLineLevel: number;
28 | }
29 | | {
30 | item: ItemInstance;
31 | };
32 |
33 | export enum DragTargetPosition {
34 | Top = "top",
35 | Bottom = "bottom",
36 | Item = "item",
37 | }
38 |
39 | export type DragAndDropFeatureDef = {
40 | state: {
41 | dnd?: DndState | null;
42 | };
43 | config: {
44 | setDndState?: SetStateFn | undefined | null>;
45 |
46 | /** Defines the size of the area at the top and bottom of an item where, when an item is dropped, the item willö
47 | * be placed above or below the item within the same parent, as opposed to being placed inside the item.
48 | * If `canReorder` is `false`, this is ignored. */
49 | reorderAreaPercentage?: number;
50 | canReorder?: boolean;
51 |
52 | canDrag?: (items: ItemInstance[]) => boolean;
53 | canDrop?: (items: ItemInstance[], target: DragTarget) => boolean;
54 |
55 | indent?: number;
56 |
57 | createForeignDragObject?: (items: ItemInstance[]) => {
58 | format: string;
59 | data: any;
60 | };
61 | canDropForeignDragObject?: (
62 | dataTransfer: DataTransfer,
63 | target: DragTarget,
64 | ) => boolean;
65 | onDrop?: (
66 | items: ItemInstance[],
67 | target: DragTarget,
68 | ) => void | Promise;
69 | onDropForeignDragObject?: (
70 | dataTransfer: DataTransfer,
71 | target: DragTarget,
72 | ) => void | Promise;
73 | onCompleteForeignDrop?: (items: ItemInstance[]) => void;
74 | };
75 | treeInstance: {
76 | getDragTarget: () => DragTarget | null;
77 | getDragLineData: () => DragLineData | null;
78 |
79 | getDragLineStyle: (
80 | topOffset?: number,
81 | leftOffset?: number,
82 | ) => Record;
83 | };
84 | itemInstance: {
85 | isDragTarget: () => boolean;
86 | isDragTargetAbove: () => boolean;
87 | isDragTargetBelow: () => boolean;
88 | isDraggingOver: () => boolean;
89 | };
90 | hotkeys: never;
91 | };
92 |
--------------------------------------------------------------------------------
/packages/core/src/features/expand-all/expand-all.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it } from "vitest";
2 | import { TestTree } from "../../test-utils/test-tree";
3 | import { expandAllFeature } from "./feature";
4 | import { propMemoizationFeature } from "../prop-memoization/feature";
5 |
6 | const factory = TestTree.default({}).withFeatures(
7 | expandAllFeature,
8 | propMemoizationFeature,
9 | );
10 |
11 | describe("core-feature/expand-all", () => {
12 | factory.forSuits((tree) => {
13 | describe("tree instance calls", () => {
14 | it("expands all", async () => {
15 | const expandPromise = tree.instance.expandAll();
16 | await tree.resolveAsyncVisibleItems();
17 | await expandPromise;
18 | tree.expect.foldersExpanded("x12", "x13", "x14", "x4", "x41", "x44");
19 | });
20 |
21 | it("collapses all", () => {
22 | tree.instance.collapseAll();
23 | tree.expect.foldersCollapsed("x1", "x2", "x3", "x4");
24 | });
25 |
26 | it("cancels expanding all", async () => {
27 | const token = { current: true };
28 | const expandPromise = tree.instance.expandAll(token);
29 | token.current = false;
30 | await tree.resolveAsyncVisibleItems();
31 | await expandPromise;
32 | tree.expect.foldersCollapsed("x2", "x3", "x4");
33 | });
34 | });
35 |
36 | describe("item instance calls", () => {
37 | it("expands all", async () => {
38 | const expandPromise = Promise.all([
39 | // not sure why all are needed...
40 | tree.instance.getItemInstance("x1").expandAll(),
41 | tree.instance.getItemInstance("x2").expandAll(),
42 | tree.instance.getItemInstance("x3").expandAll(),
43 | tree.instance.getItemInstance("x4").expandAll(),
44 | ]);
45 | await tree.resolveAsyncVisibleItems();
46 | await expandPromise;
47 | tree.expect.foldersExpanded("x2", "x21", "x24");
48 | });
49 |
50 | it("collapses all", () => {
51 | tree.instance.collapseAll();
52 | tree.expect.foldersCollapsed("x1", "x2", "x3", "x4");
53 | });
54 | });
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/packages/core/src/features/expand-all/feature.ts:
--------------------------------------------------------------------------------
1 | import { FeatureImplementation } from "../../types/core";
2 |
3 | export const expandAllFeature: FeatureImplementation = {
4 | key: "expand-all",
5 |
6 | treeInstance: {
7 | expandAll: async ({ tree }, cancelToken) => {
8 | await Promise.all(
9 | tree.getItems().map((item) => item.expandAll(cancelToken)),
10 | );
11 | },
12 |
13 | collapseAll: ({ tree }) => {
14 | tree.applySubStateUpdate("expandedItems", []);
15 | tree.rebuildTree();
16 | },
17 | },
18 |
19 | itemInstance: {
20 | expandAll: async ({ tree, item }, cancelToken) => {
21 | if (cancelToken?.current) {
22 | return;
23 | }
24 | if (!item.isFolder()) {
25 | return;
26 | }
27 |
28 | item.expand();
29 | await tree.waitForItemChildrenLoaded(item.getId());
30 | await Promise.all(
31 | item.getChildren().map(async (child) => {
32 | await tree.waitForItemChildrenLoaded(item.getId());
33 | await child?.expandAll(cancelToken);
34 | }),
35 | );
36 | },
37 |
38 | collapseAll: ({ item }) => {
39 | if (!item.isExpanded()) return;
40 | for (const child of item.getChildren()) {
41 | child?.collapseAll();
42 | }
43 | item.collapse();
44 | },
45 | },
46 |
47 | hotkeys: {
48 | expandSelected: {
49 | hotkey: "Control+Shift+Plus",
50 | handler: async (_, tree) => {
51 | const cancelToken = { current: false };
52 | const cancelHandler = (e: KeyboardEvent) => {
53 | if (e.code === "Escape") {
54 | cancelToken.current = true;
55 | }
56 | };
57 | document.addEventListener("keydown", cancelHandler);
58 | await Promise.all(
59 | tree.getSelectedItems().map((item) => item.expandAll(cancelToken)),
60 | );
61 | document.removeEventListener("keydown", cancelHandler);
62 | },
63 | },
64 |
65 | collapseSelected: {
66 | hotkey: "Control+Shift+Minus",
67 | handler: (_, tree) => {
68 | tree.getSelectedItems().forEach((item) => item.collapseAll());
69 | },
70 | },
71 | },
72 | };
73 |
--------------------------------------------------------------------------------
/packages/core/src/features/expand-all/types.ts:
--------------------------------------------------------------------------------
1 | export interface ExpandAllDataRef {}
2 |
3 | export type ExpandAllFeatureDef = {
4 | state: {};
5 | config: {};
6 | treeInstance: {
7 | expandAll: (cancelToken?: { current: boolean }) => Promise;
8 | collapseAll: () => void;
9 | };
10 | itemInstance: {
11 | expandAll: (cancelToken?: { current: boolean }) => Promise;
12 | collapseAll: () => void;
13 | };
14 | hotkeys: "expandSelected" | "collapseSelected";
15 | };
16 |
--------------------------------------------------------------------------------
/packages/core/src/features/hotkeys-core/types.ts:
--------------------------------------------------------------------------------
1 | import { CustomHotkeysConfig, TreeInstance } from "../../types/core";
2 |
3 | export interface HotkeyConfig {
4 | hotkey: string;
5 | canRepeat?: boolean;
6 | allowWhenInputFocused?: boolean;
7 | isEnabled?: (tree: TreeInstance) => boolean;
8 | preventDefault?: boolean;
9 | handler: (e: KeyboardEvent, tree: TreeInstance) => void;
10 | }
11 |
12 | export interface HotkeysCoreDataRef {
13 | keydownHandler?: (e: KeyboardEvent) => void;
14 | keyupHandler?: (e: KeyboardEvent) => void;
15 | resetHandler?: (e: FocusEvent) => void;
16 | pressedKeys: Set;
17 | }
18 |
19 | export type HotkeysCoreFeatureDef = {
20 | state: {};
21 | config: {
22 | hotkeys?: CustomHotkeysConfig;
23 | onTreeHotkey?: (name: string, e: KeyboardEvent) => void;
24 |
25 | /** Do not handle key inputs while an HTML input element is focused */
26 | ignoreHotkeysOnInputs?: boolean;
27 | };
28 | treeInstance: {};
29 | itemInstance: {};
30 | hotkeys: never;
31 | };
32 |
--------------------------------------------------------------------------------
/packages/core/src/features/keyboard-drag-and-drop/types.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance, SetStateFn } from "../../types/core";
2 |
3 | export interface KDndDataRef {
4 | kDndDataTransfer: DataTransfer | undefined;
5 | }
6 |
7 | export enum AssistiveDndState {
8 | None,
9 | Started,
10 | Dragging,
11 | Completed,
12 | Aborted,
13 | }
14 |
15 | export type KeyboardDragAndDropFeatureDef = {
16 | state: {
17 | assistiveDndState?: AssistiveDndState | null;
18 | };
19 | config: {
20 | setAssistiveDndState?: SetStateFn;
21 | onStartKeyboardDrag?: (items: ItemInstance[]) => void;
22 | };
23 | treeInstance: {
24 | startKeyboardDrag: (items: ItemInstance[]) => void;
25 | startKeyboardDragOnForeignObject: (dataTransfer: DataTransfer) => void;
26 | stopKeyboardDrag: () => void;
27 | };
28 | itemInstance: {};
29 | hotkeys: "startDrag" | "cancelDrag" | "completeDrag" | "dragUp" | "dragDown";
30 | };
31 |
--------------------------------------------------------------------------------
/packages/core/src/features/main/types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FeatureImplementation,
3 | HotkeysConfig,
4 | ItemInstance,
5 | SetStateFn,
6 | TreeConfig,
7 | TreeInstance,
8 | TreeState,
9 | Updater,
10 | } from "../../types/core";
11 | import { ItemMeta } from "../tree/types";
12 |
13 | export type InstanceTypeMap = {
14 | itemInstance: ItemInstance;
15 | treeInstance: TreeInstance;
16 | };
17 |
18 | export type InstanceBuilder = (
19 | features: FeatureImplementation[],
20 | instanceType: T,
21 | buildOpts: (self: any) => any,
22 | ) => [instance: InstanceTypeMap[T], finalize: () => void];
23 |
24 | export type MainFeatureDef = {
25 | state: {};
26 | config: {
27 | features?: FeatureImplementation[];
28 | initialState?: Partial>;
29 | state?: Partial>;
30 | setState?: SetStateFn>>;
31 | instanceBuilder?: InstanceBuilder;
32 | };
33 | treeInstance: {
34 | /** @internal */
35 | applySubStateUpdate: >(
36 | stateName: K,
37 | updater: Updater[K]>,
38 | ) => void;
39 | setState: SetStateFn>;
40 | getState: () => TreeState;
41 | setConfig: SetStateFn>;
42 | getConfig: () => TreeConfig;
43 | getItemInstance: (itemId: string) => ItemInstance;
44 | getItems: () => ItemInstance[];
45 | registerElement: (element: HTMLElement | null) => void;
46 | getElement: () => HTMLElement | undefined | null;
47 | /** @internal */
48 | getDataRef: () => { current: D };
49 | /* @internal */
50 | getHotkeyPresets: () => HotkeysConfig;
51 | rebuildTree: () => void;
52 | };
53 | itemInstance: {
54 | registerElement: (element: HTMLElement | null) => void;
55 | getItemMeta: () => ItemMeta;
56 | getElement: () => HTMLElement | undefined | null;
57 | /** @internal */
58 | getDataRef: () => { current: D };
59 | };
60 | hotkeys: never;
61 | };
62 |
--------------------------------------------------------------------------------
/packages/core/src/features/prop-memoization/feature.ts:
--------------------------------------------------------------------------------
1 | import { FeatureImplementation } from "../../types/core";
2 | import { PropMemoizationDataRef } from "./types";
3 |
4 | const memoize = (
5 | props: Record,
6 | memoizedProps: Record,
7 | ) => {
8 | for (const key in props) {
9 | if (typeof props[key] === "function") {
10 | if (memoizedProps && key in memoizedProps) {
11 | props[key] = memoizedProps[key];
12 | } else {
13 | memoizedProps[key] = props[key];
14 | }
15 | }
16 | }
17 | return props;
18 | };
19 |
20 | export const propMemoizationFeature: FeatureImplementation = {
21 | key: "prop-memoization",
22 |
23 | overwrites: [
24 | "main",
25 | "async-data-loader",
26 | "sync-data-loader",
27 | "drag-and-drop",
28 | "expand-all",
29 | "hotkeys-core",
30 | "renaming",
31 | "search",
32 | "selection",
33 | ],
34 |
35 | treeInstance: {
36 | getContainerProps: ({ tree, prev }, treeLabel) => {
37 | const dataRef = tree.getDataRef();
38 | const props = prev?.(treeLabel) ?? {};
39 | dataRef.current.memo ??= {};
40 | dataRef.current.memo.tree ??= {};
41 | return memoize(props, dataRef.current.memo.tree);
42 | },
43 |
44 | getSearchInputElementProps: ({ tree, prev }) => {
45 | const dataRef = tree.getDataRef();
46 | const props = prev?.() ?? {};
47 | dataRef.current.memo ??= {};
48 | dataRef.current.memo.search ??= {};
49 | return memoize(props, dataRef.current.memo.search);
50 | },
51 | },
52 |
53 | itemInstance: {
54 | getProps: ({ item, prev }) => {
55 | const dataRef = item.getDataRef();
56 | const props = prev?.() ?? {};
57 | dataRef.current.memo ??= {};
58 | dataRef.current.memo.item ??= {};
59 | return memoize(props, dataRef.current.memo.item);
60 | },
61 |
62 | getRenameInputProps: ({ item, prev }) => {
63 | const dataRef = item.getDataRef();
64 | const props = prev?.() ?? {};
65 | dataRef.current.memo ??= {};
66 | dataRef.current.memo.rename ??= {};
67 | return memoize(props, dataRef.current.memo.rename);
68 | },
69 | },
70 | };
71 |
--------------------------------------------------------------------------------
/packages/core/src/features/prop-memoization/prop-memoization.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it, vi } from "vitest";
2 | import { TestTree } from "../../test-utils/test-tree";
3 | import { propMemoizationFeature } from "./feature";
4 | import { FeatureImplementation } from "../../types/core";
5 |
6 | const itemHandler = vi.fn();
7 | const treeHandler = vi.fn();
8 | const createItemValue = vi.fn();
9 | const createTreeValue = vi.fn();
10 |
11 | const customFeature: FeatureImplementation = {
12 | itemInstance: {
13 | getProps: ({ prev }) => ({
14 | ...prev?.(),
15 | customValue: createItemValue(),
16 | onCustomEvent: () => itemHandler(),
17 | }),
18 | },
19 | treeInstance: {
20 | getContainerProps: ({ prev }, treeLabel) => ({
21 | ...prev?.(treeLabel),
22 | customValue: createTreeValue(),
23 | onCustomEvent: () => treeHandler(),
24 | }),
25 | },
26 | };
27 |
28 | const factory = TestTree.default({}).withFeatures(
29 | customFeature,
30 | propMemoizationFeature,
31 | );
32 |
33 | describe("core-feature/prop-memoization", () => {
34 | it("memoizes props", async () => {
35 | const tree = await factory.suits.sync().tree.createTestCaseTree();
36 | createTreeValue.mockReturnValue(123);
37 | expect(tree.instance.getContainerProps().onCustomEvent).toBe(
38 | tree.instance.getContainerProps().onCustomEvent,
39 | );
40 | expect(tree.instance.getContainerProps().customValue).toBe(123);
41 | expect(tree.instance.getContainerProps().customValue).toBe(123);
42 | });
43 | factory.forSuits((tree) => {
44 | describe("tree props", () => {
45 | it("memoizes props", async () => {
46 | createTreeValue.mockReturnValue(123);
47 | expect(tree.instance.getContainerProps().onCustomEvent).toBe(
48 | tree.instance.getContainerProps().onCustomEvent,
49 | );
50 | expect(tree.instance.getContainerProps().customValue).toBe(123);
51 | expect(tree.instance.getContainerProps().customValue).toBe(123);
52 | });
53 |
54 | it("doesnt return stale values", async () => {
55 | createTreeValue.mockReturnValueOnce(123);
56 | createTreeValue.mockReturnValueOnce(456);
57 | expect(tree.instance.getContainerProps().customValue).toBe(123);
58 | expect(tree.instance.getContainerProps().customValue).toBe(456);
59 | });
60 |
61 | it("propagates calls properly", async () => {
62 | tree.instance.getContainerProps().onCustomEvent();
63 | tree.instance.getContainerProps().onCustomEvent();
64 | expect(treeHandler).toHaveBeenCalledTimes(2);
65 | });
66 | });
67 | });
68 | });
69 |
--------------------------------------------------------------------------------
/packages/core/src/features/prop-memoization/types.ts:
--------------------------------------------------------------------------------
1 | export interface PropMemoizationDataRef {
2 | memo?: {
3 | tree?: Record;
4 | item?: Record;
5 | search?: Record;
6 | rename?: Record;
7 | };
8 | }
9 |
10 | export type PropMemoizationFeatureDef = {
11 | state: {};
12 | config: {};
13 | treeInstance: {};
14 | itemInstance: {};
15 | hotkeys: never;
16 | };
17 |
--------------------------------------------------------------------------------
/packages/core/src/features/renaming/types.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance, SetStateFn } from "../../types/core";
2 |
3 | export type RenamingFeatureDef = {
4 | state: {
5 | renamingItem?: string | null;
6 | renamingValue?: string;
7 | };
8 | config: {
9 | setRenamingItem?: SetStateFn;
10 | setRenamingValue?: SetStateFn;
11 | canRename?: (item: ItemInstance) => boolean;
12 | onRename?: (item: ItemInstance, value: string) => void;
13 | };
14 | treeInstance: {
15 | getRenamingItem: () => ItemInstance | null;
16 | getRenamingValue: () => string;
17 | abortRenaming: () => void;
18 | completeRenaming: () => void;
19 | isRenamingItem: () => boolean;
20 | };
21 | itemInstance: {
22 | getRenameInputProps: () => any;
23 | canRename: () => boolean;
24 | isRenaming: () => boolean;
25 | startRenaming: () => void;
26 | };
27 | hotkeys: "renameItem" | "abortRenaming" | "completeRenaming";
28 | };
29 |
--------------------------------------------------------------------------------
/packages/core/src/features/search/types.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance, SetStateFn } from "../../types/core";
2 | import { HotkeysCoreDataRef } from "../hotkeys-core/types";
3 |
4 | export interface SearchFeatureDataRef extends HotkeysCoreDataRef {
5 | matchingItems: ItemInstance[];
6 | searchInput: HTMLInputElement | null;
7 | }
8 |
9 | export type SearchFeatureDef = {
10 | state: {
11 | search: string | null;
12 | };
13 | config: {
14 | setSearch?: SetStateFn;
15 | onOpenSearch?: () => void;
16 | onCloseSearch?: () => void;
17 | isSearchMatchingItem?: (search: string, item: ItemInstance) => boolean;
18 | };
19 | treeInstance: {
20 | setSearch: (search: string | null) => void;
21 | openSearch: (initialValue?: string) => void;
22 | closeSearch: () => void;
23 | isSearchOpen: () => boolean;
24 | getSearchValue: () => string;
25 | registerSearchInputElement: (element: HTMLInputElement | null) => void; // TODO remove
26 | getSearchInputElement: () => HTMLInputElement | null;
27 | getSearchInputElementProps: () => any;
28 | getSearchMatchingItems: () => ItemInstance[];
29 | };
30 | itemInstance: {
31 | isMatchingSearch: () => boolean;
32 | };
33 | hotkeys:
34 | | "openSearch"
35 | | "closeSearch"
36 | | "submitSearch"
37 | | "nextSearchItem"
38 | | "previousSearchItem";
39 | };
40 |
--------------------------------------------------------------------------------
/packages/core/src/features/selection/types.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance, SetStateFn } from "../../types/core";
2 |
3 | export type SelectionFeatureDef = {
4 | state: {
5 | selectedItems: string[];
6 | };
7 | config: {
8 | setSelectedItems?: SetStateFn;
9 | };
10 | treeInstance: {
11 | setSelectedItems: (selectedItems: string[]) => void;
12 | getSelectedItems: () => ItemInstance[];
13 | };
14 | itemInstance: {
15 | select: () => void;
16 | deselect: () => void;
17 | toggleSelect: () => void;
18 | isSelected: () => boolean;
19 | selectUpTo: (ctrl: boolean) => void;
20 | };
21 | hotkeys:
22 | | "toggleSelectedItem"
23 | | "selectUpwards"
24 | | "selectDownwards"
25 | | "selectAll";
26 | };
27 |
--------------------------------------------------------------------------------
/packages/core/src/features/sync-data-loader/feature.ts:
--------------------------------------------------------------------------------
1 | import { FeatureImplementation } from "../../types/core";
2 | import { makeStateUpdater } from "../../utils";
3 | import { throwError } from "../../utilities/errors";
4 |
5 | const promiseErrorMessage = "sync dataLoader returned promise";
6 | const unpromise = (data: T | Promise): T => {
7 | if (!data || (typeof data === "object" && "then" in data)) {
8 | throw throwError(promiseErrorMessage);
9 | }
10 | return data;
11 | };
12 |
13 | export const syncDataLoaderFeature: FeatureImplementation = {
14 | key: "sync-data-loader",
15 |
16 | getInitialState: (initialState) => ({
17 | loadingItemData: [],
18 | loadingItemChildrens: [],
19 | ...initialState,
20 | }),
21 |
22 | getDefaultConfig: (defaultConfig, tree) => ({
23 | setLoadingItemData: makeStateUpdater("loadingItemData", tree),
24 | setLoadingItemChildrens: makeStateUpdater("loadingItemChildrens", tree),
25 | ...defaultConfig,
26 | }),
27 |
28 | stateHandlerNames: {
29 | loadingItemData: "setLoadingItemData",
30 | loadingItemChildrens: "setLoadingItemChildrens",
31 | },
32 |
33 | treeInstance: {
34 | waitForItemDataLoaded: async () => {},
35 | waitForItemChildrenLoaded: async () => {},
36 |
37 | retrieveItemData: ({ tree }, itemId) => {
38 | return unpromise(tree.getConfig().dataLoader.getItem(itemId));
39 | },
40 |
41 | retrieveChildrenIds: ({ tree }, itemId) => {
42 | const { dataLoader } = tree.getConfig();
43 | if ("getChildren" in dataLoader) {
44 | return unpromise(dataLoader.getChildren(itemId));
45 | }
46 | return unpromise(dataLoader.getChildrenWithData(itemId)).map(
47 | (c) => c.data,
48 | );
49 | },
50 |
51 | loadItemData: ({ tree }, itemId) => tree.retrieveItemData(itemId),
52 | loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId),
53 | },
54 |
55 | itemInstance: {
56 | isLoading: () => false,
57 | },
58 | };
59 |
--------------------------------------------------------------------------------
/packages/core/src/features/sync-data-loader/types.ts:
--------------------------------------------------------------------------------
1 | export type TreeDataLoader =
2 | | {
3 | getItem: (itemId: string) => T | Promise;
4 | getChildren: (itemId: string) => string[] | Promise;
5 | }
6 | | {
7 | getItem: (itemId: string) => T | Promise;
8 | getChildrenWithData: (
9 | itemId: string,
10 | ) => { id: string; data: T }[] | Promise<{ id: string; data: T }[]>;
11 | };
12 |
13 | export type SyncDataLoaderFeatureDef = {
14 | state: {};
15 | config: {
16 | rootItemId: string;
17 | dataLoader: TreeDataLoader;
18 | };
19 | treeInstance: {
20 | retrieveItemData: (itemId: string) => T;
21 | retrieveChildrenIds: (itemId: string) => string[];
22 | };
23 | itemInstance: {
24 | isLoading: () => boolean;
25 | };
26 | hotkeys: never;
27 | };
28 |
--------------------------------------------------------------------------------
/packages/core/src/features/tree/types.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance, SetStateFn, TreeInstance } from "../../types/core";
2 |
3 | export interface ItemMeta {
4 | itemId: string;
5 | parentId: string;
6 | level: number;
7 | index: number;
8 | setSize: number;
9 | posInSet: number;
10 | }
11 |
12 | export interface TreeItemDataRef {
13 | memoizedValues: Record;
14 | memoizedDeps: Record;
15 | }
16 |
17 | export type TreeFeatureDef = {
18 | state: {
19 | expandedItems: string[];
20 | focusedItem: string | null;
21 | };
22 | config: {
23 | isItemFolder: (item: ItemInstance) => boolean;
24 | getItemName: (item: ItemInstance) => string;
25 |
26 | onPrimaryAction?: (item: ItemInstance) => void;
27 | scrollToItem?: (item: ItemInstance) => void;
28 |
29 | setExpandedItems?: SetStateFn;
30 | setFocusedItem?: SetStateFn;
31 | };
32 | treeInstance: {
33 | /** @internal */
34 | getItemsMeta: () => ItemMeta[];
35 |
36 | getFocusedItem: () => ItemInstance;
37 | focusNextItem: () => void;
38 | focusPreviousItem: () => void;
39 | updateDomFocus: () => void;
40 |
41 | /** Pass to the container rendering the tree children. The `treeLabel` parameter
42 | * will be passed as `aria-label` parameter, and is recommended to be set. */
43 | getContainerProps: (treeLabel?: string) => Record;
44 | };
45 | itemInstance: {
46 | getId: () => string;
47 | getProps: () => Record;
48 | getItemName: () => string;
49 | getItemData: () => T;
50 | equals: (other?: ItemInstance | null) => boolean;
51 | expand: () => void;
52 | collapse: () => void;
53 | isExpanded: () => boolean;
54 | isDescendentOf: (parentId: string) => boolean;
55 | isFocused: () => boolean;
56 | isFolder: () => boolean;
57 | setFocused: () => void;
58 | getParent: () => ItemInstance | undefined;
59 | getChildren: () => ItemInstance[];
60 | getIndexInParent: () => number;
61 | primaryAction: () => void;
62 | getTree: () => TreeInstance;
63 | getItemAbove: () => ItemInstance | undefined;
64 | getItemBelow: () => ItemInstance | undefined;
65 | scrollTo: (
66 | scrollIntoViewArg?: boolean | ScrollIntoViewOptions,
67 | ) => Promise;
68 | };
69 | hotkeys:
70 | | "focusNextItem"
71 | | "focusPreviousItem"
72 | | "expandOrDown"
73 | | "collapseOrUp"
74 | | "focusFirstItem"
75 | | "focusLastItem";
76 | };
77 |
--------------------------------------------------------------------------------
/packages/core/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./types/core";
2 | export * from "./core/create-tree";
3 |
4 | export * from "./features/tree/types";
5 | export { MainFeatureDef, InstanceBuilder } from "./features/main/types";
6 | export * from "./features/drag-and-drop/types";
7 | export * from "./features/keyboard-drag-and-drop/types";
8 | export * from "./features/selection/types";
9 | export * from "./features/async-data-loader/types";
10 | export * from "./features/sync-data-loader/types";
11 | export * from "./features/hotkeys-core/types";
12 | export * from "./features/search/types";
13 | export * from "./features/renaming/types";
14 | export * from "./features/expand-all/types";
15 | export * from "./features/prop-memoization/types";
16 |
17 | export * from "./features/selection/feature";
18 | export * from "./features/hotkeys-core/feature";
19 | export * from "./features/async-data-loader/feature";
20 | export * from "./features/sync-data-loader/feature";
21 | export * from "./features/drag-and-drop/feature";
22 | export * from "./features/keyboard-drag-and-drop/feature";
23 | export * from "./features/search/feature";
24 | export * from "./features/renaming/feature";
25 | export * from "./features/expand-all/feature";
26 | export * from "./features/prop-memoization/feature";
27 |
28 | export * from "./utilities/create-on-drop-handler";
29 | export * from "./utilities/insert-items-at-target";
30 | export * from "./utilities/remove-items-from-parents";
31 |
32 | export * from "./core/build-proxified-instance";
33 | export * from "./core/build-static-instance";
34 |
--------------------------------------------------------------------------------
/packages/core/src/types/deep-merge.ts:
--------------------------------------------------------------------------------
1 | type TAllKeys = T extends any ? keyof T : never;
2 |
3 | type TIndexValue = T extends any
4 | ? K extends keyof T
5 | ? T[K]
6 | : D
7 | : never;
8 |
9 | type TPartialKeys = Omit &
10 | Partial> extends infer O
11 | ? { [P in keyof O]: O[P] }
12 | : never;
13 |
14 | type TFunction = (...a: any[]) => any;
15 |
16 | type TPrimitives =
17 | | string
18 | | number
19 | | boolean
20 | | bigint
21 | | symbol
22 | | Date
23 | | TFunction;
24 |
25 | export type TMerged = [T] extends [Array]
26 | ? { [K in keyof T]: TMerged }
27 | : [T] extends [TPrimitives]
28 | ? T
29 | : [T] extends [object]
30 | ? TPartialKeys<{ [K in TAllKeys]: TMerged> }, never>
31 | : T;
32 |
--------------------------------------------------------------------------------
/packages/core/src/utilities/create-on-drop-handler.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance } from "../types/core";
2 | import { DragTarget } from "../features/drag-and-drop/types";
3 | import { removeItemsFromParents } from "./remove-items-from-parents";
4 | import { insertItemsAtTarget } from "./insert-items-at-target";
5 |
6 | export const createOnDropHandler =
7 | (
8 | onChangeChildren: (item: ItemInstance, newChildren: string[]) => void,
9 | ) =>
10 | async (items: ItemInstance[], target: DragTarget) => {
11 | const itemIds = items.map((item) => item.getId());
12 | await removeItemsFromParents(items, onChangeChildren);
13 | await insertItemsAtTarget(itemIds, target, onChangeChildren);
14 | };
15 |
--------------------------------------------------------------------------------
/packages/core/src/utilities/errors.ts:
--------------------------------------------------------------------------------
1 | const prefix = "Headless Tree: ";
2 |
3 | export const throwError = (message: string) => Error(prefix + message);
4 |
5 | // eslint-disable-next-line no-console
6 | export const logWarning = (message: string) => console.warn(prefix + message);
7 |
--------------------------------------------------------------------------------
/packages/core/src/utilities/insert-items-at-target.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance } from "../types/core";
2 | import { DragTarget } from "../features/drag-and-drop/types";
3 |
4 | export const insertItemsAtTarget = async (
5 | itemIds: string[],
6 | target: DragTarget,
7 | onChangeChildren: (
8 | item: ItemInstance,
9 | newChildrenIds: string[],
10 | ) => Promise | void,
11 | ) => {
12 | await target.item.getTree().waitForItemChildrenLoaded(target.item.getId());
13 | const oldChildrenIds = target.item
14 | .getTree()
15 | .retrieveChildrenIds(target.item.getId());
16 |
17 | // add moved items to new common parent, if dropped onto parent
18 | if (!("childIndex" in target)) {
19 | const newChildren = [...oldChildrenIds, ...itemIds];
20 | await onChangeChildren(target.item, newChildren);
21 | if (target.item && "updateCachedChildrenIds" in target.item) {
22 | target.item.updateCachedChildrenIds(newChildren);
23 | }
24 | target.item.getTree().rebuildTree();
25 | return;
26 | }
27 |
28 | // add moved items to new common parent, if dropped between siblings
29 | const newChildren = [
30 | ...oldChildrenIds.slice(0, target.insertionIndex),
31 | ...itemIds,
32 | ...oldChildrenIds.slice(target.insertionIndex),
33 | ];
34 |
35 | await onChangeChildren(target.item, newChildren);
36 |
37 | if (target.item && "updateCachedChildrenIds" in target.item) {
38 | target.item.updateCachedChildrenIds(newChildren);
39 | }
40 | target.item.getTree().rebuildTree();
41 | };
42 |
--------------------------------------------------------------------------------
/packages/core/src/utilities/remove-items-from-parents.ts:
--------------------------------------------------------------------------------
1 | import { ItemInstance } from "../types/core";
2 |
3 | export const removeItemsFromParents = async (
4 | movedItems: ItemInstance[],
5 | onChangeChildren: (
6 | item: ItemInstance,
7 | newChildrenIds: string[],
8 | ) => void | Promise,
9 | ) => {
10 | const movedItemsIds = movedItems.map((item) => item.getId());
11 | const uniqueParents = [
12 | ...new Set(movedItems.map((item) => item.getParent())),
13 | ];
14 |
15 | for (const parent of uniqueParents) {
16 | const siblings = parent?.getChildren();
17 | if (siblings && parent) {
18 | const newChildren = siblings
19 | .filter((sibling) => !movedItemsIds.includes(sibling.getId()))
20 | .map((i) => i.getId());
21 | await onChangeChildren(parent, newChildren);
22 | if (parent && "updateCachedChildrenIds" in parent) {
23 | parent?.updateCachedChildrenIds(newChildren);
24 | }
25 | }
26 | }
27 |
28 | movedItems[0].getTree().rebuildTree();
29 | };
30 |
--------------------------------------------------------------------------------
/packages/core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { SetStateFn, TreeState, Updater } from "./types/core";
2 |
3 | export type NoInfer = [T][T extends any ? 0 : never];
4 |
5 | export const memo = (
6 | deps: (...args: [...P]) => [...D],
7 | fn: (...args: [...D]) => R,
8 | ) => {
9 | let value: R | undefined;
10 | let oldDeps: D | null = null;
11 |
12 | return (...a: [...P]) => {
13 | const newDeps = deps(...a);
14 |
15 | if (!value) {
16 | value = fn(...newDeps);
17 | oldDeps = newDeps;
18 | return value;
19 | }
20 |
21 | const match =
22 | oldDeps &&
23 | oldDeps.length === newDeps.length &&
24 | !oldDeps.some((dep, i) => dep !== newDeps[i]);
25 |
26 | if (match) {
27 | return value;
28 | }
29 |
30 | value = fn(...newDeps);
31 | oldDeps = newDeps;
32 | return value;
33 | };
34 | };
35 |
36 | export function functionalUpdate(updater: Updater, input: T): T {
37 | return typeof updater === "function"
38 | ? (updater as (input: T) => T)(input)
39 | : updater;
40 | }
41 | export function makeStateUpdater>(
42 | key: K,
43 | instance: unknown,
44 | ): SetStateFn[K]> {
45 | return (updater: Updater[K]>) => {
46 | (instance as any).setState((old: TTableState) => {
47 | return {
48 | ...old,
49 | [key]: functionalUpdate(updater, (old as any)[key]),
50 | };
51 | });
52 | };
53 | }
54 |
55 | export const poll = (fn: () => boolean, interval = 100, timeout = 1000) =>
56 | new Promise((resolve) => {
57 | let clear: ReturnType;
58 | const i = setInterval(() => {
59 | if (fn()) {
60 | resolve();
61 | clearInterval(i);
62 | clearTimeout(clear);
63 | }
64 | }, interval);
65 | clear = setTimeout(() => {
66 | clearInterval(i);
67 | }, timeout);
68 | });
69 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "include": ["./src/**/*"],
4 | "exclude": ["./src/**/*.spec.tsx", "./src/**/*.spec.ts", "./src/**/*.stories.tsx"],
5 | "compilerOptions": {
6 | "outDir": "lib/esm"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/core/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["../../typedoc.base.json"],
3 | "entryPoints": ["src/mddocs-entry.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/packages/core/vitest.config.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | import { defineConfig } from "vitest/config";
3 |
4 | export default defineConfig({
5 | test: {},
6 | });
7 |
--------------------------------------------------------------------------------
/packages/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
22 | docs/6-changelog/*.md
23 | docs/5-contributing/1-overview.mdx
24 |
25 | static/llm-full.txt
26 | static/llms-full.txt
27 | static/llm.txt
28 | static/llms.txt
29 | static/llm
30 |
--------------------------------------------------------------------------------
/packages/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @headless-tree/docs
2 |
3 | ## 0.0.4
4 |
5 | ### Patch Changes
6 |
7 | - 8ba6e91: The Headless Tree documentation site now contains Angolia-powered search
8 |
9 | ## 0.0.3
10 |
11 | ### Patch Changes
12 |
13 | - 7ed33ac: dev release
14 |
15 | ## 0.0.2
16 |
17 | ### Patch Changes
18 |
19 | - 520ec27: test release
20 |
21 | ## 0.0.1
22 |
23 | ### Patch Changes
24 |
25 | - ab2a124: dev release
26 |
--------------------------------------------------------------------------------
/packages/docs/README.md:
--------------------------------------------------------------------------------
1 | # Website
2 |
3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
4 |
5 | ### Installation
6 |
7 | ```
8 | $ yarn
9 | ```
10 |
11 | ### Local Development
12 |
13 | ```
14 | $ yarn start
15 | ```
16 |
17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
18 |
19 | ### Build
20 |
21 | ```
22 | $ yarn build
23 | ```
24 |
25 | This command generates static content into the `build` directory and can be served using any static contents hosting service.
26 |
27 | ### Deployment
28 |
29 | Using SSH:
30 |
31 | ```
32 | $ USE_SSH=true yarn deploy
33 | ```
34 |
35 | Not using SSH:
36 |
37 | ```
38 | $ GIT_USER= yarn deploy
39 | ```
40 |
41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
42 |
--------------------------------------------------------------------------------
/packages/docs/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
3 | };
4 |
--------------------------------------------------------------------------------
/packages/docs/docs/1-guides/3-styling.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/guides/styling"
3 | title: "Styling"
4 | subtitle: "Customize the appearance of your tree"
5 | category: guide
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 |
10 | # Styling
11 |
12 | Headless Tree is designed to be as unopinionated as possible when it comes to styling. This means that both
13 | rendering and styling is generally up to you. The tree does not come with any default styles, and you can
14 | use whatever styling framework you have already set up in your project.
15 |
16 | Headless Tree provides several methods on the [item instance variable](/api/core/interface/ItemInstance) that you can use to derive information
17 | about each item and how it should be styled, such as
18 |
19 | - [`isFolder()`](/api/core/interface/ItemInstance#isFolder)
20 | - [`isExpanded()`](/api/core/interface/ItemInstance#isExpanded)
21 | - [`isSelected()`](/api/core/interface/ItemInstance#isSelected)
22 | - [`isFocused()`](/api/core/interface/ItemInstance#isFocused)
23 | - [`isDragTarget()`](/api/core/interface/ItemInstance#isDragTarget)
24 | - [`isDraggingOver()`](/api/core/interface/ItemInstance#isDraggingOver)
25 | - [`isRenaming()`](/api/core/interface/ItemInstance#isRenaming)
26 | - [`isLoading()`](/api/core/interface/ItemInstance#isLoading)
27 | - [`isMatchingSearch()`](/api/core/interface/ItemInstance#isMatchingSearch)
28 |
29 | In the following example, and default styles that are typically used in other samples are omitted,
30 | and just some basic inline styles are used to demonstrate how HT looks without styling:
31 |
32 |
33 |
34 | If you want to use the default styles that are used in other samples, you can copy their CSS code
35 | from here:
36 |
37 | - https://github.com/lukasbach/headless-tree/blob/main/packages/sb-react/.storybook/style.css
38 |
--------------------------------------------------------------------------------
/packages/docs/docs/1-guides/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Guides",
3 | "position": 2
4 | }
5 |
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/02-sync-dataloader.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/sync-dataloader"
3 | title: "Sync Data Loader"
4 | subtitle: "Data interface for synchronous data sources"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 |
19 | When using Headless Tree, you need to provide an interface to read your data. The most straight-forward way
20 | to do this is using the Sync Data Loader Feature. Alternatively, you can use the [Async Data Loader Feature](/features/async-dataloader)
21 | if you are dealing with asynchronous data sources. In both cases, you need to include the respective feature
22 | explicitly.
23 |
24 | When using the Sync Data Loader Feature, you need to provide a `dataLoader` property in your tree config
25 | which implements the [`TreeDataLoader`](/api/core#TreeDataLoader) interface.
26 |
27 | ```ts
28 | const tree = useTree({
29 | rootItemId: "root-item",
30 | getItemName: (item) => item.getItemData().itemName,
31 | isItemFolder: (item) => item.isFolder,
32 | dataLoader: {
33 | getItem: (itemId) => myDataStructure[itemId],
34 | getChildren: (itemId) => myDataStructure[itemId].childrenIds,
35 | },
36 | features: [ syncDataLoaderFeature ],
37 | });
38 | ```
39 |
40 | Note that Headless Tree may call `getItem` and `getChildren` multiple times for the same item, and also during each
41 | render. Therefore, you should make sure that these functions are fast and do not perform any expensive operations.
42 |
43 | Asynchronous data providers on the other hand provide caching out of the box, and only call those functions once
44 | per item until their data are explicitly invalidated. If your data source provides an synchronous, yet expensive
45 | interface, you can still use the [Async Data Loader](/features/async-dataloader) instead.
46 |
47 | :::warning
48 |
49 | You should implement the `dataLoader.getItem` and `dataLoader.getChildren` functions so that they return
50 | synchronously. If you need to fetch data asynchronously, you should use the [Async Data Loader](/features/async-dataloader)
51 | instead.
52 |
53 | :::
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/04-selection.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/selection"
3 | title: "Selections"
4 | subtitle: "Support for selecting multiple items"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import {FeaturePageHeader} from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 | The selection feature provides the ability of multiselect, allowing users to select multiple items at once.
19 | Without it, Headless Tree just allows users to focus a single item, and act on it through that.
20 | This feature is particularly useful in combination with the [drag-and-drop](/features/dnd) feature, as it allows users to
21 | select multiple items and drag them all at once.
22 |
23 | By default, Headless Tree will maintain the selected items in its internal state. If a `setState` or `setSelectedItems`
24 | function is provided in the tree configuration, you can manage the focused item yourself (see [Managing State](/guides/state)).
25 |
26 | Call `item.select()`, `item.deselect()` or `item.toggleSelection()` to change the selection state of an item. The feature
27 | will also add behavior to the `onClick` handler provided as prop for each of the tree items for ctrl-clicking and
28 | shift-clicking to select multiple items at once.
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/05-dnd.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/dnd"
3 | title: "Drag and Drop"
4 | subtitle: "Drag-and-drop Capabilities for tree items and external drag objects"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import {FeaturePageHeader} from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 |
19 |
20 | The Drag-And-Drop Feature provides drag-and-drop capabilities. It allows to drag a single tree item (or many of the
21 | [selection feature](/features/selection) is included in the tree config) and drop it somewhere else in the tree.
22 | The feature also allows you to create interactions between the tree and external drag objects, allowing you to drag
23 | tree items out of the tree, or foreign data objects from outside the tree inside. As extension of that, this can
24 | also be used to implement drag behavior between several trees.
25 |
26 | Since this feature composes a large part of the functionality of Headless Tree, it is documented in its own section
27 | "Drag and Drop" on the left, get started with the [Drag and Drop Overview Page](/dnd/overview).
28 |
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/07-hotkeys.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/hotkeys"
3 | title: "Hotkeys"
4 | subtitle: "Support for custom and predefined hotkeys to navigate and interact with the tree"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 |
19 | The Hotkeys Core feature is necessary for any keyboard-based interaction via hotkeys to work.
20 | Each of the features define their own hotkeys configurations (e.g. the Selection Feature provides the `selectAll` hotkey
21 | defaulting to `Ctrl+A`), which can be customized or extended in the tree configuration.
22 | However, only the Hotkeys Core feature implements the logic to listen for and handle keyboard events, and
23 | is necessary for any hotkeys of other features to work.
24 |
25 | The feature also makes it very easy to add custom hotkeys with arbitrary keybindings and implementations.
26 |
27 | Look into the [Guide on Hotkeys](/guides/hotkeys) for details on how to use the Hotkeys Core Feature, and how to
28 | overwrite or add custom hotkeys.
29 |
30 | The [Accessibility Guide](/guides/accessibility#hotkeys) shows some demos of which hotkeys are available and how they can be used.
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/08-search.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/search"
3 | title: "Tree Search"
4 | subtitle: "Searching for items in the tree by typing in a search query"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 |
19 | The Search feature provides the functionality for users to quickly find a certain item by typing
20 | a part of its name. This fulfills the accessibility feature of a typeahead search, and is particularly
21 | useful in large trees where the user might not be able to find the item they are looking for by
22 | scrolling through the tree, while also making its use a bit more obvious by showing the search
23 | input field. This is consistent with similar tree implementations such as the file tree in JetBrains IDEs,
24 | where typing in the tree will show a search input field.
25 |
26 | The items will not be shown in a filtered view, but instead search matches will be visually highlighted,
27 | the tree will be scrolled and focused to the first search match, and navigating through the items with
28 | up or down arrow keys will move the focus only between matched items.
29 |
30 | The search input field will automatically be opened when the user starts typing while focusing the tree,
31 | but it can also be opened programmatically by calling `tree.openSearch()`. Blurring the input field, pressing
32 | Escape (if the Hotkeys Core Feature is included), or invoking `tree.closeSearch()` will close the search.
33 | `tree.isSearchOpen()` can be used to check if the search input field is currently open and should be rendered.
34 |
35 | Similar to the remaining tree integration, it is up to the library consumer to render the search input field
36 | correctly. Pass `tree.getSearchInputElementProps()` as props to the search input to hook it up with change
37 | handlers and register its element with Headless Tree.
38 |
39 | ```jsx
40 | {tree.isSearchOpen() && (
41 | <>
42 |
43 | ({tree.getSearchMatchingItems().length} matches)
44 | >
45 | )}
46 | ```
47 |
48 | Then, use `item.isMatchingSearch()` to determine if an item is currently a search match, and style it accordingly.
49 |
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/09-renaming.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/renaming"
3 | title: "Renaming"
4 | subtitle: "Rename items in the tree"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 |
19 | The renaming feature allows you to allow users to rename items in the tree. They can either start renaming
20 | an item via the `renameItem` hotkey (defaulting to F2), or when the `item.startRenaming()` method is called.
21 | You can hook this up to e.g. double-clicking an item-name or to a context menu option.
22 |
23 | The feature exposes two state variables, `renamingItem` to keep track of the item that is currently being renamed,
24 | and `renamingValue` to keep track of the new name that the user is typing in. When
25 | [maintaining individual states](/guides/state#managing-individual-feature-states), you can hook into changes to
26 | those states via the `setRenamingItem` and `setRenamingValue` config methods.
27 |
28 | ## Rendering the Rename Input
29 |
30 | Similar to other Headless Tree Features, the library only provides the functionality, but not the UI. For any tree
31 | item, you can determine whether it should render a renaming behavior with `item.isRenaming()`. Make sure to
32 | pass `item.getRenameInputProps()` to the input element to hook up the renaming behavior to the input field.
33 |
34 | ```jsx
35 | if (item.isRenaming()) {
36 | return (
37 |
38 | );
39 | }
40 |
41 | // otherwise, render the item as usual
42 | ```
43 |
44 | ## Customizing which items can be renamed
45 |
46 | You can also customize which items can be renamed by providing a `canRename` function in the tree configuration.
47 |
48 |
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/10-expandall.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/expandall"
3 | title: "Expand all"
4 | subtitle: "API for expanding or collapsing all items with a single click"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
10 |
11 |
16 |
17 |
18 |
19 | The expand-all feature exposes methods both on any item instance and the tree instance to expand or collapse
20 | all of its items. When called on the tree instance, it will expand or collapse all items in the tree, otherwise
21 | it will only affect all children of the item.
22 |
23 | When used in conjunction with the [async data loader](/features/async-dataloader), the expand-all feature will
24 | wait for children to load in during each step, before triggering the expanding of the next level. You can also
25 | programmatically abort the expanding process at any time when this happens. When calling the `expandAll` method,
26 | you can pass an object with a `current` variable. Setting this to `false` during the expanding progress will
27 | cancel the expanding process while it is happening.
28 |
29 | ```ts
30 | const cancelToken = { current: false };
31 |
32 | const expand = () => {
33 | cancelToken.current = false;
34 | tree.expandAll(cancelToken);
35 | };
36 | const cancelExpanding = () => {
37 | cancelToken.current = true;
38 | };
39 | ```
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/11-prop-memoization.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/propmemoization"
3 | title: "Prop Memoization"
4 | subtitle: "Feature making all props created by HT consistent and useable for memoization"
5 | hide_title: true
6 | ---
7 |
8 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
9 | import {DemoBox} from "../../src/components/demo/demo-box";
10 |
11 |
16 |
17 | By default, React props generated by `tree.getContainerProps()` and `item.getProps()` will not
18 | be memoized, and might change their reference during each render as they are generated fresh
19 | each time.
20 |
21 | If you rely on stable props, or need them to be memoized, you can simply include the `propMemoizationFeature`
22 | and the props will be memoized automatically. The feature doesn't require any configuration or expose any
23 | methods.
24 |
25 | ## Example
26 |
27 | Consider this sample, where the render method of each item is intentionally slowed down, resulting
28 | in a slow usage experience when expanding or collapsing items:
29 |
30 |
31 |
32 | Here, the render method is memoized and the propMemoizationFeature is included. Tree items still
33 | render slowly during the initial render, but items that are unchanged during rerender do not retrigger
34 | the slow render function, resulting in a faster experience when expanding or collapsing items:
35 |
36 |
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/99-main.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/features/main"
3 | title: "Main Feature"
4 | subtitle: "Core Features related to Headless-Tree"
5 | hide_title: true
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 | import { FeaturePageHeader } from "../../src/components/docs-page/feature-page-header";
10 |
11 |
17 |
18 |
19 |
20 | The main feature provides the core functionality of Headless Tree, enabling most functions that are
21 | required by other features to work. Similar to the [Tree Feature](/features/tree), the main feature
22 | is included automatically and does not need to be actively imported.
--------------------------------------------------------------------------------
/packages/docs/docs/2-features/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Features",
3 | "position": 3
4 | }
5 |
--------------------------------------------------------------------------------
/packages/docs/docs/3-dnd/3-customizability.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/dnd/customizability"
3 | title: "Customizability"
4 | category: draganddrop
5 | ---
6 |
7 | import { DemoBox } from "../../src/components/demo/demo-box";
8 |
9 | # Customizability
10 |
11 | The Drag and Drop Feature provides several options to customize the behavior of the drag-and-drop interaction,
12 | including several ways to restrict what and when users can drag and drop items.
13 |
14 | ## No reordering
15 |
16 | Set the config option `canReorder` to false, to disable users from being able to choose arbitrary drop locations
17 | within a specific item. Drag lines will not be rendered, and can be omitted from the render implementation.
18 | Dragging an item between several items will either always target the specific item that is hovered over as new parent,
19 | or the parent of the item that is currently being hovered over if the direct hover target is not a folder.
20 |
21 | The [Drop event described earlier](/dnd/overview#the-drop-event) will always contain only an item as target,
22 | never `childIndex`, `insertionIndex`, `dragLineIndex` and `dragLineLevel`.
23 |
24 |
25 |
26 | ## Limiting which items can be dragged
27 |
28 | Implement a [`canDrag`](/api/core/interface/DragAndDropFeatureConfig#canDrag)
29 | handler that specifies which items can be dragged. This will be called everytime the user
30 | tries to start dragging items, and will pass the items to drag as parameter. Returning false in the handler
31 | will prevent the drag from starting.
32 |
33 |
34 |
35 | ## Limiting where users can drop items
36 |
37 | Similarly, implement a [`canDrop`](/api/core/interface/DragAndDropFeatureConfig#canDrop)
38 | handler that specifies where items can be dropped. This will be called everytime
39 | the user drags items over a potential drop target (not on every single mousemove event, just when a different drop
40 | target is hovered on), and will pass the items to drop and the target as parameters. Returning false in the handler
41 | will prevent the drop from finalizing and calling the `onDrop` method, as well as visually indicating that the
42 | drop is not allowed at that location.
43 |
44 | Note that this doesn't concern [dragging foreign data inside](/dnd/foreign-dnd#dragging-foreign-objects-inside-of-tree),
45 | which can be controlled with [`canDropForeignDragObject`](/api/core/interface/DragAndDropFeatureConfig#canDropForeignDragObject)
46 | instead.
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/packages/docs/docs/3-dnd/4-behavior.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/dnd/behavior"
3 | title: "Details on Dnd Behavior"
4 | category: draganddrop
5 | ---
6 |
7 | ## Reparenting
8 |
9 | For items dragged on the lower half of the bottom-most item of a tree, the specific
10 | target folder is chosen based on horizontal offset of the dragged item. This is called
11 | "Reparenting".
12 |
13 |
14 | 
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/packages/docs/docs/3-dnd/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Drag and Drop",
3 | "position": 4
4 | }
5 |
--------------------------------------------------------------------------------
/packages/docs/docs/4-recipes/1-handling-expensive-components.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/recipe/handling-expensive-components"
3 | title: "Handling expensive Components"
4 | subtitle: "How to use proper memoization to use expensive tree-item components"
5 | category: recipe
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 |
10 | Sometimes, rendering tree items can be expensive. This can cause slow usage of the Tree react component,
11 | since all items are rendered as part of a single flat list, and mutations to the displayed tree structure,
12 | such as expanding or collapsing items or changing the tree will trigger a re-render of all items.
13 |
14 |
15 |
16 | Memoizing the individual tree item render methods can help reduce the performance impact of rendering
17 | expensive tree items. This can be done using the `React.memo` function, which will only re-render the
18 | component if the props have changed.
19 |
20 | `React.memo` will only rerender its contents if any of its props have changed. Headless Tree doesn't
21 | memoize props generated with `tree.getContainerProps()` and `item.getProps()` by default, and will
22 | create new props during each render. By including the [Prop Memoization Feature](/features/propmemoization)
23 | however, these props will be memoized automatically, making memoization of render items feasible.
24 |
25 | Note that, in the sample below, expanding or collapsing individual items is much more efficient than
26 | in the sample above, since now only those items that actually change will be rerendered.
27 |
28 |
29 |
30 | You can further improve performance on large trees by making use of Virtualization, which is explained in
31 | the [Virtualization Guide](/recipe/virtualization).
32 |
--------------------------------------------------------------------------------
/packages/docs/docs/4-recipes/2-virtualization.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | slug: "/recipe/virtualization"
3 | title: "Virtualization"
4 | subtitle: "Integrate virtualization into your tree to support many items"
5 | category: recipe
6 | ---
7 |
8 | import { DemoBox } from "../../src/components/demo/demo-box";
9 |
10 | Large trees can have a significant impact on render and usage performance, and cause slow
11 | interactions. Virtualization is a concept, where the performance hit of a very large list of items
12 | is mitigated by only rendering the items that are currently visible in the viewport.
13 |
14 | While this is not trivial to do with nested structures like trees, Headless Tree makes this
15 | easy by flattening the tree structure and providing the tree items as flat list. Virtualization
16 | is not a included feature of Headless Tree, but you can easily pass this flat list to any virtualization
17 | library of your choice, and use that to create a tree that only renders the visible items.
18 |
19 | In the sample below, `react-virtual` is used to virtualize the tree and render 100k items while
20 | still being performant in rendering and interaction.
21 |
22 |
23 |
24 | :::warning
25 |
26 | You likely will want to use proxified item instances instead of static item instances when
27 | using trees with many items. Read this guide to learn more about [Proxy Item Instances](/recipe/proxy-instances).
28 | You can use them by setting the `instanceBuilder` tree config option to [`buildProxiedInstance`](/api/core/function/buildProxiedInstance),
29 | a symbol that you can import from `@headless-tree/core`.
30 |
31 | :::
32 |
33 | If you need to need a ref to the virtualized DOM items, keep in mind that `treeItem.getProps()` also
34 | returns a ref that needs to be assigned to the DOM element. Also keep in mind that the ref function
35 | of a DOM element is called both on mount and unmount, and calling `treeItem.getProps()` will fail
36 | if the item is already unloaded in the tree. Calling `treeItem.getProps()` outside the ref, like the following,
37 | will work:
38 |
39 | ```ts jsx
40 | const props = item.getProps();
41 | return (
42 |