├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── main.yml │ ├── prerelease.yml │ ├── release.yml │ └── verify.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── main.js ├── manager.js ├── preview.js └── utils │ ├── story-helpers.js │ └── storyOrder.js ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── demo.gif ├── demo2.gif ├── lerna-publish-summary.json ├── lerna.json ├── next-release-notes-template.md ├── next-release-notes.md ├── package.json ├── packages ├── autodemo │ ├── .gitignore │ ├── .npmignore │ ├── babel.config.js │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── AutoDemo.tsx │ │ ├── index.ts │ │ ├── stories │ │ │ └── AutoDemoComponent.stories.tsx │ │ └── styles.css │ └── tsconfig.json ├── blueprintjs-renderers │ ├── .gitignore │ ├── .npmignore │ ├── babel.config.js │ ├── package.json │ ├── readme.md │ ├── src │ │ ├── index.ts │ │ ├── renderers.tsx │ │ ├── stories │ │ │ └── BlueprintJsRenderers.stories.tsx │ │ └── styles.css │ └── tsconfig.json ├── core │ ├── .gitignore │ ├── .npmignore │ ├── babel.config.js │ ├── package.json │ ├── src │ │ ├── EventEmitter.ts │ │ ├── EventListenerManager.ts │ │ ├── controlledEnvironment │ │ │ ├── ControlledTreeEnvironment.tsx │ │ │ ├── InteractionManagerProvider.tsx │ │ │ ├── buildInteractionMode.ts │ │ │ ├── layoutUtils.ts │ │ │ ├── mergeInteractionManagers.ts │ │ │ ├── useControlledTreeEnvironmentProps.ts │ │ │ └── useLinearItems.ts │ │ ├── drag │ │ │ ├── DragAndDropProvider.tsx │ │ │ ├── DraggingPositionEvaluation.ts │ │ │ ├── useCanDropAt.ts │ │ │ ├── useDraggingPosition.ts │ │ │ ├── useGetParentOfLinearItem.ts │ │ │ └── useGetViableDragPositions.ts │ │ ├── environmentActions │ │ │ ├── EnvironmentActionsProvider.tsx │ │ │ └── useCreatedEnvironmentRef.ts │ │ ├── hotkeys │ │ │ ├── defaultKeyboardBindings.ts │ │ │ ├── useHotkey.ts │ │ │ ├── useKey.ts │ │ │ └── useKeyboardBindings.ts │ │ ├── index.ts │ │ ├── interactionMode │ │ │ ├── ClickArrowToExpandInteractionManager.ts │ │ │ ├── ClickItemToExpandInteractionManager.ts │ │ │ └── DoubleClickItemToExpandInteractionManager.ts │ │ ├── isControlKey.ts │ │ ├── renderers │ │ │ ├── createDefaultRenderers.tsx │ │ │ ├── index.ts │ │ │ └── useRenderers.ts │ │ ├── search │ │ │ ├── SearchInput.tsx │ │ │ ├── defaultMatcher.ts │ │ │ └── useSearchMatchFocus.ts │ │ ├── stories │ │ │ ├── 363.stories.tsx │ │ │ ├── Accessibility.stories.tsx │ │ │ ├── BasicExamples.stories.tsx │ │ │ ├── ControlledEnvironment.stories.tsx │ │ │ ├── CustomDataProvider.stories.tsx │ │ │ ├── CustomViewState.stories.tsx │ │ │ ├── DelayedDataSource.stories.tsx │ │ │ ├── DndConfigurability.stories.tsx │ │ │ ├── FindingItems.stories.tsx │ │ │ ├── HardcodedState.stories.tsx │ │ │ ├── InteractionModes.stories.tsx │ │ │ ├── Refs.stories.tsx │ │ │ ├── Scalability.stories.tsx │ │ │ ├── Search.stories.tsx │ │ │ └── Theming.stories.tsx │ │ ├── style-modern.css │ │ ├── style.css │ │ ├── tree │ │ │ ├── DragBetweenLine.tsx │ │ │ ├── LiveDescription.tsx │ │ │ ├── MaybeLiveDescription.tsx │ │ │ ├── Tree.tsx │ │ │ ├── TreeManager.tsx │ │ │ ├── defaultLiveDescriptors.ts │ │ │ ├── getItemsLinearly.ts │ │ │ ├── resolveLiveDescriptor.ts │ │ │ ├── scrollIntoView.ts │ │ │ ├── useCreatedTreeInformation.ts │ │ │ ├── useFocusWithin.ts │ │ │ ├── useMoveFocusToIndex.ts │ │ │ ├── useSelectUpTo.ts │ │ │ ├── useTreeKeyboardBindings.ts │ │ │ └── useViewState.ts │ │ ├── treeActions │ │ │ ├── TreeActionsProvider.tsx │ │ │ └── useCreatedTreeRef.ts │ │ ├── treeItem │ │ │ ├── TreeItemChildren.tsx │ │ │ ├── TreeItemElement.tsx │ │ │ ├── TreeItemRenamingInput.tsx │ │ │ └── useTreeItemRenderContext.ts │ │ ├── types.ts │ │ ├── uncontrolledEnvironment │ │ │ ├── CompleteTreeDataProvider.ts │ │ │ ├── StaticTreeDataProvider.ts │ │ │ └── UncontrolledTreeEnvironment.tsx │ │ ├── useCallSoon.ts │ │ ├── useGetOriginalItemOrder.ts │ │ ├── useHtmlElementEventListener.ts │ │ ├── useIsMounted.ts │ │ ├── useRefCopy.ts │ │ ├── useSideEffect.ts │ │ ├── useStableHandler.ts │ │ ├── utils.ts │ │ └── waitFor.ts │ ├── test │ │ ├── dnd-basics.spec.tsx │ │ ├── dnd-restrictions.spec.tsx │ │ ├── helpers │ │ │ ├── TestUtil.tsx │ │ │ ├── index.ts │ │ │ ├── setup.ts │ │ │ └── testTree.ts │ │ ├── navigation.spec.tsx │ │ └── selection.spec.tsx │ ├── tsconfig.json │ └── webpack.config.ts ├── demodata │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── treeData.ts │ └── tsconfig.json └── docs │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── docs │ ├── advanced │ │ └── _category_.json │ ├── changelog.mdx │ ├── faq.mdx │ ├── getstarted.mdx │ ├── guides │ │ ├── _category_.json │ │ ├── accessibility.mdx │ │ ├── autodemo.mdx │ │ ├── blueprintjs.mdx │ │ ├── controlled-environment.mdx │ │ ├── custom-data-provider.mdx │ │ ├── drag-and-drop.mdx │ │ ├── interaction-modes.mdx │ │ ├── keyboard.mdx │ │ ├── multiple-trees.mdx │ │ ├── refs.mdx │ │ ├── renaming.mdx │ │ ├── rendering.mdx │ │ ├── search.mdx │ │ ├── static-data-provider.mdx │ │ ├── styling.mdx │ │ ├── uncontrolled-environment.mdx │ │ └── viewstate.mdx │ └── react │ │ ├── ControlledTreeEnvironment.mdx │ │ ├── Tree.mdx │ │ ├── UncontrolledTreeEnvironment.mdx │ │ └── _category_.json │ ├── docusaurus.config.js │ ├── package.json │ ├── sidebars.js │ ├── src │ ├── components │ │ ├── CampaignBar.js │ │ ├── CampaignBar.module.css │ │ ├── HomepageFeatures.js │ │ ├── HomepageFeatures.module.css │ │ ├── PropTable.js │ │ └── StoryEmbed.js │ ├── css │ │ └── custom.css │ ├── pages │ │ ├── index.js │ │ ├── index.module.css │ │ └── markdown-page.md │ └── theme │ │ └── ReactLiveScope │ │ └── index.js │ └── static │ ├── .nojekyll │ └── img │ ├── example │ ├── 01.png │ ├── 02.png │ ├── 03.png │ ├── 04.png │ ├── 05.png │ └── 06.png │ ├── favicon.ico │ ├── logo.ai │ ├── logo.png │ ├── logo.svg │ └── tutorial │ ├── docsVersionDropdown.png │ └── localeDropdown.png ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "plugin:prettier/recommended", 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:react-hooks/recommended", 13 | "airbnb", 14 | "airbnb-typescript", 15 | "prettier" 16 | ], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "ecmaFeatures": { 20 | "jsx": true 21 | }, 22 | "ecmaVersion": 12, 23 | "sourceType": "module", 24 | "project": "./tsconfig.json" 25 | }, 26 | "plugins": ["react", "@typescript-eslint", "prettier"], 27 | "rules": { 28 | "linebreak-style": ["error", "unix"], 29 | "quotes": ["error", "single"], 30 | "semi": ["error", "always"], 31 | "@typescript-eslint/no-explicit-any": "off", 32 | "@typescript-eslint/no-non-null-assertion": "off", 33 | "@typescript-eslint/explicit-module-boundary-types": "off", 34 | "@typescript-eslint/no-empty-function": "off", 35 | "@typescript-eslint/ban-types": "off", 36 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 37 | "react/display-name": "off", 38 | "react/prop-types": "off", 39 | "react-hooks/exhaustive-deps": [ 40 | "error", 41 | { 42 | "additionalHooks": "(useSideEffect)|(useMemoizedObject)" 43 | } 44 | ], 45 | "import/prefer-default-export": "off", 46 | "react/jsx-props-no-spreading": "off", 47 | "no-nested-ternary": "off", 48 | "@typescript-eslint/no-shadow": "off", 49 | "import/no-extraneous-dependencies": "off", 50 | "class-methods-use-this": "off", 51 | "react/require-default-props": "off", 52 | "no-restricted-syntax": "off", 53 | "react/function-component-definition": "off", 54 | 55 | // Dependency cycles come from contexts, context providers and context hooks being in the same file. 56 | // Could be solved, but also doesn't really bring any harm to keep as is. 57 | "import/no-cycle": "off" 58 | }, 59 | "ignorePatterns": ["**/lib", "**/webpack.config.ts", "**/*.js"] 60 | } 61 | -------------------------------------------------------------------------------- /.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 | 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **To Reproduce** 22 | Steps to reproduce the behavior (ideally a codesandbox link or similar if for a library issue): 23 | 24 | **Expected behavior** 25 | A clear and concise description of what you expected to happen. 26 | 27 | **Screenshots** 28 | If applicable, add screenshots to help explain your problem. A gif or screencast can also help understand your problem. 29 | 30 | **Additional context** 31 | You can help by providing additional details that are available to you, such as 32 | 33 | - Device if mobile 34 | - Operating System, Browser 35 | - Version of the Library or tool for which you report the bug 36 | 37 | -------------------------------------------------------------------------------- /.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 6 | - name: Github Discussions 7 | url: https://github.com/lukasbach/react-complex-tree/discussions 8 | about: Discussions can also be posted on Github Discussions 9 | -------------------------------------------------------------------------------- /.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 | 17 | 18 | **Is your feature request related to a problem? Please describe.** 19 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 20 | 21 | **Describe the solution you'd like** 22 | A clear and concise description of what you want to happen. 23 | 24 | **Describe alternatives you've considered** 25 | A clear and concise description of any alternative solutions or features you've considered. 26 | 27 | **Additional context** 28 | Add any other context or screenshots about the feature request here. 29 | 30 | -------------------------------------------------------------------------------- /.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' 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Solves #TODO 2 | 3 | 4 | 5 | 11 | 12 | @lukasbach 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: 'CodeQL' 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [main] 9 | schedule: 10 | - cron: '39 18 * * 4' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: ['javascript'] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 26 | # Learn more: 27 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v2 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v1 36 | with: 37 | languages: ${{ matrix.language }} 38 | # If you wish to specify custom queries, you can do so here or in a config file. 39 | # By default, queries listed here will override any specified in a config file. 40 | # Prefix the list here with "+" to use these queries and those in the config file. 41 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 42 | 43 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 44 | # If this step fails, then you should remove it and run the build manually (see below) 45 | - name: Autobuild 46 | uses: github/codeql-action/autobuild@v1 47 | 48 | # ℹ️ Command-line programs to run using the OS shell. 49 | # 📚 https://git.io/JvXDl 50 | 51 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 52 | # and modify them (or add more) to build your code if your project 53 | # uses a compiled language 54 | 55 | #- run: | 56 | # make bootstrap 57 | # make release 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v1 61 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Docs Deployment 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: volta-cli/action@v4 13 | - name: Dependencies 14 | run: yarn 15 | - name: Build 16 | run: yarn build 17 | - name: Storybook 18 | run: yarn build-storybook 19 | - name: Prepare 20 | run: | 21 | mv ./storybook-static ./packages/docs/build/storybook 22 | echo 'rct.lukasbach.com' >> ./packages/docs/build/CNAME 23 | - name: Deploy 🚀 24 | uses: JamesIves/github-pages-deploy-action@3.6.2 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | BRANCH: gh-pages 28 | FOLDER: packages/docs/build 29 | CLEAN: true 30 | SINGLE_COMMIT: true 31 | GIT_CONFIG_NAME: lukasbachbot 32 | GIT_CONFIG_EMAIL: bot@noreply.lukasbach.com 33 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Prerelease 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: volta-cli/action@v4 12 | - name: Dependencies 13 | run: yarn 14 | - name: Lint 15 | run: yarn lint 16 | - name: Build 17 | run: yarn build 18 | - name: Test 19 | run: yarn test 20 | - name: Do Release 21 | run: | 22 | git config --global user.email "bot@noreply.lukasbach.com" 23 | git config --global user.name "lukasbachbot" 24 | npm set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }} 25 | yarn lerna publish prerelease --yes --no-verify-access --summary-file --loglevel silly --dist-tag prerelease 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 28 | - name: Release info 29 | run: cat ./lerna-publish-summary.json 30 | - name: Update lockfile 31 | run: | 32 | echo "enableImmutableInstalls: false" > ./.yarnrc.yml 33 | yarn 34 | git checkout HEAD -- ./.yarnrc.yml 35 | env: 36 | CI: false 37 | - name: Push remaining changes 38 | uses: EndBug/add-and-commit@v9 39 | with: 40 | author_name: lukasbachbot 41 | author_email: bot@noreply.lukasbach.com 42 | message: 'chore: tidy up after prerelease' 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | bump: 7 | required: true 8 | type: choice 9 | options: 10 | - major 11 | - minor 12 | - patch 13 | - premajor 14 | - preminor 15 | - prepatch 16 | - prerelease 17 | - from-git 18 | - from-package 19 | 20 | jobs: 21 | release: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | - uses: volta-cli/action@v4 26 | - name: Dependencies 27 | run: yarn 28 | - name: Lint 29 | run: yarn lint 30 | - name: Build 31 | run: yarn build 32 | - name: Test 33 | run: yarn test 34 | - name: Do Release 35 | run: | 36 | git config --global user.email "bot@noreply.lukasbach.com" 37 | git config --global user.name "lukasbachbot" 38 | npm set //registry.npmjs.org/:_authToken ${{ secrets.NPM_TOKEN }} 39 | yarn lerna publish ${{ github.event.inputs.bump }} --yes --no-verify-access --summary-file --loglevel silly 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | - name: Release info 43 | run: cat ./lerna-publish-summary.json 44 | - name: Read new version 45 | run: | 46 | pkg=$(cat ./packages/core/package.json) 47 | ver=$(jq -r '.version' <<< $pkg) 48 | echo "version=$ver" >> $GITHUB_ENV 49 | echo "New Version is $ver" 50 | - name: Check Changelog 51 | run: | 52 | if ! test -f "./next-release-notes.md"; then 53 | exit 1 54 | fi 55 | echo "Changelog:" 56 | cat ./next-release-notes.md 57 | - name: Update Changelog 58 | run: | 59 | now="$(date +'%d/%m/%Y')" 60 | # sorry 61 | echo -e "$(sed -n '1,6p' ./packages/docs/docs/changelog.mdx)\n\n## ${{env.version}} - $now\n\n$(cat ./next-release-notes.md)\n\n\n$(sed -n '6,99999p' ./packages/docs/docs/changelog.mdx)" > ./packages/docs/docs/changelog.mdx 62 | - name: Github Release 63 | uses: softprops/action-gh-release@v1 64 | with: 65 | body_path: next-release-notes.md 66 | name: ${{env.version}} 67 | tag_name: ${{env.version}} 68 | files: | 69 | lerna-publish-summary.json 70 | next-release-notes.md 71 | LICENSE 72 | - name: Prepare next Changelog 73 | run: | 74 | rm ./next-release-notes.md 75 | cp ./next-release-notes-template.md ./next-release-notes.md 76 | - name: Update lockfile 77 | run: | 78 | echo "enableImmutableInstalls: false" > ./.yarnrc.yml 79 | yarn 80 | git checkout HEAD -- ./.yarnrc.yml 81 | env: 82 | CI: false 83 | - name: Push remaining changes 84 | uses: EndBug/add-and-commit@v9 85 | with: 86 | author_name: lukasbachbot 87 | author_email: bot@noreply.lukasbach.com 88 | message: 'chore: tidy up after release' 89 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | react-versions: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | react: [ 16, 17, 18 ] 13 | name: React ${{ matrix.react }} Build 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: volta-cli/action@v4 17 | - name: Dependencies 18 | run: yarn 19 | - name: Custom React version 20 | run: | 21 | echo -e "nodeLinker: node-modules\nenableImmutableInstalls: false" > ./.yarnrc.yml 22 | npm pkg set devDependencies.react=${{matrix.react}} --ws 23 | cat ./.yarnrc.yml 24 | yarn 25 | - name: Build 26 | run: yarn build:core 27 | 28 | verify: 29 | runs-on: ubuntu-latest 30 | name: Lint and Test 31 | steps: 32 | - uses: actions/checkout@v3 33 | - uses: volta-cli/action@v4 34 | - name: Dependencies 35 | run: yarn 36 | - name: Lint 37 | run: yarn lint 38 | - name: Build 39 | run: yarn build 40 | - name: Test 41 | run: yarn test 42 | - name: Storybook 43 | run: yarn build-storybook 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | storybook-static 4 | yarn-error.log 5 | 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/sdks 12 | !.yarn/versions -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | packages/*/lib 3 | packages/*/node_modules 4 | storybook-static 5 | packages/docs/.docusaurus 6 | packages/docs/build 7 | lerna.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "bracketSpacing": true, 4 | "printWidth": 80, 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "arrowParens": "avoid", 10 | "endOfLine": "lf", 11 | "trailingComma": "es5" 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../packages/**/*.stories.mdx', '../packages/**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: [ 4 | '@storybook/addon-docs', 5 | '@storybook/addon-actions', 6 | '@storybook/addon-links', 7 | '@storybook/addon-essentials', 8 | '@storybook/addon-a11y', 9 | ], 10 | core: { 11 | builder: "webpack5", 12 | }, 13 | typescript: { 14 | check: true, 15 | reactDocgen: 'react-docgen-typescript', 16 | reactDocgenTypescriptOptions: { 17 | compilerOptions: { 18 | allowSyntheticDefaultImports: true, 19 | esModuleInterop: true, 20 | module: 'CommonJS', 21 | }, 22 | }, 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons'; 2 | 3 | addons.setConfig({ 4 | enableShortcuts: false, 5 | }); 6 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { sortStories } from './utils/story-helpers'; 2 | import { storyOrder } from './utils/storyOrder'; 3 | import React from 'react'; 4 | 5 | import 'react-complex-tree/src/style-modern.css'; 6 | import '../packages/autodemo/src/styles.css'; 7 | 8 | export const parameters = { 9 | options: { 10 | storySort: sortStories(storyOrder), 11 | }, 12 | actions: { argTypesRegex: '^on[A-Z].*' }, 13 | controls: { 14 | matchers: { 15 | color: /(background|color)$/i, 16 | date: /Date$/, 17 | }, 18 | }, 19 | a11y: { 20 | manual: true, 21 | }, 22 | }; 23 | 24 | if (process.env.NODE_ENV === 'development') { 25 | const whyDidYouRender = require('@welldone-software/why-did-you-render'); 26 | whyDidYouRender(React, { 27 | trackAllPureComponents: true, 28 | trackHooks: true, 29 | logOnDifferentValues: true, 30 | collapseGroups: true, 31 | }); 32 | } 33 | 34 | (() => { 35 | const el = document.createElement('script'); 36 | el.src = 'https://unpkg.com/iframe-resizer@4.3.2/js/iframeResizer.contentWindow.min.js'; 37 | document.head.append(el); 38 | })(); 39 | -------------------------------------------------------------------------------- /.storybook/utils/story-helpers.js: -------------------------------------------------------------------------------- 1 | // https://github.com/sumup-oss/circuit-ui/blob/canary/.storybook/util/story-helpers.js#L9 2 | export function sortStories(sortOrder) { 3 | const groups = Object.keys(sortOrder); 4 | 5 | return (a, b) => { 6 | const aKind = a[1].kind; 7 | const bKind = b[1].kind; 8 | const [aGroup, aComponent] = splitStoryName(aKind); 9 | const [bGroup, bComponent] = splitStoryName(bKind); 10 | 11 | // Preserve story sort order. 12 | if (aKind === bKind) { 13 | return 0; 14 | } 15 | 16 | // Sort stories in a group. 17 | if (aGroup === bGroup) { 18 | const group = sortOrder[aGroup]; 19 | 20 | return position(aComponent, group) - position(bComponent, group); 21 | } 22 | 23 | // Sort groups. 24 | return position(aGroup, groups) - position(bGroup, groups); 25 | }; 26 | } 27 | 28 | export function splitStoryName(name) { 29 | return name.split('/'); 30 | } 31 | 32 | function position(item, array = []) { 33 | const index = array.indexOf(item); 34 | // Show unsorted items at the bottom. 35 | return index === -1 ? 10000 : index; 36 | } 37 | -------------------------------------------------------------------------------- /.storybook/utils/storyOrder.js: -------------------------------------------------------------------------------- 1 | export const storyOrder = { 2 | Core: ['Basic Examples', 'Controlled Environment'], 3 | AutoDemo: [], 4 | }; 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | [ 5 | '@babel/preset-react', 6 | { 7 | runtime: 'automatic', 8 | development: process.env.NODE_ENV === 'development', 9 | importSource: '@welldone-software/why-did-you-render', 10 | }, 11 | ], 12 | '@babel/preset-typescript', 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/demo.gif -------------------------------------------------------------------------------- /demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/demo2.gif -------------------------------------------------------------------------------- /lerna-publish-summary.json: -------------------------------------------------------------------------------- 1 | [{"packageName":"react-complex-tree-autodemo","version":"2.6.1"},{"packageName":"react-complex-tree-blueprintjs-renderers","version":"2.6.1"},{"packageName":"react-complex-tree","version":"2.6.1"}] -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "2.6.1" 6 | } 7 | -------------------------------------------------------------------------------- /next-release-notes-template.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next-release-notes.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-complex-tree-root", 3 | "version": "0.0.1", 4 | "repository": { 5 | "type": "git", 6 | "url": "git@github.com:lukasbach/react-complex-tree.git", 7 | "directory": "." 8 | }, 9 | "author": "Lukas Bach", 10 | "license": "MIT", 11 | "private": true, 12 | "scripts": { 13 | "start": "lerna run start --parallel", 14 | "build": "lerna run build", 15 | "build:core": "lerna run build --scope react-complex-tree", 16 | "test": "lerna run test --stream", 17 | "storybook": "start-storybook -p 6006", 18 | "build-storybook": "build-storybook", 19 | "prepublishOnly": "yarn build", 20 | "lint": "eslint --ext .ts,.tsx packages/", 21 | "lint:fix": "eslint --ext .ts,.tsx packages/ --fix" 22 | }, 23 | "devDependencies": { 24 | "@storybook/addon-a11y": "^6.5.14", 25 | "@storybook/addon-actions": "^6.5.14", 26 | "@storybook/addon-docs": "^6.5.14", 27 | "@storybook/addon-essentials": "^6.5.14", 28 | "@storybook/addon-links": "^6.5.14", 29 | "@storybook/addons": "^6.5.14", 30 | "@storybook/builder-webpack5": "^6.5.14", 31 | "@storybook/manager-webpack5": "^6.5.14", 32 | "@storybook/preset-typescript": "^3.0.0", 33 | "@storybook/react": "^6.5.14", 34 | "@types/storybook__react": "^5.2.1", 35 | "@typescript-eslint/eslint-plugin": "^5.41.0", 36 | "@typescript-eslint/parser": "^5.41.0", 37 | "eslint": "^8.26.0", 38 | "eslint-config-airbnb": "^19.0.4", 39 | "eslint-config-airbnb-typescript": "^17.0.0", 40 | "eslint-config-prettier": "^8.5.0", 41 | "eslint-plugin-import": "^2.26.0", 42 | "eslint-plugin-jsx-a11y": "^6.6.1", 43 | "eslint-plugin-prettier": "^4.2.1", 44 | "eslint-plugin-react": "^7.31.10", 45 | "eslint-plugin-react-hooks": "^4.6.0", 46 | "eslint-plugin-storybook": "^0.6.8", 47 | "lerna": "^6.1.0", 48 | "prettier": "^2.7.1", 49 | "react": "^18.2.0", 50 | "react-docgen": "^5.3.1", 51 | "react-docgen-typescript": "^2.2.2", 52 | "react-docgen-typescript-loader": "^3.7.2", 53 | "react-dom": "^18.2.0", 54 | "storybook-addon-react-docgen": "^1.2.42", 55 | "ts-loader": "^9.4.1", 56 | "webpack": "^5.74.0" 57 | }, 58 | "workspaces": { 59 | "packages": [ 60 | "packages/*" 61 | ], 62 | "nohoist": [ 63 | "**/html-minifier-terser" 64 | ] 65 | }, 66 | "volta": { 67 | "node": "18.12.1", 68 | "yarn": "3.3.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/autodemo/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /packages/autodemo/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | babel.config.js 3 | tsconfig.json 4 | node_modules 5 | -------------------------------------------------------------------------------- /packages/autodemo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/autodemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-complex-tree-autodemo", 3 | "version": "2.6.1", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:lukasbach/react-complex-tree.git", 9 | "directory": "packages/autodemo" 10 | }, 11 | "author": "Lukas Bach ", 12 | "description": "Autodemo component for the react-complex-tree package", 13 | "homepage": "https://rct.lukasbach.com/docs/guides/autodemo", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/core": "^7.14.0", 17 | "@babel/preset-env": "^7.14.1", 18 | "@babel/preset-react": "^7.13.13", 19 | "@babel/preset-typescript": "^7.13.0", 20 | "@lukasbach/tsconfig": "^0.1.0", 21 | "@types/jest": "^27.4.1", 22 | "@types/react": "^18.0.14", 23 | "@types/react-dom": "^18.0.7", 24 | "babel-jest": "^27.5.1", 25 | "babel-loader": "^9.1.0", 26 | "demodata": "^2.6.1", 27 | "jest": "^26.6.3", 28 | "react": "^18.2.0", 29 | "react-complex-tree": "^2.6.1", 30 | "react-dom": "^18.2.0", 31 | "react-test-renderer": "^18.2.0", 32 | "ts-node": "^10.7.0", 33 | "typescript": "4.9.3" 34 | }, 35 | "scripts": { 36 | "build": "tsc" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/autodemo/readme.md: -------------------------------------------------------------------------------- 1 | # React Complex Tree - AutoDemo 2 | 3 | > Utility component for `react-complex-tree` that visualizes tree interactions according 4 | > to a hardcoded script. Mainly used as automated demo for `react-complex-tree`. 5 | 6 | Relevant links: 7 | 8 | - [Example](https://rct.lukasbach.com/storybook/?path=/story/auto-demo-autodemo-component--single-tree-demo) 9 | - [Documentation](https://rct.lukasbach.com/docs/guides/autodemo) 10 | - [`react-complex-tree`](https://rct.lukasbach.com) 11 | -------------------------------------------------------------------------------- /packages/autodemo/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AutoDemo'; 2 | -------------------------------------------------------------------------------- /packages/autodemo/src/styles.css: -------------------------------------------------------------------------------- 1 | .rct-autodemo-container { 2 | position: relative; 3 | } 4 | 5 | .rct-autodemo-content { 6 | display: flex; 7 | } 8 | 9 | .rct-autodemo-content-childcount-1 > * { 10 | width: 100%; 11 | } 12 | .rct-autodemo-content-childcount-2 > * { 13 | width: 50%; 14 | } 15 | .rct-autodemo-content-childcount-3 > * { 16 | width: 33%; 17 | } 18 | .rct-autodemo-content-childcount-4 > * { 19 | width: 25%; 20 | } 21 | .rct-autodemo-content-childcount-5 > * { 22 | width: 20%; 23 | } 24 | 25 | .rct-autodemo-content > * { 26 | flex-grow: 1; 27 | margin: 5px; 28 | } 29 | 30 | .rct-autodemo-controls { 31 | position: absolute; 32 | bottom: 20px; 33 | right: 20px; 34 | font-family: Arial, sans-serif; 35 | font-size: 14px; 36 | background-color: #ffffff; 37 | border: 1px solid #cbcbcb; 38 | border-radius: 8px; 39 | padding: 8px; 40 | display: flex; 41 | color: #444; 42 | opacity: 0.5; 43 | } 44 | 45 | .rct-autodemo-controls:hover { 46 | opacity: 1; 47 | } 48 | 49 | .rct-autodemo-controls-header { 50 | display: flex; 51 | } 52 | 53 | .rct-autodemo-controls h2 { 54 | font-size: 16px; 55 | margin: 0; 56 | } 57 | .rct-autodemo-controls p { 58 | margin: 0; 59 | } 60 | 61 | .rct-autodemo-controls-left { 62 | flex-grow: 1; 63 | align-self: center; 64 | } 65 | .rct-autodemo-controls-right { 66 | align-self: center; 67 | margin-left: 10px; 68 | } 69 | 70 | .rct-autodemo-controls button { 71 | background-color: #ffffff; 72 | color: inherit; 73 | border: 1px solid #cbcbcb; 74 | border-radius: 4px; 75 | padding: 4px 8px; 76 | cursor: pointer; 77 | } 78 | 79 | .rct-autodemo-controls button:hover { 80 | background-color: #ececec; 81 | } 82 | 83 | .rct-autodemo-controls-speed { 84 | margin-left: 8px; 85 | } 86 | 87 | .rct-autodemo-controls-speed button { 88 | border: none; 89 | padding: 0 4px; 90 | } 91 | 92 | .rct-autodemo-controls-speed button.active { 93 | font-weight: bold; 94 | text-decoration: underline; 95 | } 96 | -------------------------------------------------------------------------------- /packages/autodemo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lukasbach/tsconfig/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["src/stories"], 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | babel.config.js 3 | tsconfig.json 4 | node_modules 5 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-complex-tree-blueprintjs-renderers", 3 | "version": "2.6.1", 4 | "main": "lib/index.js", 5 | "types": "lib/index.d.ts", 6 | "repository": { 7 | "type": "git", 8 | "url": "git@github.com:lukasbach/react-complex-tree.git", 9 | "directory": "packages/blueprintjs-renderers" 10 | }, 11 | "funding": "https://github.com/sponsors/lukasbach", 12 | "description": "Renderers for react-complex-tree with a BlueprintJS-styled theme", 13 | "homepage": "https://rct.lukasbach.com/docs/guides/blueprintjs", 14 | "author": "Lukas Bach ", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@babel/core": "^7.14.0", 18 | "@babel/preset-env": "^7.14.1", 19 | "@babel/preset-react": "^7.13.13", 20 | "@babel/preset-typescript": "^7.13.0", 21 | "@blueprintjs/core": "^4.0.0", 22 | "@blueprintjs/icons": "^4.0.0", 23 | "@lukasbach/tsconfig": "^0.1.0", 24 | "@types/jest": "^27.4.1", 25 | "@types/react": "^18.0.14", 26 | "@types/react-dom": "^18.0.7", 27 | "babel-jest": "^27.5.1", 28 | "babel-loader": "^9.1.0", 29 | "demodata": "^2.6.1", 30 | "jest": "^26.6.3", 31 | "react": "^18.2.0", 32 | "react-complex-tree": "^2.6.1", 33 | "react-dom": "^18.2.0", 34 | "react-test-renderer": "^18.2.0", 35 | "ts-node": "^10.7.0", 36 | "typescript": "4.9.3" 37 | }, 38 | "scripts": { 39 | "build": "tsc" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/readme.md: -------------------------------------------------------------------------------- 1 | # React Complex Tree - BlueprintJS Renderers 2 | 3 | > Render methods for `react-complex-tree` that style the tree like 4 | > [BlueprintJS's](https://blueprintjs.com/docs/#core/components/tree) 5 | > trees. 6 | 7 | Relevant links: 8 | 9 | - [Example](https://rct.lukasbach.com/storybook/?path=/story/blueprintjs-renderers-blueprintjs-renderers--blueprint-js-tree) 10 | - [Documentation](https://rct.lukasbach.com/docs/guides/blueprintjs) 11 | - [`react-complex-tree`](https://rct.lukasbach.com) 12 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './renderers'; 2 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/src/stories/BlueprintJsRenderers.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React from 'react'; 3 | import { 4 | InteractionMode, 5 | StaticTreeDataProvider, 6 | Tree, 7 | UncontrolledTreeEnvironment, 8 | } from 'react-complex-tree'; 9 | import { longTree, shortTree } from 'demodata'; 10 | import { Colors, FocusStyleManager } from '@blueprintjs/core'; 11 | import { renderers } from '../renderers'; 12 | 13 | FocusStyleManager.onlyShowFocusOnTabs(); 14 | 15 | // import '@blueprintjs/core/lib/css/blueprint.css'; 16 | 17 | export default { 18 | title: 'BlueprintJs Renderers/BlueprintJs Renderers', 19 | decorators: [ 20 | Story => ( 21 | <> 22 | 23 | 27 | 28 | 29 | ), 30 | ], 31 | } as Meta; 32 | 33 | export const BlueprintJsTree = () => ( 34 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions 35 |
FocusStyleManager.onlyShowFocusOnTabs()} 38 | onKeyDown={() => FocusStyleManager.alwaysShowFocus()} 39 | > 40 | 41 | canDragAndDrop 42 | canDropOnFolder 43 | canReorderItems 44 | dataProvider={ 45 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 46 | ...item, 47 | data, 48 | })) 49 | } 50 | getItemTitle={item => item.data} 51 | viewState={{ 52 | 'tree-1': { 53 | expandedItems: [ 54 | 'Fruit', 55 | 'Meals', 56 | 'America', 57 | 'Europe', 58 | 'Asia', 59 | 'Desserts', 60 | ], 61 | }, 62 | }} 63 | {...renderers} 64 | > 65 | 66 | 67 |
68 | ); 69 | 70 | export const ShortBlueprintJsTree = () => ( 71 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions 72 |
FocusStyleManager.onlyShowFocusOnTabs()} 75 | onKeyDown={() => FocusStyleManager.alwaysShowFocus()} 76 | > 77 | 78 | canDragAndDrop 79 | canDropOnFolder 80 | canReorderItems 81 | dataProvider={ 82 | new StaticTreeDataProvider(shortTree.items, (item, data) => ({ 83 | ...item, 84 | data, 85 | })) 86 | } 87 | getItemTitle={item => item.data} 88 | viewState={{ 89 | 'tree-1': { 90 | expandedItems: ['container'], 91 | }, 92 | }} 93 | {...renderers} 94 | > 95 | 96 | 97 |
98 | ); 99 | 100 | export const BlueprintJsTreeWithClickArrowToExpand = () => ( 101 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions 102 |
FocusStyleManager.onlyShowFocusOnTabs()} 105 | onKeyDown={() => FocusStyleManager.alwaysShowFocus()} 106 | > 107 | 108 | canDragAndDrop 109 | canDropOnFolder 110 | canReorderItems 111 | dataProvider={ 112 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 113 | ...item, 114 | data, 115 | })) 116 | } 117 | getItemTitle={item => item.data} 118 | defaultInteractionMode={InteractionMode.ClickArrowToExpand} 119 | viewState={{ 120 | 'tree-1': {}, 121 | }} 122 | {...renderers} 123 | > 124 | 125 | 126 |
127 | ); 128 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/src/styles.css: -------------------------------------------------------------------------------- 1 | .rct-autodemo-container { 2 | position: relative; 3 | } 4 | 5 | .rct-autodemo-content { 6 | display: flex; 7 | } 8 | 9 | .rct-autodemo-content > * { 10 | flex-grow: 1; 11 | margin: 5px; 12 | } 13 | 14 | .rct-autodemo-controls { 15 | position: absolute; 16 | bottom: 20px; 17 | right: 20px; 18 | font-family: Arial, sans-serif; 19 | font-size: 14px; 20 | background-color: #ffffff; 21 | border: 1px solid #cbcbcb; 22 | border-radius: 8px; 23 | padding: 8px; 24 | display: flex; 25 | color: #444; 26 | opacity: 0.5; 27 | } 28 | 29 | .rct-autodemo-controls:hover { 30 | opacity: 1; 31 | } 32 | 33 | .rct-autodemo-controls-header { 34 | display: flex; 35 | } 36 | 37 | .rct-autodemo-controls h2 { 38 | font-size: 16px; 39 | margin: 0; 40 | } 41 | .rct-autodemo-controls p { 42 | margin: 0; 43 | } 44 | 45 | .rct-autodemo-controls-left { 46 | flex-grow: 1; 47 | align-self: center; 48 | } 49 | .rct-autodemo-controls-right { 50 | align-self: center; 51 | margin-left: 10px; 52 | } 53 | 54 | .rct-autodemo-controls button { 55 | background-color: #ffffff; 56 | color: inherit; 57 | border: 1px solid #cbcbcb; 58 | border-radius: 4px; 59 | padding: 4px 8px; 60 | cursor: pointer; 61 | } 62 | 63 | .rct-autodemo-controls button:hover { 64 | background-color: #ececec; 65 | } 66 | 67 | .rct-autodemo-controls-speed { 68 | margin-left: 8px; 69 | } 70 | 71 | .rct-autodemo-controls-speed button { 72 | border: none; 73 | padding: 0 4px; 74 | } 75 | 76 | .rct-autodemo-controls-speed button.active { 77 | font-weight: bold; 78 | text-decoration: underline; 79 | } 80 | -------------------------------------------------------------------------------- /packages/blueprintjs-renderers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lukasbach/tsconfig/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["src/stories"], 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | README.md -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | babel.config.js 3 | tsconfig.json 4 | node_modules 5 | webpack.config.ts -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | [ 5 | '@babel/preset-react', 6 | { 7 | runtime: 'automatic', 8 | development: process.env.NODE_ENV === 'development', 9 | importSource: '@welldone-software/why-did-you-render', 10 | }, 11 | ], 12 | '@babel/preset-typescript', 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-complex-tree", 3 | "version": "2.6.1", 4 | "main": "lib/cjs/index.js", 5 | "module": "lib/esm/index.js", 6 | "esnext": "lib/esnext/index.js", 7 | "typings": "lib/esm/index.d.ts", 8 | "style": "lib/style-modern.css", 9 | "unpkg": "lib/bundle.js", 10 | "funding": "https://github.com/sponsors/lukasbach", 11 | "sideEffects": [ 12 | "**/*.css" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:lukasbach/react-complex-tree.git", 17 | "directory": "packages/core" 18 | }, 19 | "description": "Unopinionated Accessible Tree Component with Multi-Select and Drag-And-Drop", 20 | "homepage": "https://rct.lukasbach.com/", 21 | "author": "Lukas Bach ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@babel/core": "^7.14.0", 25 | "@babel/preset-env": "^7.14.1", 26 | "@babel/preset-react": "^7.13.13", 27 | "@babel/preset-typescript": "^7.13.0", 28 | "@lukasbach/tsconfig": "^0.1.0", 29 | "@testing-library/jest-dom": "^5.16.5", 30 | "@testing-library/react": "^13.4.0", 31 | "@types/jest": "^27.4.1", 32 | "@types/react": "^18.0.14", 33 | "@types/react-dom": "^18.0.7", 34 | "@welldone-software/why-did-you-render": "^7.0.1", 35 | "babel-jest": "^27.5.1", 36 | "babel-loader": "^9.1.0", 37 | "cpy-cli": "^3.1.1", 38 | "demodata": "^2.6.1", 39 | "jest": "^29.2.2", 40 | "jest-dom": "^4.0.0", 41 | "jest-environment-jsdom": "^29.2.2", 42 | "loader-utils": "^3.2.0", 43 | "npm-run-all": "^4.1.5", 44 | "react": "^18.2.0", 45 | "react-dom": "^18.2.0", 46 | "react-test-renderer": "^18.2.0", 47 | "ts-loader": "^8.3.0", 48 | "ts-node": "^10.7.0", 49 | "typescript": "4.9.3", 50 | "webpack-cli": "^4.7.2" 51 | }, 52 | "peerDependencies": { 53 | "react": ">=16.0.0" 54 | }, 55 | "scripts": { 56 | "build": "run-p \"build:*\"", 57 | "build:cjs": "tsc -m commonjs --outDir lib/cjs", 58 | "build:esm": "tsc -m es2015 --outDir lib/esm", 59 | "build:esnext": "tsc -m esnext --outDir lib/esnext", 60 | "build:bundle": "webpack", 61 | "build:prepare": "cpy ./src/style.css ./lib && cpy ./src/style-modern.css ./lib && cpy ../../README.md ./", 62 | "test": "jest" 63 | }, 64 | "jest": { 65 | "testEnvironment": "jsdom", 66 | "setupFiles": [ 67 | "./test/helpers/setup.ts" 68 | ] 69 | }, 70 | "volta": { 71 | "node": "18.12.1", 72 | "yarn": "3.3.0" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/core/src/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | export interface EventEmitterOptions { 2 | logger?: (log: string, payload?: EventPayload) => void; 3 | } 4 | 5 | export type EventHandler = 6 | | ((payload: EventPayload) => Promise | void) 7 | | null 8 | | undefined; 9 | 10 | export class EventEmitter { 11 | private handlerCount = 0; 12 | 13 | private handlers: Array> = []; 14 | 15 | private options?: EventEmitterOptions; 16 | 17 | constructor(options?: EventEmitterOptions) { 18 | this.options = options; 19 | } 20 | 21 | public get numberOfHandlers() { 22 | return this.handlers.filter(h => !!h).length; 23 | } 24 | 25 | public async emit(payload: EventPayload): Promise { 26 | const promises: Array> = []; 27 | 28 | this.options?.logger?.('emit', payload); 29 | 30 | for (const handler of this.handlers) { 31 | if (handler) { 32 | const res = handler(payload) as Promise; 33 | if (typeof res?.then === 'function') { 34 | promises.push(res); 35 | } 36 | } 37 | } 38 | 39 | await Promise.all(promises); 40 | } 41 | 42 | public on(handler: EventHandler): number { 43 | this.options?.logger?.('on'); 44 | this.handlers.push(handler); 45 | // eslint-disable-next-line no-plusplus 46 | return this.handlerCount++; 47 | } 48 | 49 | public off(handlerId: number) { 50 | this.delete(handlerId); 51 | } 52 | 53 | public delete(handlerId: number) { 54 | this.options?.logger?.('off'); 55 | this.handlers[handlerId] = null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/core/src/EventListenerManager.ts: -------------------------------------------------------------------------------- 1 | type AbstractEventMap = { [eventName: string]: any }; 2 | type Listener = ( 3 | event: M[K] 4 | ) => any; 5 | 6 | export class EventListenerManager { 7 | private handlers: { [eventName: string]: Listener[] } = {}; 8 | 9 | public addEventListener( 10 | type: K, 11 | listener: Listener 12 | ) { 13 | if (!this.handlers[type]) { 14 | this.handlers[type] = []; 15 | } 16 | 17 | this.handlers[type].push(listener); 18 | } 19 | 20 | public removeEventListener( 21 | type: K, 22 | listener: Listener 23 | ) { 24 | const idx = this.handlers[type].indexOf(listener); 25 | if (idx >= 0) { 26 | this.handlers[type].splice(idx, 1); 27 | } 28 | } 29 | 30 | public emitEvent(type: K, payload: M[K]) { 31 | for (const handler of this.handlers[type]) { 32 | handler(payload); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/controlledEnvironment/ControlledTreeEnvironment.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useContext, useEffect } from 'react'; 3 | import { 4 | ControlledTreeEnvironmentProps, 5 | TreeEnvironmentContextProps, 6 | TreeEnvironmentRef, 7 | } from '../types'; 8 | import { InteractionManagerProvider } from './InteractionManagerProvider'; 9 | import { DragAndDropProvider } from '../drag/DragAndDropProvider'; 10 | import { EnvironmentActionsProvider } from '../environmentActions/EnvironmentActionsProvider'; 11 | import { useControlledTreeEnvironmentProps } from './useControlledTreeEnvironmentProps'; 12 | 13 | const TreeEnvironmentContext = React.createContext( 14 | null as any 15 | ); 16 | export const useTreeEnvironment = () => useContext(TreeEnvironmentContext); 17 | 18 | export const ControlledTreeEnvironment = React.forwardRef< 19 | TreeEnvironmentRef, 20 | ControlledTreeEnvironmentProps 21 | >((props, ref) => { 22 | const environmentContextProps = useControlledTreeEnvironmentProps(props); 23 | 24 | const { viewState, onFocusItem } = props; 25 | 26 | // Make sure that every tree view state has a focused item 27 | useEffect(() => { 28 | for (const treeId of Object.keys(environmentContextProps.trees)) { 29 | const firstItemIndex = 30 | props.items[environmentContextProps.trees[treeId].rootItem] 31 | ?.children?.[0]; 32 | const firstItem = firstItemIndex && props.items[firstItemIndex]; 33 | if ( 34 | !viewState[treeId]?.focusedItem && 35 | environmentContextProps.trees[treeId] && 36 | firstItem 37 | ) { 38 | onFocusItem?.(firstItem, treeId, false); 39 | } 40 | } 41 | }, [environmentContextProps.trees, onFocusItem, props.items, viewState]); 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | {props.children} 49 | 50 | 51 | 52 | 53 | ); 54 | }) as ( 55 | p: ControlledTreeEnvironmentProps & { 56 | ref?: React.Ref>; 57 | } 58 | ) => React.ReactElement; 59 | -------------------------------------------------------------------------------- /packages/core/src/controlledEnvironment/InteractionManagerProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useMemo } from 'react'; 3 | import { InteractionManager, InteractionMode } from '../types'; 4 | import { useTreeEnvironment } from './ControlledTreeEnvironment'; 5 | import { mergeInteractionManagers } from './mergeInteractionManagers'; 6 | import { buildInteractionMode } from './buildInteractionMode'; 7 | 8 | const InteractionManagerContext = React.createContext>( 9 | null as any 10 | ); 11 | export const useInteractionManager = () => 12 | React.useContext(InteractionManagerContext); 13 | 14 | export const InteractionManagerProvider: React.FC = ({ 15 | children, 16 | }) => { 17 | const environment = useTreeEnvironment(); 18 | const { defaultInteractionMode } = environment; 19 | 20 | const interactionManager = useMemo(() => { 21 | if (defaultInteractionMode && typeof defaultInteractionMode !== 'string') { 22 | if (defaultInteractionMode.extends) { 23 | return mergeInteractionManagers( 24 | defaultInteractionMode, 25 | buildInteractionMode(defaultInteractionMode.extends, environment) 26 | ); 27 | } 28 | return defaultInteractionMode; 29 | } 30 | 31 | return buildInteractionMode( 32 | (defaultInteractionMode as InteractionMode) ?? 33 | InteractionMode.ClickItemToExpand, 34 | environment 35 | ); 36 | // eslint-disable-next-line react-hooks/exhaustive-deps 37 | }, []); // TODO make sure that environment does not need to be refreshed 38 | 39 | return ( 40 | 41 | {children} 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/core/src/controlledEnvironment/buildInteractionMode.ts: -------------------------------------------------------------------------------- 1 | import { InteractionMode, TreeEnvironmentContextProps } from '../types'; 2 | import { DoubleClickItemToExpandInteractionManager } from '../interactionMode/DoubleClickItemToExpandInteractionManager'; 3 | import { ClickItemToExpandInteractionManager } from '../interactionMode/ClickItemToExpandInteractionManager'; 4 | import { ClickArrowToExpandInteractionManager } from '../interactionMode/ClickArrowToExpandInteractionManager'; 5 | 6 | export const buildInteractionMode = ( 7 | mode: InteractionMode, 8 | environment: TreeEnvironmentContextProps 9 | ) => { 10 | switch (mode) { 11 | case InteractionMode.DoubleClickItemToExpand: 12 | return new DoubleClickItemToExpandInteractionManager(environment); 13 | case InteractionMode.ClickItemToExpand: 14 | return new ClickItemToExpandInteractionManager(environment); 15 | case InteractionMode.ClickArrowToExpand: 16 | return new ClickArrowToExpandInteractionManager(environment); 17 | default: 18 | throw Error(`Unknown interaction mode ${mode}`); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/core/src/controlledEnvironment/layoutUtils.ts: -------------------------------------------------------------------------------- 1 | import { getDocument } from '../utils'; 2 | 3 | export const computeItemHeight = (treeId: string) => { 4 | const firstItem = getDocument()?.querySelector( 5 | `[data-rct-tree="${treeId}"] [data-rct-item-container="true"]` 6 | ); 7 | if (firstItem) { 8 | const style = getComputedStyle(firstItem); 9 | // top margin flows into the bottom margin of the previous item, so ignore it 10 | return ( 11 | firstItem.offsetHeight + 12 | Math.max(parseFloat(style.marginTop), parseFloat(style.marginBottom)) 13 | ); 14 | } 15 | return 5; 16 | }; 17 | 18 | export const isOutsideOfContainer = (e: DragEvent, treeBb: DOMRect) => 19 | e.clientX <= treeBb.left || 20 | e.clientX >= treeBb.right || 21 | e.clientY <= treeBb.top || 22 | e.clientY >= treeBb.bottom; 23 | -------------------------------------------------------------------------------- /packages/core/src/controlledEnvironment/mergeInteractionManagers.ts: -------------------------------------------------------------------------------- 1 | import { InteractionManager } from '../types'; 2 | 3 | export const mergeInteractionManagers = ( 4 | main: InteractionManager, 5 | fallback: InteractionManager 6 | ): InteractionManager => ({ 7 | mode: main.mode, 8 | createInteractiveElementProps: (item, treeId, actions, renderFlags) => ({ 9 | ...fallback.createInteractiveElementProps( 10 | item, 11 | treeId, 12 | actions, 13 | renderFlags 14 | ), 15 | ...main.createInteractiveElementProps(item, treeId, actions, renderFlags), 16 | }), 17 | }); 18 | -------------------------------------------------------------------------------- /packages/core/src/controlledEnvironment/useLinearItems.ts: -------------------------------------------------------------------------------- 1 | import { useTreeEnvironment } from './ControlledTreeEnvironment'; 2 | 3 | export const useLinearItems = (treeId: string) => 4 | useTreeEnvironment().linearItems[treeId]; 5 | -------------------------------------------------------------------------------- /packages/core/src/drag/useCanDropAt.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { DraggingPosition, TreeItem } from '../types'; 3 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 4 | 5 | export const useCanDropAt = () => { 6 | const environment = useTreeEnvironment(); 7 | 8 | return useCallback( 9 | (draggingPosition: DraggingPosition, draggingItems: TreeItem[]) => { 10 | if (draggingPosition.targetType === 'between-items') { 11 | if (!environment.canReorderItems) { 12 | return false; 13 | } 14 | } else if (draggingPosition.targetType === 'root') { 15 | if (!environment.canDropOnFolder) { 16 | return false; 17 | } 18 | } else { 19 | const resolvedItem = environment.items[draggingPosition.targetItem]; 20 | if ( 21 | !resolvedItem || 22 | (!environment.canDropOnFolder && resolvedItem.isFolder) || 23 | (!environment.canDropOnNonFolder && !resolvedItem.isFolder) || 24 | draggingItems.some( 25 | draggingItem => draggingItem.index === draggingPosition.targetItem 26 | ) 27 | ) { 28 | return false; 29 | } 30 | } 31 | 32 | if ( 33 | environment.canDropAt && 34 | (!draggingItems || 35 | !environment.canDropAt(draggingItems, draggingPosition)) 36 | ) { 37 | // setDraggingPosition(undefined); 38 | return false; 39 | } 40 | 41 | return true; 42 | }, 43 | [environment] 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/src/drag/useGetParentOfLinearItem.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 3 | 4 | export const useGetGetParentOfLinearItem = () => { 5 | const environment = useTreeEnvironment(); 6 | 7 | return useCallback( 8 | (itemLinearIndex: number, treeId: string) => { 9 | const linearItems = environment.linearItems[treeId]; 10 | const { depth } = linearItems[itemLinearIndex]; 11 | let parentLinearIndex = itemLinearIndex; 12 | for ( 13 | ; 14 | !!linearItems[parentLinearIndex] && 15 | linearItems[parentLinearIndex].depth !== depth - 1; 16 | parentLinearIndex -= 1 17 | ); 18 | let parent = linearItems[parentLinearIndex]; 19 | 20 | if (!parent) { 21 | parent = { item: environment.trees[treeId].rootItem, depth: 0 }; 22 | parentLinearIndex = 0; 23 | } 24 | 25 | return { parent, parentLinearIndex }; 26 | }, 27 | [environment.linearItems, environment.trees] 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/core/src/drag/useGetViableDragPositions.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-continue */ 2 | import { useCallback } from 'react'; 3 | import { DraggingPosition, TreeItem } from '../types'; 4 | import { useGetGetParentOfLinearItem } from './useGetParentOfLinearItem'; 5 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 6 | import { useCanDropAt } from './useCanDropAt'; 7 | 8 | export const useGetViableDragPositions = () => { 9 | const environment = useTreeEnvironment(); 10 | const getParentOfLinearItem = useGetGetParentOfLinearItem(); 11 | const canDropAt = useCanDropAt(); 12 | 13 | const isDescendant = useCallback( 14 | (treeId: string, itemLinearIndex: number, potentialParents: TreeItem[]) => { 15 | // based on DraggingPositionEvaluation.isDescendant() 16 | const { parent, parentLinearIndex } = getParentOfLinearItem( 17 | itemLinearIndex, 18 | treeId 19 | ); 20 | if (potentialParents.some(p => p.index === parent.item)) return true; 21 | if (parent.depth === 0) return false; 22 | return isDescendant(treeId, parentLinearIndex, potentialParents); 23 | }, 24 | [getParentOfLinearItem] 25 | ); 26 | 27 | return useCallback( 28 | (treeId: string, draggingItems: TreeItem[]) => { 29 | const linearItems = environment.linearItems[treeId]; 30 | const targets: DraggingPosition[] = []; 31 | let skipUntilDepthIsLowerThan = -1; 32 | 33 | for ( 34 | let linearIndex = 0; 35 | linearIndex < linearItems.length; 36 | // eslint-disable-next-line no-plusplus 37 | linearIndex++ 38 | ) { 39 | const { item, depth } = linearItems[linearIndex]; 40 | 41 | if ( 42 | skipUntilDepthIsLowerThan !== -1 && 43 | depth > skipUntilDepthIsLowerThan 44 | ) { 45 | continue; 46 | } else { 47 | skipUntilDepthIsLowerThan = -1; 48 | } 49 | 50 | const { parent } = getParentOfLinearItem(linearIndex, treeId); 51 | const childIndex = 52 | environment.items[parent.item].children!.indexOf(item); 53 | 54 | if (isDescendant(treeId, linearIndex, draggingItems)) { 55 | skipUntilDepthIsLowerThan = depth + 1; 56 | continue; 57 | } 58 | 59 | const itemPosition: DraggingPosition = { 60 | targetType: 'item', 61 | parentItem: parent.item, 62 | targetItem: item, 63 | linearIndex, 64 | depth, 65 | treeId, 66 | }; 67 | 68 | const topPosition: DraggingPosition = { 69 | targetType: 'between-items', 70 | parentItem: parent.item, 71 | linePosition: 'top', 72 | childIndex, 73 | depth, 74 | treeId, 75 | linearIndex, 76 | }; 77 | 78 | const bottomPosition: DraggingPosition = { 79 | targetType: 'between-items', 80 | parentItem: parent.item, 81 | linePosition: 'bottom', 82 | linearIndex: linearIndex + 1, 83 | childIndex: childIndex + 1, 84 | depth, 85 | treeId, 86 | }; 87 | 88 | const depthOfItemAbove = linearItems[linearIndex - 1]?.depth ?? -1; 89 | const depthOfItemBelow = linearItems[linearIndex + 1]?.depth ?? -1; 90 | const isWithinFolder = depth === depthOfItemAbove; 91 | const isBelowOpenFolder = depth === depthOfItemBelow - 1; 92 | 93 | if (!isWithinFolder && canDropAt(topPosition, draggingItems)) { 94 | targets.push(topPosition); 95 | } 96 | if (canDropAt(itemPosition, draggingItems)) { 97 | targets.push(itemPosition); 98 | } 99 | if (!isBelowOpenFolder && canDropAt(bottomPosition, draggingItems)) { 100 | targets.push(bottomPosition); 101 | } 102 | } 103 | 104 | return targets; 105 | }, 106 | [ 107 | canDropAt, 108 | environment.items, 109 | environment.linearItems, 110 | getParentOfLinearItem, 111 | isDescendant, 112 | ] 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /packages/core/src/environmentActions/useCreatedEnvironmentRef.ts: -------------------------------------------------------------------------------- 1 | import { Ref, useImperativeHandle } from 'react'; 2 | import { TreeEnvironmentChangeActions, TreeEnvironmentRef } from '../types'; 3 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 4 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 5 | 6 | export const useCreatedEnvironmentRef = ( 7 | ref: Ref, 8 | actions: TreeEnvironmentChangeActions 9 | ) => { 10 | const environment = useTreeEnvironment(); 11 | const dnd = useDragAndDrop(); 12 | 13 | useImperativeHandle(ref, () => ({ 14 | ...actions, 15 | ...environment, 16 | treeEnvironmentContext: environment, 17 | dragAndDropContext: dnd, 18 | })); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/core/src/hotkeys/defaultKeyboardBindings.ts: -------------------------------------------------------------------------------- 1 | import { KeyboardBindings } from '../types'; 2 | 3 | export const defaultKeyboardBindings: Required = { 4 | expandSiblings: ['control+*'], 5 | moveFocusToFirstItem: ['home'], 6 | moveFocusToLastItem: ['end'], 7 | primaryAction: ['enter'], 8 | renameItem: ['f2'], 9 | abortRenameItem: ['escape'], 10 | toggleSelectItem: ['control+ '], 11 | abortSearch: ['escape', 'enter'], 12 | startSearch: [], 13 | selectAll: ['control+a'], 14 | startProgrammaticDnd: ['control+shift+d'], 15 | completeProgrammaticDnd: ['enter'], 16 | abortProgrammaticDnd: ['escape'], 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/src/hotkeys/useHotkey.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react'; 2 | import { useHtmlElementEventListener } from '../useHtmlElementEventListener'; 3 | import { KeyboardBindings } from '../types'; 4 | import { useKeyboardBindings } from './useKeyboardBindings'; 5 | import { useCallSoon } from '../useCallSoon'; 6 | import { getDocument } from '../utils'; 7 | 8 | const elementsThatCanTakeText = ['input', 'textarea']; 9 | 10 | export const useHotkey = ( 11 | combinationName: keyof KeyboardBindings, 12 | onHit: (e: KeyboardEvent) => void, 13 | active?: boolean, 14 | activatableWhileFocusingInput = false 15 | ) => { 16 | const pressedKeys = useRef([]); 17 | const keyboardBindings = useKeyboardBindings(); 18 | const callSoon = useCallSoon(); 19 | 20 | const possibleCombinations = useMemo( 21 | () => 22 | keyboardBindings[combinationName].map(combination => 23 | combination.split('+') 24 | ), 25 | [combinationName, keyboardBindings] 26 | ); 27 | 28 | useHtmlElementEventListener(getDocument(), 'keydown', e => { 29 | if (active === false) { 30 | return; 31 | } 32 | 33 | if ( 34 | (elementsThatCanTakeText.includes( 35 | (e.target as HTMLElement).tagName?.toLowerCase() 36 | ) || 37 | (e.target as HTMLElement).isContentEditable) && 38 | !activatableWhileFocusingInput 39 | ) { 40 | // Skip if an input is selected 41 | return; 42 | } 43 | 44 | if (!pressedKeys.current.includes(e.key)) { 45 | pressedKeys.current.push(e.key); 46 | const pressedKeysLowercase = pressedKeys.current.map(key => 47 | key.toLowerCase() 48 | ); 49 | 50 | const partialMatch = possibleCombinations 51 | .map(combination => 52 | pressedKeysLowercase 53 | .map(key => combination.includes(key.toLowerCase())) 54 | .reduce((a, b) => a && b, true) 55 | ) 56 | .reduce((a, b) => a || b, false); 57 | 58 | if (partialMatch) { 59 | if (pressedKeys.current.length > 1 || !/^[a-zA-Z\s]$/.test(e.key)) { 60 | // Prevent default, but not if this is the first input and a letter (which should trigger a search) 61 | // also not on first input and spacebar, as that should trigger an item directly 62 | e.preventDefault(); 63 | } 64 | } 65 | } 66 | }); 67 | 68 | useHtmlElementEventListener(getDocument(), 'keyup', e => { 69 | if (active === false) { 70 | return; 71 | } 72 | 73 | const pressedKeysLowercase = pressedKeys.current.map(key => 74 | key.toLowerCase() 75 | ); 76 | const match = possibleCombinations 77 | .map(combination => 78 | combination 79 | .map(key => pressedKeysLowercase.includes(key.toLowerCase())) 80 | .reduce((a, b) => a && b, true) 81 | ) 82 | .reduce((a, b) => a || b, false); 83 | 84 | if (match) { 85 | callSoon(() => onHit(e)); 86 | } 87 | 88 | pressedKeys.current = pressedKeys.current.filter(key => key !== e.key); 89 | }); 90 | }; 91 | -------------------------------------------------------------------------------- /packages/core/src/hotkeys/useKey.ts: -------------------------------------------------------------------------------- 1 | import { useHtmlElementEventListener } from '../useHtmlElementEventListener'; 2 | import { getDocument } from '../utils'; 3 | 4 | export const useKey = ( 5 | key: string, 6 | onHit: (e: KeyboardEvent) => void, 7 | active?: boolean 8 | ) => { 9 | useHtmlElementEventListener(getDocument(), 'keydown', e => { 10 | if (!active) { 11 | return; 12 | } 13 | 14 | if (active && key.toLowerCase() === e.key.toLowerCase()) { 15 | onHit(e); 16 | } 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/core/src/hotkeys/useKeyboardBindings.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 3 | import { defaultKeyboardBindings } from './defaultKeyboardBindings'; 4 | 5 | export const useKeyboardBindings = () => { 6 | const environment = useTreeEnvironment(); 7 | 8 | return useMemo(() => { 9 | if (environment.keyboardBindings) { 10 | return { 11 | ...defaultKeyboardBindings, 12 | ...environment.keyboardBindings, 13 | }; 14 | } 15 | return defaultKeyboardBindings; 16 | }, [environment.keyboardBindings]); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { TreeItemElement } from './treeItem/TreeItemElement'; 2 | import { TreeItemChildren } from './treeItem/TreeItemChildren'; 3 | 4 | export * from './controlledEnvironment/ControlledTreeEnvironment'; 5 | export * from './tree/Tree'; 6 | export * from './uncontrolledEnvironment/UncontrolledTreeEnvironment'; 7 | export * from './uncontrolledEnvironment/StaticTreeDataProvider'; 8 | export * from './types'; 9 | export * from './renderers'; 10 | export * from './treeItem/useTreeItemRenderContext'; 11 | export * from './controlledEnvironment/useControlledTreeEnvironmentProps'; 12 | 13 | export const INTERNALS = { 14 | TreeItemElement, 15 | TreeItemChildren, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/src/interactionMode/ClickArrowToExpandInteractionManager.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from 'react'; 2 | import { 3 | InteractionMode, 4 | InteractionManager, 5 | TreeEnvironmentContextProps, 6 | TreeItem, 7 | TreeItemActions, 8 | TreeItemRenderFlags, 9 | } from '../types'; 10 | import { isControlKey } from '../isControlKey'; 11 | 12 | export class ClickArrowToExpandInteractionManager 13 | implements InteractionManager 14 | { 15 | public readonly mode = InteractionMode.ClickItemToExpand; 16 | 17 | private environment: TreeEnvironmentContextProps; 18 | 19 | constructor(environment: TreeEnvironmentContextProps) { 20 | this.environment = environment; 21 | } 22 | 23 | createInteractiveElementProps( 24 | item: TreeItem, 25 | treeId: string, 26 | actions: TreeItemActions, 27 | renderFlags: TreeItemRenderFlags 28 | ): HTMLProps { 29 | return { 30 | onClick: e => { 31 | const isSpacebarEvent = e.detail === 0; 32 | actions.focusItem(); 33 | if (e.shiftKey && !isSpacebarEvent) { 34 | actions.selectUpTo(!isControlKey(e)); 35 | } else if (isControlKey(e) && !isSpacebarEvent) { 36 | if (renderFlags.isSelected) { 37 | actions.unselectItem(); 38 | } else { 39 | actions.addToSelectedItems(); 40 | } 41 | } else { 42 | actions.selectItem(); 43 | if ( 44 | !item.isFolder || 45 | this.environment.canInvokePrimaryActionOnItemContainer 46 | ) { 47 | actions.primaryAction(); 48 | } 49 | } 50 | }, 51 | onFocus: () => { 52 | actions.focusItem(); 53 | }, 54 | onDragStart: e => { 55 | e.dataTransfer.dropEffect = 'move'; 56 | actions.startDragging(); 57 | }, 58 | onDragOver: e => { 59 | e.preventDefault(); // Allow drop 60 | }, 61 | draggable: renderFlags.canDrag && !renderFlags.isRenaming, 62 | tabIndex: !renderFlags.isRenaming 63 | ? renderFlags.isFocused 64 | ? 0 65 | : -1 66 | : undefined, 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/core/src/interactionMode/ClickItemToExpandInteractionManager.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from 'react'; 2 | import { 3 | InteractionMode, 4 | InteractionManager, 5 | TreeEnvironmentContextProps, 6 | TreeItem, 7 | TreeItemActions, 8 | TreeItemRenderFlags, 9 | } from '../types'; 10 | import { isControlKey } from '../isControlKey'; 11 | 12 | export class ClickItemToExpandInteractionManager implements InteractionManager { 13 | public readonly mode = InteractionMode.ClickItemToExpand; 14 | 15 | private environment: TreeEnvironmentContextProps; 16 | 17 | constructor(environment: TreeEnvironmentContextProps) { 18 | this.environment = environment; 19 | } 20 | 21 | createInteractiveElementProps( 22 | item: TreeItem, 23 | treeId: string, 24 | actions: TreeItemActions, 25 | renderFlags: TreeItemRenderFlags 26 | ): HTMLProps { 27 | return { 28 | onClick: e => { 29 | const isSpacebarEvent = e.detail === 0; 30 | actions.focusItem(); 31 | if (e.shiftKey && !isSpacebarEvent) { 32 | actions.selectUpTo(!isControlKey(e)); 33 | } else if (isControlKey(e) && !isSpacebarEvent) { 34 | if (renderFlags.isSelected) { 35 | actions.unselectItem(); 36 | } else { 37 | actions.addToSelectedItems(); 38 | } 39 | } else { 40 | if (item.isFolder) { 41 | actions.toggleExpandedState(); 42 | } 43 | actions.selectItem(); 44 | 45 | if ( 46 | !item.isFolder || 47 | this.environment.canInvokePrimaryActionOnItemContainer 48 | ) { 49 | actions.primaryAction(); 50 | } 51 | } 52 | }, 53 | onFocus: () => { 54 | actions.focusItem(); 55 | }, 56 | onDragStart: e => { 57 | e.dataTransfer.dropEffect = 'move'; 58 | actions.startDragging(); 59 | }, 60 | onDragOver: e => { 61 | e.preventDefault(); // Allow drop 62 | }, 63 | draggable: renderFlags.canDrag && !renderFlags.isRenaming, 64 | tabIndex: !renderFlags.isRenaming 65 | ? renderFlags.isFocused 66 | ? 0 67 | : -1 68 | : undefined, 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/core/src/interactionMode/DoubleClickItemToExpandInteractionManager.ts: -------------------------------------------------------------------------------- 1 | import { HTMLProps } from 'react'; 2 | import { 3 | InteractionMode, 4 | InteractionManager, 5 | TreeEnvironmentContextProps, 6 | TreeItem, 7 | TreeItemActions, 8 | TreeItemRenderFlags, 9 | } from '../types'; 10 | import { isControlKey } from '../isControlKey'; 11 | 12 | export class DoubleClickItemToExpandInteractionManager 13 | implements InteractionManager 14 | { 15 | public readonly mode = InteractionMode.DoubleClickItemToExpand; 16 | 17 | private environment: TreeEnvironmentContextProps; 18 | 19 | constructor(environment: TreeEnvironmentContextProps) { 20 | this.environment = environment; 21 | } 22 | 23 | createInteractiveElementProps( 24 | item: TreeItem, 25 | treeId: string, 26 | actions: TreeItemActions, 27 | renderFlags: TreeItemRenderFlags 28 | ): HTMLProps { 29 | return { 30 | onClick: e => { 31 | const isSpacebarEvent = e.detail === 0; 32 | actions.focusItem(); 33 | if (e.shiftKey && !isSpacebarEvent) { 34 | actions.selectUpTo(!isControlKey(e)); 35 | } else if (isControlKey(e) && !isSpacebarEvent) { 36 | if (renderFlags.isSelected) { 37 | actions.unselectItem(); 38 | } else { 39 | actions.addToSelectedItems(); 40 | } 41 | } else { 42 | actions.selectItem(); 43 | } 44 | }, 45 | onDoubleClick: () => { 46 | actions.focusItem(); 47 | actions.selectItem(); 48 | 49 | if (item.isFolder) { 50 | actions.toggleExpandedState(); 51 | } 52 | 53 | if ( 54 | !item.isFolder || 55 | this.environment.canInvokePrimaryActionOnItemContainer 56 | ) { 57 | actions.primaryAction(); 58 | } 59 | }, 60 | onFocus: () => { 61 | actions.focusItem(); 62 | }, 63 | onDragStart: e => { 64 | e.dataTransfer.dropEffect = 'move'; 65 | actions.startDragging(); 66 | }, 67 | onDragOver: e => { 68 | e.preventDefault(); // Allow drop 69 | }, 70 | draggable: renderFlags.canDrag && !renderFlags.isRenaming, 71 | tabIndex: !renderFlags.isRenaming 72 | ? renderFlags.isFocused 73 | ? 0 74 | : -1 75 | : undefined, 76 | }; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/core/src/isControlKey.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const isControlKey = (e: React.MouseEvent) => 4 | e.ctrlKey || 5 | (navigator.platform.toUpperCase().indexOf('MAC') >= 0 && e.metaKey); 6 | -------------------------------------------------------------------------------- /packages/core/src/renderers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createDefaultRenderers'; 2 | -------------------------------------------------------------------------------- /packages/core/src/renderers/useRenderers.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { AllTreeRenderProps, TreeRenderProps } from '../types'; 3 | import { createDefaultRenderers } from './createDefaultRenderers'; 4 | 5 | export const useRenderers = ({ 6 | renderItem, 7 | renderItemTitle, 8 | renderItemArrow, 9 | renderRenameInput, 10 | renderItemsContainer, 11 | renderTreeContainer, 12 | renderDragBetweenLine, 13 | renderSearchInput, 14 | renderLiveDescriptorContainer, 15 | renderDepthOffset, 16 | }: TreeRenderProps) => { 17 | const defaultRenderers = useMemo( 18 | () => createDefaultRenderers(renderDepthOffset ?? 10), 19 | [renderDepthOffset] 20 | ); 21 | 22 | const customRenderers: TreeRenderProps = { 23 | renderItem, 24 | renderItemTitle, 25 | renderItemArrow, 26 | renderRenameInput, 27 | renderItemsContainer, 28 | renderTreeContainer, 29 | renderDragBetweenLine, 30 | renderSearchInput, 31 | renderLiveDescriptorContainer, 32 | renderDepthOffset, 33 | }; 34 | 35 | const renderers = Object.entries(defaultRenderers).reduce( 36 | (acc, [key, value]) => { 37 | const keyMapped = key as keyof AllTreeRenderProps; 38 | if (customRenderers[keyMapped]) { 39 | acc[keyMapped] = customRenderers[keyMapped] as any; 40 | } else { 41 | acc[keyMapped] = value as any; 42 | } 43 | return acc; 44 | }, 45 | {} as AllTreeRenderProps 46 | ); 47 | 48 | (renderers.renderItem as any).displayName = 'RenderItem'; 49 | (renderers.renderItemTitle as any).displayName = 'RenderItemTitle'; 50 | (renderers.renderItemArrow as any).displayName = 'RenderItemArrow'; 51 | (renderers.renderRenameInput as any).displayName = 'RenderRenameInput'; 52 | (renderers.renderItemsContainer as any).displayName = 'RenderItemsContainer'; 53 | (renderers.renderTreeContainer as any).displayName = 'RenderTreeContainer'; 54 | (renderers.renderDragBetweenLine as any).displayName = 55 | 'RenderDragBetweenLine'; 56 | (renderers.renderSearchInput as any).displayName = 'RenderSearchInput'; 57 | 58 | return renderers; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/core/src/search/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useHtmlElementEventListener } from '../useHtmlElementEventListener'; 3 | import { useHotkey } from '../hotkeys/useHotkey'; 4 | import { useTree } from '../tree/Tree'; 5 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 6 | import { useSearchMatchFocus } from './useSearchMatchFocus'; 7 | import { useViewState } from '../tree/useViewState'; 8 | import { useCallSoon } from '../useCallSoon'; 9 | import { getDocument } from '../utils'; 10 | 11 | export const SearchInput: React.FC<{ 12 | containerRef?: HTMLElement; 13 | }> = ({ containerRef }) => { 14 | const { search, setSearch, treeId, renderers, renamingItem } = useTree(); 15 | const environment = useTreeEnvironment(); 16 | useViewState(); 17 | const isActiveTree = environment.activeTreeId === treeId; 18 | const callSoon = useCallSoon(); 19 | 20 | useSearchMatchFocus(); 21 | 22 | const clearSearch = () => { 23 | setSearch(null); 24 | 25 | if (environment.autoFocus ?? true) { 26 | // Refocus item in tree 27 | // TODO move logic as reusable method into tree or tree environment 28 | const focusItem = getDocument()?.querySelector( 29 | `[data-rct-tree="${treeId}"] [data-rct-item-focus="true"]` 30 | ); 31 | (focusItem as HTMLElement)?.focus?.(); 32 | } 33 | }; 34 | 35 | useHotkey( 36 | 'abortSearch', 37 | () => { 38 | // Without the callSoon, hitting enter to abort 39 | // and then moving focus weirdly moves the selected item along 40 | // with the focused item. 41 | callSoon(() => { 42 | clearSearch(); 43 | }); 44 | }, 45 | isActiveTree && search !== null, 46 | true 47 | ); 48 | 49 | useHtmlElementEventListener(containerRef, 'keydown', e => { 50 | const unicode = e.key.charCodeAt(0); 51 | if ( 52 | (environment.canSearch ?? true) && 53 | (environment.canSearchByStartingTyping ?? true) && 54 | isActiveTree && 55 | search === null && 56 | !renamingItem && 57 | !e.ctrlKey && 58 | !e.shiftKey && 59 | !e.altKey && 60 | !e.metaKey && 61 | ((unicode >= 48 && unicode <= 57) || // number 62 | // (unicode >= 65 && unicode <= 90) || // uppercase letter 63 | (unicode >= 97 && unicode <= 122)) // lowercase letter 64 | ) { 65 | setSearch(''); 66 | } 67 | }); 68 | 69 | if (!(environment.canSearch ?? true) || search === null) { 70 | return null; 71 | } 72 | 73 | return renderers.renderSearchInput({ 74 | inputProps: { 75 | value: search, 76 | onChange: (e: any) => setSearch(e.target.value), 77 | onBlur: () => { 78 | clearSearch(); 79 | }, 80 | ref: el => { 81 | el?.focus?.(); 82 | }, 83 | 'aria-label': 'Search for items', // TODO 84 | ...({ 85 | 'data-rct-search-input': 'true', 86 | } as any), 87 | }, 88 | }) as any; 89 | }; 90 | -------------------------------------------------------------------------------- /packages/core/src/search/defaultMatcher.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from '../types'; 2 | 3 | export const defaultMatcher: ( 4 | search: string, 5 | item: TreeItem, 6 | itemTitle: string 7 | ) => boolean = (search, item, itemTitle) => 8 | itemTitle.toLowerCase().includes(search.toLowerCase()); 9 | -------------------------------------------------------------------------------- /packages/core/src/search/useSearchMatchFocus.ts: -------------------------------------------------------------------------------- 1 | import { useTree } from '../tree/Tree'; 2 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 3 | import { defaultMatcher } from './defaultMatcher'; 4 | import { useSideEffect } from '../useSideEffect'; 5 | import { useLinearItems } from '../controlledEnvironment/useLinearItems'; 6 | import { useCallSoon } from '../useCallSoon'; 7 | 8 | export const useSearchMatchFocus = () => { 9 | const { doesSearchMatchItem, items, getItemTitle, onFocusItem } = 10 | useTreeEnvironment(); 11 | const { search, treeId } = useTree(); 12 | const linearItems = useLinearItems(treeId); 13 | const callSoon = useCallSoon(); 14 | 15 | useSideEffect( 16 | () => { 17 | if (search && search.length > 0) { 18 | callSoon(() => { 19 | const focusItem = linearItems.find(({ item }) => 20 | (doesSearchMatchItem ?? defaultMatcher)( 21 | search, 22 | items[item], 23 | getItemTitle(items[item]) 24 | ) 25 | ); 26 | 27 | if (focusItem) { 28 | onFocusItem?.(items[focusItem.item], treeId); 29 | } 30 | }); 31 | } 32 | }, 33 | [ 34 | doesSearchMatchItem, 35 | getItemTitle, 36 | linearItems, 37 | items, 38 | onFocusItem, 39 | search, 40 | treeId, 41 | callSoon, 42 | ], 43 | [search] 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/src/stories/363.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React, { useState } from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { Tree } from '../tree/Tree'; 5 | import { ControlledTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 6 | import { TreeItemIndex } from '../types'; 7 | 8 | export default { 9 | title: 'Core/Issue Report Reproduction', 10 | } as Meta; 11 | 12 | export const Issue363 = () => { 13 | const [focusedItem, setFocusedItem] = useState(); 14 | const [expandedItems, setExpandedItems] = useState([]); 15 | const [selectedItems, setSelectedItems] = useState([]); 16 | return ( 17 | 18 | canDragAndDrop 19 | canDropOnFolder 20 | canReorderItems 21 | items={longTree.items} 22 | getItemTitle={item => item.data} 23 | onFocusItem={item => { 24 | setFocusedItem(item.index); 25 | console.log(`Focused item: ${item.index}`); 26 | }} 27 | onSelectItems={items => { 28 | setSelectedItems(items); 29 | }} 30 | onExpandItem={item => { 31 | setExpandedItems([...expandedItems, item.index]); 32 | }} 33 | onCollapseItem={item => { 34 | setExpandedItems(expandedItems.filter(i => i !== item.index)); 35 | }} 36 | viewState={{ 37 | 'tree-1': { 38 | focusedItem, 39 | expandedItems, 40 | selectedItems, 41 | }, 42 | }} 43 | > 44 | 45 | 46 |
47 |         {JSON.stringify(
48 |           {
49 |             focusedItem,
50 |             expandedItems,
51 |             selectedItems,
52 |           },
53 |           null,
54 |           2
55 |         )}
56 |       
57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/core/src/stories/ControlledEnvironment.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { Tree } from '../tree/Tree'; 5 | import { ControlledTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 6 | 7 | export default { 8 | title: 'Core/Controlled Environment', 9 | } as Meta; 10 | 11 | export const StaticState = () => ( 12 | 13 | canDragAndDrop 14 | canDropOnFolder 15 | canReorderItems 16 | items={longTree.items} 17 | getItemTitle={item => item.data} 18 | viewState={{}} 19 | > 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /packages/core/src/stories/CustomViewState.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 5 | import { StaticTreeDataProvider } from '../uncontrolledEnvironment/StaticTreeDataProvider'; 6 | import { Tree } from '../tree/Tree'; 7 | 8 | const cx = (...classNames: Array) => 9 | classNames.filter(cn => !!cn).join(' '); 10 | 11 | export default { 12 | title: 'Core/Custom View State', 13 | } as Meta; 14 | 15 | export const SingleTree = () => ( 16 | 17 | canDragAndDrop 18 | canDropOnFolder 19 | canReorderItems 20 | dataProvider={ 21 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 22 | ...item, 23 | data, 24 | })) 25 | } 26 | getItemTitle={item => item.data} 27 | renderItem={({ item, depth, children, title, context, arrow }) => { 28 | const InteractiveComponent = context.isRenaming ? 'div' : 'button'; 29 | const type = context.isRenaming ? undefined : 'button'; 30 | return ( 31 |
  • 43 |
    58 | {arrow} 59 | 64 | {title} 65 | {context.viewStateFlags.activeItems ? ' (marked as active)' : ''} 66 | 67 |
    68 | {children} 69 |
  • 70 | ); 71 | }} 72 | viewState={{ 73 | 'tree-1': { 74 | expandedItems: [ 75 | 'Fruit', 76 | 'Meals', 77 | 'America', 78 | 'Europe', 79 | 'Asia', 80 | 'Desserts', 81 | ], 82 | activeItems: ['America', 'Europe'], 83 | }, 84 | }} 85 | > 86 | 87 | 88 | ); 89 | -------------------------------------------------------------------------------- /packages/core/src/stories/DelayedDataSource.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | import { longTree } from 'demodata'; 4 | import { Tree } from '../tree/Tree'; 5 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 6 | import { TreeEnvironmentRef, TreeRef } from '../types'; 7 | 8 | export default { 9 | title: 'Core/Delayed Data Source', 10 | } as Meta; 11 | 12 | export const TreeWithDelayedDataProvider = () => ( 13 | 14 | canDragAndDrop 15 | canDropOnFolder 16 | canReorderItems 17 | dataProvider={{ 18 | getTreeItem: itemId => 19 | new Promise(res => { 20 | setTimeout(() => res(longTree.items[itemId]), 750); 21 | }), 22 | }} 23 | getItemTitle={item => item.data} 24 | viewState={{ 25 | 'tree-1': {}, 26 | }} 27 | > 28 | 29 | 30 | ); 31 | 32 | export const WithExpandOrCollapseAll = () => { 33 | const treeEnvironment = useRef(null); 34 | const tree = useRef(null); 35 | return ( 36 | 37 | ref={treeEnvironment} 38 | canDragAndDrop 39 | canDropOnFolder 40 | canReorderItems 41 | dataProvider={{ 42 | getTreeItem: itemId => 43 | new Promise(res => { 44 | setTimeout(() => res(longTree.items[itemId]), 750); 45 | }), 46 | }} 47 | getItemTitle={item => item.data} 48 | viewState={{ 49 | 'tree-1': { 50 | // expandedItems: ['Fruit', 'Meals', 'Asia', 'Desserts'], 51 | }, 52 | }} 53 | > 54 | 57 | 60 | 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/core/src/stories/FindingItems.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React, { useCallback, useMemo, useRef, useState } from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 5 | import { StaticTreeDataProvider } from '../uncontrolledEnvironment/StaticTreeDataProvider'; 6 | import { Tree } from '../tree/Tree'; 7 | import { TreeItemIndex, TreeRef } from '../types'; 8 | 9 | export default { 10 | title: 'Core/Finding Items', 11 | } as Meta; 12 | 13 | export const CustomFinder = () => { 14 | const [search, setSearch] = useState('pizza'); 15 | const tree = useRef(null); 16 | 17 | const dataProvider = useMemo( 18 | () => 19 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 20 | ...item, 21 | data, 22 | })), 23 | [] 24 | ); 25 | 26 | const findItemPath = useCallback( 27 | async (search: string, searchRoot: TreeItemIndex = 'root') => { 28 | const item = await dataProvider.getTreeItem(searchRoot); 29 | if (item.data.toLowerCase().includes(search.toLowerCase())) { 30 | return [item.index]; 31 | } 32 | const searchedItems = await Promise.all( 33 | item.children?.map(child => findItemPath(search, child)) ?? [] 34 | ); 35 | const result = searchedItems.find(item => item !== null); 36 | if (!result) { 37 | return null; 38 | } 39 | return [item.index, ...result]; 40 | }, 41 | [dataProvider] 42 | ); 43 | 44 | const find = useCallback( 45 | e => { 46 | e.preventDefault(); 47 | if (search) { 48 | findItemPath(search).then(path => { 49 | if (path) { 50 | // wait for full path including leaf, to make sure it loaded in 51 | tree.current?.expandSubsequently(path).then(() => { 52 | tree.current?.selectItems([path[path.length - 1]]); 53 | tree.current?.focusItem(path[path.length - 1]); 54 | }); 55 | } 56 | }); 57 | } 58 | }, 59 | [findItemPath, search] 60 | ); 61 | 62 | return ( 63 | <> 64 |
    65 | setSearch(e.target.value)} 68 | placeholder="Search..." 69 | /> 70 | 71 |
    72 | 73 | dataProvider={dataProvider} 74 | getItemTitle={item => item.data} 75 | viewState={{ 76 | 'tree-1': {}, 77 | }} 78 | > 79 | 85 | 86 | 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /packages/core/src/stories/HardcodedState.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { Tree } from '../tree/Tree'; 5 | import { ControlledTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 6 | 7 | export default { 8 | title: 'Core/Hardcoded State', 9 | } as Meta; 10 | 11 | export const SimpleTree = () => ( 12 | 13 | canDragAndDrop 14 | canDropOnFolder 15 | canReorderItems 16 | items={longTree.items} 17 | getItemTitle={item => item.data} 18 | viewState={{ 19 | 'tree-1': { 20 | expandedItems: [ 21 | 'Fruit', 22 | 'Meals', 23 | 'America', 24 | 'Europe', 25 | 'Asia', 26 | 'Desserts', 27 | ], 28 | }, 29 | }} 30 | > 31 | 32 | 33 | ); 34 | -------------------------------------------------------------------------------- /packages/core/src/stories/Refs.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React, { useEffect, useRef } from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 5 | import { StaticTreeDataProvider } from '../uncontrolledEnvironment/StaticTreeDataProvider'; 6 | import { Tree } from '../tree/Tree'; 7 | import { TreeEnvironmentRef, TreeRef } from '../types'; 8 | 9 | export default { 10 | title: 'Core/React Refs', 11 | } as Meta; 12 | 13 | export const ControlTreeExternally = () => { 14 | const treeEnvironment = useRef(null); 15 | const tree = useRef(null); 16 | 17 | useEffect(() => { 18 | const interval = setInterval(() => { 19 | if (treeEnvironment.current && tree.current) { 20 | const linearItems = tree.current.treeContext.getItemsLinearly(); 21 | const focusItem = 22 | treeEnvironment.current.viewState[tree.current.treeId]!.focusedItem ?? 23 | linearItems[0].item; 24 | 25 | if ( 26 | !treeEnvironment.current.viewState[ 27 | tree.current.treeId 28 | ]!.expandedItems?.includes(focusItem) 29 | ) { 30 | tree.current.expandItem(focusItem); 31 | return; 32 | } 33 | 34 | tree.current.moveFocusDown(); 35 | } 36 | }, 500); 37 | 38 | return () => clearInterval(interval); 39 | }, []); 40 | 41 | return ( 42 | 43 | ref={treeEnvironment} 44 | canDragAndDrop 45 | canDropOnFolder 46 | canReorderItems 47 | dataProvider={ 48 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 49 | ...item, 50 | data, 51 | })) 52 | } 53 | getItemTitle={item => item.data} 54 | viewState={{}} 55 | > 56 | 62 | 63 | ); 64 | }; 65 | 66 | export const ExpandOrCollapseAll = () => { 67 | const treeEnvironment = useRef(null); 68 | const tree = useRef(null); 69 | return ( 70 | 71 | ref={treeEnvironment} 72 | canDragAndDrop 73 | canDropOnFolder 74 | canReorderItems 75 | dataProvider={ 76 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 77 | ...item, 78 | data, 79 | })) 80 | } 81 | getItemTitle={item => item.data} 82 | viewState={{ 83 | 'tree-1': { 84 | // expandedItems: ['Fruit', 'Meals', 'Asia', 'Desserts'], 85 | }, 86 | }} 87 | > 88 | 91 | 94 | 100 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /packages/core/src/stories/Scalability.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | import { ExplicitDataSource } from '../types'; 4 | import { Tree } from '../tree/Tree'; 5 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 6 | import { StaticTreeDataProvider } from '../uncontrolledEnvironment/StaticTreeDataProvider'; 7 | 8 | const itemsWithManyChildren: ExplicitDataSource = { 9 | items: { 10 | root: { 11 | index: 'root', 12 | children: ['innerRoot'], 13 | data: 'root', 14 | isFolder: true, 15 | canMove: true, 16 | canRename: true, 17 | }, 18 | innerRoot: { 19 | index: 'innerRoot', 20 | children: [], 21 | data: 'innerRoot', 22 | isFolder: true, 23 | canMove: true, 24 | canRename: true, 25 | }, 26 | }, 27 | }; 28 | 29 | for (let i = 0; i < 1000; i += 1) { 30 | const id = `item${i}`; 31 | itemsWithManyChildren.items[id] = { 32 | index: id, 33 | isFolder: false, 34 | data: id, 35 | canMove: true, 36 | canRename: true, 37 | }; 38 | itemsWithManyChildren.items.innerRoot.children!.push(id); 39 | } 40 | 41 | export default { 42 | title: 'Core/Scalability', 43 | } as Meta; 44 | 45 | export const SingleTree = () => ( 46 | 47 | canDragAndDrop 48 | canDropOnFolder 49 | canReorderItems 50 | dataProvider={new StaticTreeDataProvider(itemsWithManyChildren.items)} 51 | getItemTitle={item => item.data} 52 | viewState={{}} 53 | > 54 | 55 | 56 | ); 57 | -------------------------------------------------------------------------------- /packages/core/src/stories/Search.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import React from 'react'; 3 | import { longTree } from 'demodata'; 4 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 5 | import { StaticTreeDataProvider } from '../uncontrolledEnvironment/StaticTreeDataProvider'; 6 | import { Tree } from '../tree/Tree'; 7 | 8 | export default { 9 | title: 'Core/Search Configurability', 10 | } as Meta; 11 | 12 | export const SearchByStartTyping = () => ( 13 | 14 | dataProvider={new StaticTreeDataProvider(longTree.items)} 15 | getItemTitle={item => item.data} 16 | canSearchByStartingTyping 17 | viewState={{ 18 | 'tree-1': { 19 | expandedItems: [ 20 | 'Fruit', 21 | 'Meals', 22 | 'America', 23 | 'Europe', 24 | 'Asia', 25 | 'Desserts', 26 | ], 27 | }, 28 | }} 29 | > 30 | 31 | 32 | ); 33 | 34 | export const SearchOnlyWithHotkey = () => ( 35 | 36 | dataProvider={new StaticTreeDataProvider(longTree.items)} 37 | getItemTitle={item => item.data} 38 | canSearchByStartingTyping={false} 39 | keyboardBindings={{ 40 | startSearch: ['f1'], 41 | // TODO hotkeys do not overwrite browser default because preventDefault is called on keyup, not keydown 42 | // TODO fix by checking whether hotkey is already fulfilled during keydown 43 | }} 44 | viewState={{ 45 | 'tree-1': { 46 | expandedItems: [ 47 | 'Fruit', 48 | 'Meals', 49 | 'America', 50 | 'Europe', 51 | 'Asia', 52 | 'Desserts', 53 | ], 54 | }, 55 | }} 56 | > 57 | 58 | 59 | ); 60 | 61 | export const NoSearch = () => ( 62 | 63 | dataProvider={new StaticTreeDataProvider(longTree.items)} 64 | getItemTitle={item => item.data} 65 | canSearch={false} 66 | viewState={{ 67 | 'tree-1': { 68 | expandedItems: [ 69 | 'Fruit', 70 | 'Meals', 71 | 'America', 72 | 'Europe', 73 | 'Asia', 74 | 'Desserts', 75 | ], 76 | }, 77 | }} 78 | > 79 | 80 | 81 | ); 82 | 83 | export const CustomSearchEvaluation = () => ( 84 | <> 85 |

    86 | In the following example, the search evaluates only the children of an 87 | item, not the items title itself. This means that searching for 88 | "Orange" will match its parent "Fruit". 89 |

    90 | 91 | dataProvider={new StaticTreeDataProvider(longTree.items)} 92 | getItemTitle={item => item.data} 93 | doesSearchMatchItem={(search, item) => 94 | !!item.children?.join(' ').toLowerCase().includes(search.toLowerCase()) 95 | } 96 | renderItemTitle={props => 97 | props.info.isSearching && props.context.isSearchMatching ? ( 98 | {props.title} 99 | ) : ( 100 | props.title 101 | ) 102 | } 103 | viewState={{ 104 | 'tree-1': { 105 | expandedItems: [ 106 | 'Fruit', 107 | 'Meals', 108 | 'America', 109 | 'Europe', 110 | 'Asia', 111 | 'Desserts', 112 | ], 113 | }, 114 | }} 115 | > 116 | 117 | 118 | 119 | ); 120 | -------------------------------------------------------------------------------- /packages/core/src/stories/Theming.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/react'; 2 | import { longTree } from 'demodata'; 3 | import React from 'react'; 4 | import { UncontrolledTreeEnvironment } from '../uncontrolledEnvironment/UncontrolledTreeEnvironment'; 5 | import { StaticTreeDataProvider } from '../uncontrolledEnvironment/StaticTreeDataProvider'; 6 | import { Tree } from '../tree/Tree'; 7 | 8 | export default { 9 | title: 'Core/Custom Renderers', 10 | } as Meta; 11 | 12 | export const MinimalRenderers = () => ( 13 | 14 | canDragAndDrop 15 | canDropOnFolder 16 | canReorderItems 17 | dataProvider={ 18 | new StaticTreeDataProvider(longTree.items, (item, data) => ({ 19 | ...item, 20 | data, 21 | })) 22 | } 23 | getItemTitle={item => item.data} 24 | viewState={{ 25 | 'tree-1': { 26 | expandedItems: [ 27 | 'Fruit', 28 | 'Meals', 29 | 'America', 30 | 'Europe', 31 | 'Asia', 32 | 'Desserts', 33 | ], 34 | }, 35 | }} 36 | renderItemTitle={({ title }) => {title}} 37 | renderItemArrow={({ item, context }) => 38 | item.isFolder ? ( 39 | context.isExpanded ? ( 40 | {'>'} 41 | ) : ( 42 | v 43 | ) 44 | ) : null 45 | } 46 | renderItem={({ title, arrow, context, children }) => { 47 | const InteractiveComponent = context.isRenaming ? 'div' : 'button'; 48 | return ( 49 |
  • 50 | 55 | {arrow} 56 | {title} 57 | 58 | {children} 59 |
  • 60 | ); 61 | }} 62 | renderTreeContainer={({ children, containerProps }) => ( 63 |
    {children}
    64 | )} 65 | renderItemsContainer={({ children, containerProps }) => ( 66 |
      {children}
    67 | )} 68 | > 69 | 70 | 71 | ); 72 | -------------------------------------------------------------------------------- /packages/core/src/tree/DragBetweenLine.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { HTMLProps } from 'react'; 3 | import { useTree } from './Tree'; 4 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 5 | 6 | export const DragBetweenLine: React.FC<{ 7 | treeId: string; 8 | }> = ({ treeId }) => { 9 | const { draggingPosition, itemHeight } = useDragAndDrop(); 10 | const { renderers } = useTree(); 11 | 12 | const shouldDisplay = 13 | draggingPosition && 14 | draggingPosition.targetType === 'between-items' && 15 | draggingPosition.treeId === treeId; 16 | 17 | if (!shouldDisplay) { 18 | return null; 19 | } 20 | 21 | const lineProps: HTMLProps = { 22 | onDragOver: e => e.preventDefault(), // Allow dropping 23 | }; 24 | 25 | return ( 26 |
    34 | {renderers.renderDragBetweenLine({ 35 | draggingPosition: draggingPosition!, 36 | lineProps, 37 | })} 38 |
    39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/core/src/tree/LiveDescription.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useMemo } from 'react'; 3 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 4 | import { defaultLiveDescriptors } from './defaultLiveDescriptors'; 5 | import { useTree } from './Tree'; 6 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 7 | import { resolveLiveDescriptor } from './resolveLiveDescriptor'; 8 | import { useKeyboardBindings } from '../hotkeys/useKeyboardBindings'; 9 | 10 | const LiveWrapper = ({ 11 | children, 12 | live, 13 | }: { 14 | children: string; 15 | live: 'off' | 'assertive' | 'polite'; 16 | // eslint-disable-next-line react/no-danger 17 | }) =>
    ; 18 | 19 | export const LiveDescription: React.FC = () => { 20 | const env = useTreeEnvironment(); 21 | const tree = useTree(); 22 | const dnd = useDragAndDrop(); 23 | const keys = useKeyboardBindings(); 24 | 25 | const descriptors = useMemo( 26 | () => env.liveDescriptors ?? defaultLiveDescriptors, 27 | [env.liveDescriptors] 28 | ); 29 | 30 | const MainWrapper = tree.renderers.renderLiveDescriptorContainer; 31 | 32 | if (tree.treeInformation.isRenaming) { 33 | return ( 34 | 35 | 36 | {resolveLiveDescriptor( 37 | descriptors.renamingItem, 38 | env, 39 | dnd, 40 | tree, 41 | keys 42 | )} 43 | 44 | 45 | ); 46 | } 47 | if (tree.treeInformation.isSearching) { 48 | return ( 49 | 50 | 51 | {resolveLiveDescriptor(descriptors.searching, env, dnd, tree, keys)} 52 | 53 | 54 | ); 55 | } 56 | if (tree.treeInformation.isProgrammaticallyDragging) { 57 | return ( 58 | 59 | 60 | {resolveLiveDescriptor( 61 | descriptors.programmaticallyDragging, 62 | env, 63 | dnd, 64 | tree, 65 | keys 66 | )} 67 | 68 | 69 | {resolveLiveDescriptor( 70 | descriptors.programmaticallyDraggingTarget, 71 | env, 72 | dnd, 73 | tree, 74 | keys 75 | )} 76 | 77 | 78 | ); 79 | } 80 | return ( 81 | 82 | 83 | {resolveLiveDescriptor(descriptors.introduction, env, dnd, tree, keys)} 84 | 85 | 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /packages/core/src/tree/MaybeLiveDescription.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 3 | import { LiveDescription } from './LiveDescription'; 4 | 5 | export const MaybeLiveDescription: React.FC = () => { 6 | const env = useTreeEnvironment(); 7 | 8 | if (!(env.showLiveDescription ?? true)) { 9 | return null; 10 | } 11 | 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/core/src/tree/Tree.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useContext, useEffect, useMemo, useState } from 'react'; 3 | import { 4 | AllTreeRenderProps, 5 | TreeContextProps, 6 | TreeItemIndex, 7 | TreeProps, 8 | TreeRef, 9 | } from '../types'; 10 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 11 | import { TreeManager } from './TreeManager'; 12 | import { useCreatedTreeInformation } from './useCreatedTreeInformation'; 13 | import { getItemsLinearly } from './getItemsLinearly'; 14 | import { TreeActionsProvider } from '../treeActions/TreeActionsProvider'; 15 | 16 | const TreeContext = React.createContext(null as any); // TODO default value 17 | 18 | export const useTree = () => useContext(TreeContext); 19 | 20 | export const Tree = React.forwardRef((props, ref) => { 21 | const environment = useTreeEnvironment(); 22 | const renderers = useMemo( 23 | () => ({ ...environment, ...props }), 24 | [props, environment] 25 | ); 26 | const [search, setSearch] = useState(null); 27 | const [renamingItem, setRenamingItem] = useState(null); 28 | const rootItem = environment.items[props.rootItem]; 29 | const viewState = environment.viewState[props.treeId]; 30 | 31 | useEffect(() => { 32 | environment.registerTree({ 33 | treeId: props.treeId, 34 | rootItem: props.rootItem, 35 | }); 36 | 37 | return () => environment.unregisterTree(props.treeId); 38 | // TODO should be able to remove soon, and add environment.registerTree, environment.unregisterTree as deps 39 | // eslint-disable-next-line react-hooks/exhaustive-deps 40 | }, [props.treeId, props.rootItem]); 41 | 42 | const treeInformation = useCreatedTreeInformation( 43 | props, 44 | renamingItem, 45 | search 46 | ); 47 | 48 | const treeContextProps = useMemo( 49 | () => ({ 50 | treeId: props.treeId, 51 | rootItem: props.rootItem, 52 | treeLabel: props.treeLabel, 53 | treeLabelledBy: props.treeLabelledBy, 54 | getItemsLinearly: () => 55 | getItemsLinearly(props.rootItem, viewState ?? {}, environment.items), 56 | treeInformation, 57 | search, 58 | setSearch, 59 | renamingItem, 60 | setRenamingItem, 61 | renderers, 62 | }), 63 | [ 64 | environment.items, 65 | props.rootItem, 66 | props.treeId, 67 | props.treeLabel, 68 | props.treeLabelledBy, 69 | renamingItem, 70 | renderers, 71 | search, 72 | treeInformation, 73 | viewState, 74 | ] 75 | ); 76 | 77 | if (rootItem === undefined) { 78 | environment.onMissingItems?.([props.rootItem]); 79 | return null; 80 | } 81 | 82 | return ( 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | }) as ( 90 | p: TreeProps & { ref?: React.Ref> } 91 | ) => React.ReactElement; 92 | -------------------------------------------------------------------------------- /packages/core/src/tree/TreeManager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { HTMLProps, useRef } from 'react'; 3 | import { TreeItemChildren } from '../treeItem/TreeItemChildren'; 4 | import { DragBetweenLine } from './DragBetweenLine'; 5 | import { useFocusWithin } from './useFocusWithin'; 6 | import { useTreeKeyboardBindings } from './useTreeKeyboardBindings'; 7 | import { SearchInput } from '../search/SearchInput'; 8 | import { useTree } from './Tree'; 9 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 10 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 11 | import { MaybeLiveDescription } from './MaybeLiveDescription'; 12 | 13 | export const TreeManager = (): React.ReactElement => { 14 | const { treeId, rootItem, renderers, treeInformation } = useTree(); 15 | const environment = useTreeEnvironment(); 16 | const containerRef = useRef(); 17 | const dnd = useDragAndDrop(); 18 | 19 | useTreeKeyboardBindings(); 20 | 21 | useFocusWithin( 22 | containerRef.current, 23 | () => { 24 | environment.setActiveTree(treeId); 25 | }, 26 | () => { 27 | environment.setActiveTree(oldTreeId => 28 | oldTreeId === treeId ? undefined : oldTreeId 29 | ); 30 | } 31 | ); 32 | 33 | const rootChildren = environment.items[rootItem].children; 34 | 35 | const treeChildren = ( 36 | <> 37 | 38 | 39 | {rootChildren ?? []} 40 | 41 | 42 | 43 | 44 | ); 45 | 46 | const containerProps: HTMLProps = { 47 | onDragOver: e => { 48 | e.preventDefault(); // Allow drop. Also implicitly set by items, but needed here as well for dropping on empty space 49 | dnd.onDragOverTreeHandler(e as any, treeId, containerRef); 50 | }, 51 | onDragLeave: e => { 52 | dnd.onDragLeaveContainerHandler(e as any, containerRef); 53 | }, 54 | onMouseDown: () => dnd.abortProgrammaticDrag(), 55 | ref: containerRef, 56 | style: { position: 'relative' }, 57 | role: 'tree', 58 | 'aria-label': !treeInformation.treeLabelledBy 59 | ? treeInformation.treeLabel 60 | : undefined, 61 | 'aria-labelledby': treeInformation.treeLabelledBy, 62 | ...({ 63 | 'data-rct-tree': treeId, 64 | } as any), 65 | }; 66 | 67 | return renderers.renderTreeContainer({ 68 | children: treeChildren, 69 | info: treeInformation, 70 | containerProps, 71 | }) as React.ReactElement; 72 | }; 73 | -------------------------------------------------------------------------------- /packages/core/src/tree/defaultLiveDescriptors.ts: -------------------------------------------------------------------------------- 1 | import { LiveDescriptors } from '../types'; 2 | 3 | export const defaultLiveDescriptors: LiveDescriptors = { 4 | introduction: ` 5 |

    Accessibility guide for tree {treeLabel}.

    6 |

    7 | Navigate the tree with the arrow keys. Common tree hotkeys apply. Further keybindings are available: 8 |

    9 |
      10 |
    • {keybinding:primaryAction} to execute primary action on focused item
    • 11 |
    • {keybinding:renameItem} to start renaming the focused item
    • 12 |
    • {keybinding:abortRenameItem} to abort renaming an item
    • 13 |
    • {keybinding:startProgrammaticDnd} to start dragging selected items
    • 14 |
    15 | `, 16 | 17 | renamingItem: ` 18 |

    Renaming the item {renamingItem}.

    19 |

    Use the keybinding {keybinding:abortRenameItem} to abort renaming.

    20 | `, 21 | 22 | searching: ` 23 |

    Searching

    24 | `, 25 | 26 | programmaticallyDragging: ` 27 |

    Dragging items {dragItems}.

    28 |

    Press the arrow keys to move the drag target.

    29 |

    Press {keybinding:completeProgrammaticDnd} to drop or {keybinding:abortProgrammaticDnd} to abort.

    30 | `, 31 | 32 | programmaticallyDraggingTarget: ` 33 |

    Drop target is {dropTarget}.

    34 | `, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/core/src/tree/getItemsLinearly.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IndividualTreeViewState, 3 | LinearItem, 4 | TreeItem, 5 | TreeItemIndex, 6 | } from '../types'; 7 | 8 | export const getItemsLinearly = ( 9 | rootItem: TreeItemIndex, 10 | viewState: IndividualTreeViewState, 11 | items: Record>, 12 | depth = 0 13 | ): LinearItem[] => { 14 | const itemIds: Array<{ item: TreeItemIndex; depth: number }> = []; 15 | 16 | for (const itemId of items[rootItem]?.children ?? []) { 17 | const item = items[itemId]; 18 | itemIds.push({ item: itemId, depth }); 19 | if ( 20 | item && 21 | item.isFolder && 22 | !!item.children && 23 | viewState.expandedItems?.includes(itemId) 24 | ) { 25 | itemIds.push(...getItemsLinearly(itemId, viewState, items, depth + 1)); 26 | } 27 | } 28 | 29 | return itemIds; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/core/src/tree/resolveLiveDescriptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DragAndDropContextProps, 3 | KeyboardBindings, 4 | TreeContextProps, 5 | TreeEnvironmentContextProps, 6 | TreeItemIndex, 7 | } from '../types'; 8 | 9 | export const resolveLiveDescriptor = ( 10 | descriptor: string, 11 | environment: TreeEnvironmentContextProps, 12 | dnd: DragAndDropContextProps, 13 | tree: TreeContextProps, 14 | keyboardBindings: Required 15 | ) => { 16 | const getItemTitle = (index: TreeItemIndex) => 17 | environment.getItemTitle(environment.items[index]); 18 | 19 | return descriptor.replace(/({[^\s}]+)}/g, variableNameWithBrackets => { 20 | const variableName = variableNameWithBrackets.slice(1, -1); 21 | switch (variableName) { 22 | case 'treeLabel': 23 | return tree.treeLabel ?? ''; 24 | case 'renamingItem': 25 | return tree.renamingItem ? getItemTitle(tree.renamingItem) : 'None'; 26 | case 'dragItems': 27 | return ( 28 | dnd.draggingItems 29 | ?.map(item => environment.getItemTitle(item)) 30 | .join(', ') ?? 'None' 31 | ); 32 | case 'dropTarget': { 33 | if (!dnd.draggingPosition) { 34 | return 'None'; 35 | } 36 | if ( 37 | dnd.draggingPosition.targetType === 'item' || 38 | dnd.draggingPosition.targetType === 'root' 39 | ) { 40 | return `within ${getItemTitle(dnd.draggingPosition.targetItem)}`; 41 | } 42 | const parentItem = environment.items[dnd.draggingPosition.parentItem]; 43 | const parentTitle = environment.getItemTitle(parentItem); 44 | 45 | if (dnd.draggingPosition.childIndex === 0) { 46 | return `within ${parentTitle} at the start`; 47 | } 48 | return `within ${parentTitle} after ${getItemTitle( 49 | parentItem.children![dnd.draggingPosition.childIndex - 1] 50 | )}`; 51 | } 52 | 53 | default: 54 | if (variableName.startsWith('keybinding:')) { 55 | return keyboardBindings[ 56 | variableName.slice(11) as keyof KeyboardBindings 57 | ]![0]; 58 | } 59 | throw Error(`Unknown live descriptor variable {${variableName}}`); 60 | } 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/core/src/tree/scrollIntoView.ts: -------------------------------------------------------------------------------- 1 | import { getDocument } from '../utils'; 2 | 3 | export const scrollIntoView = (element: Element | undefined | null) => { 4 | if (!element) { 5 | return; 6 | } 7 | 8 | if ((element as any).scrollIntoViewIfNeeded) { 9 | (element as any).scrollIntoViewIfNeeded(); 10 | } else { 11 | const boundingBox = element.getBoundingClientRect(); 12 | const isElementInViewport = 13 | boundingBox.top >= 0 && 14 | boundingBox.left >= 0 && 15 | boundingBox.bottom <= 16 | (window.innerHeight || 17 | !!getDocument()?.documentElement?.clientHeight) && 18 | boundingBox.right <= 19 | (window.innerWidth || !!getDocument()?.documentElement?.clientWidth); 20 | if (!isElementInViewport) { 21 | element.scrollIntoView(); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/core/src/tree/useCreatedTreeInformation.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { TreeInformation, TreeItemIndex, TreeProps } from '../types'; 3 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 4 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 5 | 6 | export const useCreatedTreeInformation = ( 7 | tree: TreeProps, 8 | renamingItem: TreeItemIndex | null, 9 | search: string | null 10 | ) => { 11 | const environment = useTreeEnvironment(); 12 | const dnd = useDragAndDrop(); 13 | const selectedItems = environment.viewState[tree.treeId]?.selectedItems; 14 | return useMemo( 15 | () => ({ 16 | isFocused: environment.activeTreeId === tree.treeId, 17 | isRenaming: !!renamingItem, 18 | areItemsSelected: (selectedItems?.length ?? 0) > 0, 19 | isSearching: search !== null, 20 | search, 21 | isProgrammaticallyDragging: dnd.isProgrammaticallyDragging ?? false, 22 | treeId: tree.treeId, 23 | rootItem: tree.rootItem, 24 | treeLabel: tree.treeLabel, 25 | treeLabelledBy: tree.treeLabelledBy, 26 | }), 27 | [ 28 | environment.activeTreeId, 29 | tree.treeId, 30 | tree.rootItem, 31 | tree.treeLabel, 32 | tree.treeLabelledBy, 33 | renamingItem, 34 | selectedItems?.length, 35 | search, 36 | dnd.isProgrammaticallyDragging, 37 | ] 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/tree/useFocusWithin.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { useHtmlElementEventListener } from '../useHtmlElementEventListener'; 3 | import { useCallSoon } from '../useCallSoon'; 4 | 5 | export const useFocusWithin = ( 6 | element: HTMLElement | undefined, 7 | onFocusIn?: () => void, 8 | onFocusOut?: () => void 9 | ) => { 10 | const [focusWithin, setFocusWithin] = useState(false); 11 | const isLoosingFocusFlag = useRef(false); 12 | const callSoon = useCallSoon(); 13 | 14 | useHtmlElementEventListener(element, 'focusin', () => { 15 | if (!focusWithin) { 16 | setFocusWithin(true); 17 | onFocusIn?.(); 18 | } 19 | 20 | if (isLoosingFocusFlag.current) { 21 | isLoosingFocusFlag.current = false; 22 | } 23 | }); 24 | 25 | useHtmlElementEventListener(element, 'focusout', () => { 26 | isLoosingFocusFlag.current = true; 27 | 28 | callSoon(() => { 29 | if ( 30 | isLoosingFocusFlag.current && 31 | !element?.contains(document.activeElement) 32 | ) { 33 | onFocusOut?.(); 34 | isLoosingFocusFlag.current = false; 35 | setFocusWithin(false); 36 | } 37 | }); 38 | }); 39 | 40 | return focusWithin; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/core/src/tree/useMoveFocusToIndex.ts: -------------------------------------------------------------------------------- 1 | import { useViewState } from './useViewState'; 2 | import { useTree } from './Tree'; 3 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 4 | import { useLinearItems } from '../controlledEnvironment/useLinearItems'; 5 | import { LinearItem } from '../types'; 6 | import { useStableHandler } from '../useStableHandler'; 7 | 8 | export const useMoveFocusToIndex = () => { 9 | const { treeId } = useTree(); 10 | const { onFocusItem, items } = useTreeEnvironment(); 11 | const linearItems = useLinearItems(treeId); 12 | const viewState = useViewState(); 13 | 14 | return useStableHandler( 15 | ( 16 | computeNewIndex: ( 17 | currentIndex: number, 18 | linearItems: LinearItem[] 19 | ) => number 20 | ) => { 21 | const currentIndex = 22 | linearItems.findIndex(item => item.item === viewState.focusedItem) ?? 0; 23 | const newIndex = computeNewIndex(currentIndex, linearItems); 24 | const newIndexBounded = Math.max( 25 | 0, 26 | Math.min(linearItems.length - 1, newIndex) 27 | ); 28 | const newFocusItem = items[linearItems[newIndexBounded].item]; 29 | onFocusItem?.(newFocusItem, treeId); 30 | return newFocusItem; 31 | } 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/core/src/tree/useSelectUpTo.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | import { useViewState } from './useViewState'; 3 | import { useTree } from './Tree'; 4 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 5 | import { TreeItem, TreeItemIndex } from '../types'; 6 | import { useLinearItems } from '../controlledEnvironment/useLinearItems'; 7 | 8 | const usePrevious = (value: T) => { 9 | const ref = useRef<{ target: T; previous?: T }>({ 10 | target: value, 11 | previous: undefined, 12 | }); 13 | if (ref.current.target !== value) { 14 | ref.current.previous = ref.current.target; 15 | ref.current.target = value; 16 | } 17 | return ref.current.previous; 18 | }; 19 | 20 | export const useSelectUpTo = (startingAt: 'last-focus' | 'first-selected') => { 21 | const viewState = useViewState(); 22 | const { treeId } = useTree(); 23 | const linearItems = useLinearItems(treeId); 24 | const { onSelectItems } = useTreeEnvironment(); 25 | const focusedItemPrevious = usePrevious(viewState.focusedItem); 26 | 27 | return useCallback( 28 | (item: TreeItem, overrideOldSelection = false) => { 29 | const itemIndex = item.index; 30 | const selectMergedItems = ( 31 | oldSelection: TreeItemIndex[], 32 | newSelection: TreeItemIndex[] 33 | ) => { 34 | const merged = [ 35 | ...(overrideOldSelection ? [] : oldSelection), 36 | ...newSelection.filter( 37 | i => overrideOldSelection || !oldSelection.includes(i) 38 | ), 39 | ]; 40 | onSelectItems?.(merged, treeId); 41 | }; 42 | 43 | if ( 44 | viewState && 45 | viewState.selectedItems && 46 | viewState.selectedItems.length > 0 47 | ) { 48 | // Depending on whether focusItem() or selectUpTo() was called first, which item was the last focused item depends 49 | const lastFocus = 50 | viewState.focusedItem === itemIndex 51 | ? focusedItemPrevious 52 | : viewState.focusedItem; 53 | 54 | const selectionStart = 55 | startingAt === 'last-focus' 56 | ? linearItems.findIndex(linearItem => lastFocus === linearItem.item) 57 | : linearItems.findIndex(linearItem => 58 | viewState.selectedItems?.includes(linearItem.item) 59 | ); 60 | const selectionEnd = linearItems.findIndex( 61 | linearItem => linearItem.item === itemIndex 62 | ); 63 | 64 | if (selectionStart < selectionEnd) { 65 | const selection = linearItems 66 | .slice(selectionStart, selectionEnd + 1) 67 | .map(({ item }) => item); 68 | selectMergedItems(viewState.selectedItems ?? [], selection); 69 | } else { 70 | const selection = linearItems 71 | .slice(selectionEnd, selectionStart + 1) 72 | .map(({ item }) => item); 73 | selectMergedItems(viewState.selectedItems ?? [], selection); 74 | } 75 | } else { 76 | onSelectItems?.([itemIndex], treeId); 77 | } 78 | }, 79 | [ 80 | viewState, 81 | onSelectItems, 82 | treeId, 83 | startingAt, 84 | linearItems, 85 | focusedItemPrevious, 86 | ] 87 | ); 88 | }; 89 | -------------------------------------------------------------------------------- /packages/core/src/tree/useViewState.ts: -------------------------------------------------------------------------------- 1 | import { useTree } from './Tree'; 2 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 3 | 4 | export const useViewState = () => { 5 | const { treeId } = useTree(); 6 | const { viewState } = useTreeEnvironment(); 7 | return viewState[treeId] ?? {}; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/core/src/treeActions/TreeActionsProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PropsWithChildren, useMemo } from 'react'; 3 | import { 4 | TreeChangeActions, 5 | TreeChangeActionsContextProps, 6 | TreeItemIndex, 7 | TreeRef, 8 | } from '../types'; 9 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 10 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 11 | import { useCreatedTreeRef } from './useCreatedTreeRef'; 12 | import { useTree } from '../tree/Tree'; 13 | import { useEnvironmentActions } from '../environmentActions/EnvironmentActionsProvider'; 14 | 15 | const EnvironmentActionsContext = 16 | React.createContext(null as any); 17 | export const useTreeActions = () => React.useContext(EnvironmentActionsContext); 18 | 19 | export const TreeActionsProvider = React.forwardRef< 20 | TreeRef, 21 | PropsWithChildren> 22 | >((props, ref) => { 23 | useTreeEnvironment(); 24 | const tree = useTree(); 25 | useDragAndDrop(); 26 | const envActions = useEnvironmentActions(); 27 | 28 | // TODO change tree childs to use actions rather than output events where possible 29 | // TODO maybe replace with stable handlers 30 | const actions = useMemo( 31 | () => ({ 32 | abortRenamingItem(): void { 33 | tree.setRenamingItem(null); 34 | }, 35 | abortSearch(): void { 36 | tree.setSearch(null); 37 | }, 38 | collapseItem(itemId: TreeItemIndex): void { 39 | envActions.collapseItem(itemId, tree.treeId); 40 | }, 41 | completeRenamingItem(): void { 42 | // TODO 43 | }, 44 | expandItem(itemId: TreeItemIndex): void { 45 | envActions.expandItem(itemId, tree.treeId); 46 | }, 47 | focusItem(itemId: TreeItemIndex, setDomFocus = true): void { 48 | envActions.focusItem(itemId, tree.treeId, setDomFocus); 49 | }, 50 | focusTree(autoFocus = true): void { 51 | envActions.focusTree(tree.treeId, autoFocus); 52 | }, 53 | invokePrimaryAction(itemId: TreeItemIndex): void { 54 | envActions.invokePrimaryAction(itemId, tree.treeId); 55 | }, 56 | moveFocusDown(): void { 57 | envActions.moveFocusDown(tree.treeId); 58 | }, 59 | moveFocusUp(): void { 60 | envActions.moveFocusUp(tree.treeId); 61 | }, 62 | renameItem(itemId: TreeItemIndex, name: string): void { 63 | envActions.renameItem(itemId, name, tree.treeId); 64 | }, 65 | selectItems(itemsIds: TreeItemIndex[]): void { 66 | envActions.selectItems(itemsIds, tree.treeId); 67 | }, 68 | setSearch(search: string | null): void { 69 | tree.setSearch(search); 70 | }, 71 | startRenamingItem(itemId: TreeItemIndex): void { 72 | tree.setRenamingItem(itemId); 73 | }, 74 | stopRenamingItem(): void { 75 | tree.setRenamingItem(null); 76 | }, 77 | toggleItemExpandedState(itemId: TreeItemIndex): void { 78 | envActions.toggleItemExpandedState(itemId, tree.treeId); 79 | }, 80 | toggleItemSelectStatus(itemId: TreeItemIndex): void { 81 | envActions.toggleItemSelectStatus(itemId, tree.treeId); 82 | }, 83 | expandAll(): void { 84 | envActions.expandAll(tree.treeId); 85 | }, 86 | collapseAll(): void { 87 | envActions.collapseAll(tree.treeId); 88 | }, 89 | expandSubsequently(itemIds: TreeItemIndex[]): Promise { 90 | return envActions.expandSubsequently(tree.treeId, itemIds); 91 | }, 92 | }), 93 | [envActions, tree] 94 | ); 95 | 96 | useCreatedTreeRef(ref, actions); 97 | 98 | return ( 99 | 100 | {props.children} 101 | 102 | ); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/core/src/treeActions/useCreatedTreeRef.ts: -------------------------------------------------------------------------------- 1 | import { Ref, useImperativeHandle } from 'react'; 2 | import { TreeChangeActions, TreeRef } from '../types'; 3 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 4 | import { useDragAndDrop } from '../drag/DragAndDropProvider'; 5 | import { useTree } from '../tree/Tree'; 6 | 7 | export const useCreatedTreeRef = ( 8 | ref: Ref, 9 | actions: TreeChangeActions 10 | ) => { 11 | const environment = useTreeEnvironment(); 12 | const tree = useTree(); 13 | const dnd = useDragAndDrop(); 14 | 15 | useImperativeHandle(ref, () => ({ 16 | ...actions, 17 | treeEnvironmentContext: environment, 18 | dragAndDropContext: dnd, 19 | treeContext: tree, 20 | ...tree.treeInformation, 21 | })); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/treeItem/TreeItemChildren.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps } from 'react'; 2 | import { TreeItemElement } from './TreeItemElement'; 3 | import { TreeItemIndex } from '../types'; 4 | import { useTree } from '../tree/Tree'; 5 | 6 | export const TreeItemChildren = (props: { 7 | children: TreeItemIndex[]; 8 | depth: number; 9 | parentId: TreeItemIndex; 10 | }): React.ReactElement => { 11 | const { renderers, treeInformation } = useTree(); 12 | 13 | const childElements: React.ReactElement[] = []; 14 | 15 | for (const child of props.children) { 16 | childElements.push( 17 | 18 | ); 19 | } 20 | 21 | if (childElements.length === 0) { 22 | return null as any; 23 | } 24 | 25 | const containerProps: HTMLProps = { 26 | role: props.depth !== 0 ? 'group' : undefined, 27 | }; 28 | 29 | return renderers.renderItemsContainer({ 30 | children: childElements, 31 | info: treeInformation, 32 | containerProps, 33 | depth: props.depth, 34 | parentId: props.parentId, 35 | }) as any; 36 | }; 37 | -------------------------------------------------------------------------------- /packages/core/src/treeItem/TreeItemElement.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react'; 2 | import { TreeItemIndex } from '../types'; 3 | import { TreeItemChildren } from './TreeItemChildren'; 4 | import { useViewState } from '../tree/useViewState'; 5 | import { useTree } from '../tree/Tree'; 6 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 7 | import { useTreeItemRenderContext } from './useTreeItemRenderContext'; 8 | import { TreeItemRenamingInput } from './TreeItemRenamingInput'; 9 | 10 | export const TreeItemElement = (props: { 11 | itemIndex: TreeItemIndex; 12 | depth: number; 13 | }): React.ReactElement => { 14 | const [hasBeenRequested, setHasBeenRequested] = useState(false); 15 | const { renderers, treeInformation, renamingItem } = useTree(); 16 | const environment = useTreeEnvironment(); 17 | const viewState = useViewState(); 18 | const item = environment.items[props.itemIndex]; 19 | 20 | const isExpanded = useMemo( 21 | () => viewState.expandedItems?.includes(props.itemIndex), 22 | [props.itemIndex, viewState.expandedItems] 23 | ); 24 | 25 | const renderContext = useTreeItemRenderContext(item)!; 26 | 27 | if (item === undefined || renderContext === undefined) { 28 | if (!hasBeenRequested) { 29 | setHasBeenRequested(true); 30 | environment.onMissingItems?.([props.itemIndex]); 31 | } 32 | return null as any; 33 | } 34 | 35 | const shouldRenderChildren = 36 | environment.shouldRenderChildren?.(item, renderContext) ?? 37 | (item.isFolder && isExpanded); 38 | 39 | const children = item.children && shouldRenderChildren && ( 40 | 41 | {item.children} 42 | 43 | ); 44 | 45 | const title = environment.getItemTitle(item); 46 | const titleComponent = 47 | renamingItem === props.itemIndex ? ( 48 | 49 | ) : ( 50 | renderers.renderItemTitle({ 51 | info: treeInformation, 52 | context: renderContext, 53 | title, 54 | item, 55 | }) 56 | ); 57 | 58 | const arrowComponent = renderers.renderItemArrow({ 59 | info: treeInformation, 60 | context: renderContext, 61 | item: environment.items[props.itemIndex], 62 | }); 63 | 64 | return (renderers.renderItem({ 65 | item: environment.items[props.itemIndex], 66 | depth: props.depth, 67 | title: titleComponent, 68 | arrow: arrowComponent, 69 | context: renderContext, 70 | info: treeInformation, 71 | children, 72 | }) ?? null) as any; // Type to use AllTreeRenderProps 73 | }; 74 | -------------------------------------------------------------------------------- /packages/core/src/treeItem/TreeItemRenamingInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | FormHTMLAttributes, 4 | HTMLProps, 5 | InputHTMLAttributes, 6 | useRef, 7 | useState, 8 | } from 'react'; 9 | import { TreeItemIndex } from '../types'; 10 | import { useTree } from '../tree/Tree'; 11 | import { useTreeEnvironment } from '../controlledEnvironment/ControlledTreeEnvironment'; 12 | import { useHotkey } from '../hotkeys/useHotkey'; 13 | import { useSideEffect } from '../useSideEffect'; 14 | import { useCallSoon } from '../useCallSoon'; 15 | 16 | export const TreeItemRenamingInput: React.FC<{ 17 | itemIndex: TreeItemIndex; 18 | }> = props => { 19 | const { renderers, treeInformation, setRenamingItem, treeId } = useTree(); 20 | const environment = useTreeEnvironment(); 21 | const inputRef = useRef(null); 22 | const submitButtonRef = useRef(null); 23 | const item = environment.items[props.itemIndex]; 24 | const [title, setTitle] = useState(environment.getItemTitle(item)); 25 | const callSoon = useCallSoon(true); 26 | 27 | const abort = () => { 28 | environment.onAbortRenamingItem?.(item, treeInformation.treeId); 29 | setRenamingItem(null); 30 | callSoon(() => { 31 | environment.setActiveTree(treeId); 32 | }); 33 | }; 34 | 35 | const confirm = () => { 36 | environment.onRenameItem?.(item, title, treeInformation.treeId); 37 | setRenamingItem(null); 38 | callSoon(() => { 39 | environment.setActiveTree(treeId); 40 | }); 41 | }; 42 | 43 | useSideEffect( 44 | () => { 45 | environment.setActiveTree(treeId); 46 | 47 | if (environment.autoFocus ?? true) { 48 | inputRef.current?.select(); 49 | inputRef.current?.focus?.(); 50 | } 51 | }, 52 | [environment, treeId], 53 | [] 54 | ); 55 | 56 | useHotkey( 57 | 'abortRenameItem', 58 | () => { 59 | abort(); 60 | }, 61 | true, 62 | true 63 | ); 64 | 65 | const inputProps: InputHTMLAttributes = { 66 | value: title, 67 | onChange: e => { 68 | setTitle(e.target.value); 69 | }, 70 | onBlur: e => { 71 | if (!e.relatedTarget || e.relatedTarget !== submitButtonRef.current) { 72 | abort(); 73 | } 74 | }, 75 | 'aria-label': 'New item name', 76 | tabIndex: 0, 77 | }; 78 | 79 | const submitButtonProps: HTMLProps = { 80 | onClick: e => { 81 | e.stopPropagation(); 82 | confirm(); 83 | }, 84 | }; 85 | 86 | const formProps: FormHTMLAttributes = { 87 | onSubmit: e => { 88 | e.preventDefault(); 89 | confirm(); 90 | }, 91 | }; 92 | 93 | return renderers.renderRenameInput({ 94 | item, 95 | inputRef, 96 | submitButtonProps, 97 | submitButtonRef, 98 | formProps, 99 | inputProps, 100 | }) as React.ReactElement; 101 | }; 102 | -------------------------------------------------------------------------------- /packages/core/src/uncontrolledEnvironment/CompleteTreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Disposable, 3 | TreeDataProvider, 4 | TreeItem, 5 | TreeItemIndex, 6 | } from '../types'; 7 | 8 | export class CompleteTreeDataProvider 9 | implements Required> 10 | { 11 | private provider: TreeDataProvider; 12 | 13 | constructor(provider: TreeDataProvider) { 14 | this.provider = provider; 15 | } 16 | 17 | public async getTreeItem(itemId: TreeItemIndex): Promise { 18 | return this.provider.getTreeItem(itemId); 19 | } 20 | 21 | public async getTreeItems(itemIds: TreeItemIndex[]): Promise { 22 | return this.provider.getTreeItems 23 | ? this.provider.getTreeItems(itemIds) 24 | : Promise.all(itemIds.map(id => this.provider.getTreeItem(id))); 25 | } 26 | 27 | public async onChangeItemChildren( 28 | itemId: TreeItemIndex, 29 | newChildren: TreeItemIndex[] 30 | ): Promise { 31 | return this.provider.onChangeItemChildren?.(itemId, newChildren); 32 | } 33 | 34 | public onDidChangeTreeData( 35 | listener: (changedItemIds: TreeItemIndex[]) => void 36 | ): Disposable { 37 | return this.provider.onDidChangeTreeData 38 | ? this.provider.onDidChangeTreeData(listener) 39 | : { dispose: () => {} }; 40 | } 41 | 42 | public async onRenameItem(item: TreeItem, name: string): Promise { 43 | return this.provider.onRenameItem?.(item, name); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/uncontrolledEnvironment/StaticTreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Disposable, 3 | ExplicitDataSource, 4 | TreeDataProvider, 5 | TreeItem, 6 | TreeItemIndex, 7 | } from '../types'; 8 | import { EventEmitter } from '../EventEmitter'; 9 | 10 | export class StaticTreeDataProvider implements TreeDataProvider { 11 | private data: ExplicitDataSource; 12 | 13 | /** Emit an event with the changed item ids to notify the tree view about changes. */ 14 | public readonly onDidChangeTreeDataEmitter = new EventEmitter< 15 | TreeItemIndex[] 16 | >(); 17 | 18 | private setItemName?: (item: TreeItem, newName: string) => TreeItem; 19 | 20 | constructor( 21 | items: Record>, 22 | setItemName?: (item: TreeItem, newName: string) => TreeItem 23 | // private implicitItemOrdering?: (itemA: TreeItem, itemB: TreeItem) => number, 24 | ) { 25 | this.data = { items }; 26 | this.setItemName = setItemName; 27 | } 28 | 29 | public async getTreeItem(itemId: TreeItemIndex): Promise { 30 | return this.data.items[itemId]; 31 | } 32 | 33 | public async onChangeItemChildren( 34 | itemId: TreeItemIndex, 35 | newChildren: TreeItemIndex[] 36 | ): Promise { 37 | this.data.items[itemId].children = newChildren; 38 | this.onDidChangeTreeDataEmitter.emit([itemId]); 39 | } 40 | 41 | public onDidChangeTreeData( 42 | listener: (changedItemIds: TreeItemIndex[]) => void 43 | ): Disposable { 44 | const handlerId = this.onDidChangeTreeDataEmitter.on(payload => 45 | listener(payload) 46 | ); 47 | return { dispose: () => this.onDidChangeTreeDataEmitter.off(handlerId) }; 48 | } 49 | 50 | public async onRenameItem(item: TreeItem, name: string): Promise { 51 | if (this.setItemName) { 52 | this.data.items[item.index] = this.setItemName(item, name); 53 | // this.onDidChangeTreeDataEmitter.emit(item.index); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/useCallSoon.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * React hook that schedules a callback to be run "soon" and will cancel the 5 | * callback if it is still pending when the component is unmounted. 6 | * 7 | * @returns A function that can be used to schedule a deferred callback. 8 | */ 9 | export function useCallSoon(dontClean = false): (callback: () => void) => void { 10 | const handleRef = useRef(new Array()); 11 | 12 | useEffect(() => { 13 | if (dontClean) { 14 | return () => {}; 15 | } 16 | 17 | const handles = handleRef.current; 18 | return () => handles.forEach(handle => cancelAnimationFrame(handle)); 19 | }, [dontClean, handleRef]); 20 | 21 | return useCallback( 22 | (callback: () => void) => { 23 | const handle = requestAnimationFrame(() => { 24 | handleRef.current.splice(handleRef.current.indexOf(handle), 1); 25 | 26 | callback(); 27 | }); 28 | 29 | handleRef.current.push(handle); 30 | }, 31 | [handleRef] 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/core/src/useGetOriginalItemOrder.ts: -------------------------------------------------------------------------------- 1 | import { useTreeEnvironment } from './controlledEnvironment/ControlledTreeEnvironment'; 2 | import { TreeItem } from './types'; 3 | import { useStableHandler } from './useStableHandler'; 4 | 5 | export const useGetOriginalItemOrder = () => { 6 | const env = useTreeEnvironment(); 7 | return useStableHandler((treeId: string, items: TreeItem[]) => 8 | items 9 | .map( 10 | item => 11 | [ 12 | item, 13 | env.linearItems[treeId].findIndex( 14 | linearItem => linearItem.item === item.index 15 | ), 16 | ] as const 17 | ) 18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | .sort(([_, aPos], [_2, bPos]) => aPos - bPos) 20 | .map(([item]) => item) 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/useHtmlElementEventListener.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useStableHandler } from './useStableHandler'; 3 | 4 | export const useHtmlElementEventListener = < 5 | K extends keyof HTMLElementEventMap 6 | >( 7 | element: HTMLElement | Document | undefined, 8 | type: K, 9 | listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any 10 | ) => { 11 | const stableListener = useStableHandler(listener); 12 | useEffect(() => { 13 | if (element) { 14 | element.addEventListener(type, stableListener as any); 15 | return () => element.removeEventListener(type, stableListener as any); 16 | } 17 | 18 | return () => {}; 19 | }, [element, stableListener, type]); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/core/src/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useIsMounted = () => { 4 | const mountedRef = useRef(false); 5 | useEffect(() => { 6 | mountedRef.current = true; 7 | return () => { 8 | mountedRef.current = false; 9 | }; 10 | }, []); 11 | return mountedRef; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/useRefCopy.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export const useRefCopy = (value: T) => { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | return ref; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/core/src/useSideEffect.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useEffect, useRef } from 'react'; 2 | 3 | export const useSideEffect = ( 4 | effect: Function, 5 | deps: DependencyList, 6 | changeOn: DependencyList 7 | ): void => { 8 | const previousRef = useRef(); 9 | useEffect(() => { 10 | if (!previousRef.current) { 11 | previousRef.current = [...changeOn]; 12 | effect(); 13 | } else { 14 | const changed = previousRef.current.some((v, i) => v !== changeOn[i]); 15 | if (changed) { 16 | previousRef.current = [...changeOn]; 17 | effect(); 18 | } 19 | } 20 | // eslint-disable-next-line react-hooks/exhaustive-deps 21 | }, [...deps, ...changeOn]); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/useStableHandler.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useRefCopy } from './useRefCopy'; 3 | 4 | export const useStableHandler = any>( 5 | handler: T 6 | ) => { 7 | const handlerRef = useRefCopy(handler); 8 | // eslint-disable-next-line react-hooks/exhaustive-deps 9 | return useCallback(((...args) => handlerRef.current(...args)) as T, [ 10 | handlerRef, 11 | ]); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const buildMapForTrees = ( 2 | treeIds: string[], 3 | build: (treeId: string) => T 4 | ): { [treeId: string]: T } => 5 | treeIds 6 | .map(id => [id, build(id)] as const) 7 | .reduce((a, [id, obj]) => ({ ...a, [id]: obj }), {}); 8 | 9 | export const getDocument = () => 10 | typeof document !== 'undefined' ? document : undefined; 11 | -------------------------------------------------------------------------------- /packages/core/src/waitFor.ts: -------------------------------------------------------------------------------- 1 | export const waitFor = ( 2 | check: () => boolean, 3 | intervalMs = 50, 4 | timeoutMs = 10000 5 | ) => 6 | new Promise(resolve => { 7 | if (check()) { 8 | resolve(); 9 | } 10 | 11 | let complete: () => void; 12 | 13 | const interval = setInterval(() => { 14 | if (check()) { 15 | complete(); 16 | } 17 | }, intervalMs); 18 | 19 | const timeout = setTimeout(() => { 20 | complete(); 21 | }, timeoutMs); 22 | 23 | complete = () => { 24 | clearInterval(interval); 25 | clearTimeout(timeout); 26 | resolve(); 27 | }; 28 | }); 29 | -------------------------------------------------------------------------------- /packages/core/test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TestUtil'; 2 | export * from './testTree'; 3 | -------------------------------------------------------------------------------- /packages/core/test/helpers/setup.ts: -------------------------------------------------------------------------------- 1 | globalThis.IS_REACT_ACT_ENVIRONMENT = true; 2 | 3 | // eslint-disable-next-line no-console 4 | const originalConsoleError = console.error; 5 | // eslint-disable-next-line no-console 6 | console.error = (...args) => { 7 | const firstArg = args[0]; 8 | if ( 9 | typeof args[0] === 'string' && 10 | (args[0].startsWith( 11 | // eslint-disable-next-line quotes 12 | "Warning: It looks like you're using the wrong act()" 13 | ) || 14 | firstArg.startsWith( 15 | 'Warning: The current testing environment is not configured to support act' 16 | ) || 17 | firstArg.startsWith('Warning: You seem to have overlapping act() calls')) 18 | ) { 19 | return; 20 | } 21 | originalConsoleError.apply(console, args); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/test/helpers/testTree.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemIndex } from '../../src'; 2 | 3 | type ConvenientItem = Omit>, 'children'>; 4 | type ConvenientItemData = Omit, 'children'> & { 5 | children?: ConvenientItemData[]; 6 | }; 7 | 8 | const buildItem = ( 9 | index: TreeItemIndex, 10 | children?: ConvenientItemData[], 11 | props: ConvenientItem = {} 12 | ): ConvenientItemData => ({ 13 | index, 14 | isFolder: props.isFolder ?? !!children, 15 | children, 16 | canMove: true, 17 | canRename: true, 18 | ...props, 19 | data: `${index}`, 20 | }); 21 | 22 | const template = buildItem('root', [ 23 | buildItem('target-parent', [ 24 | buildItem('before'), 25 | buildItem('target'), 26 | buildItem('after'), 27 | ]), 28 | buildItem('a', [ 29 | buildItem('aa', [ 30 | buildItem('aaa'), 31 | buildItem('aab'), 32 | buildItem('aac'), 33 | buildItem('aad'), 34 | ]), 35 | buildItem('ab', [ 36 | buildItem('aba'), 37 | buildItem('abb'), 38 | buildItem('abc'), 39 | buildItem('abd'), 40 | ]), 41 | buildItem('ac', [ 42 | buildItem('aca'), 43 | buildItem('acb'), 44 | buildItem('acc'), 45 | buildItem('acd'), 46 | ]), 47 | buildItem('ad', [ 48 | buildItem('ada'), 49 | buildItem('adb'), 50 | buildItem('adc'), 51 | buildItem('add'), 52 | ]), 53 | ]), 54 | buildItem('b', [ 55 | buildItem('ba', [ 56 | buildItem('baa'), 57 | buildItem('bab'), 58 | buildItem('bac'), 59 | buildItem('bad'), 60 | ]), 61 | buildItem('bb', [ 62 | buildItem('bba'), 63 | buildItem('bbb'), 64 | buildItem('bbc'), 65 | buildItem('bbd'), 66 | ]), 67 | buildItem('bc', [ 68 | buildItem('bca'), 69 | buildItem('bcb'), 70 | buildItem('bcc'), 71 | buildItem('bcd'), 72 | ]), 73 | buildItem('bd', [ 74 | buildItem('bda'), 75 | buildItem('bdb'), 76 | buildItem('bdc'), 77 | buildItem('bdd'), 78 | ]), 79 | ]), 80 | buildItem('c', [ 81 | buildItem('ca', [ 82 | buildItem('caa'), 83 | buildItem('cab'), 84 | buildItem('cac'), 85 | buildItem('cad'), 86 | ]), 87 | buildItem('cb', [ 88 | buildItem('cba'), 89 | buildItem('cbb'), 90 | buildItem('cbc'), 91 | buildItem('cbd'), 92 | ]), 93 | buildItem('cc', [ 94 | buildItem('cca'), 95 | buildItem('ccb'), 96 | buildItem('ccc'), 97 | buildItem('ccd'), 98 | ]), 99 | buildItem('cd', [ 100 | buildItem('cda'), 101 | buildItem('cdb'), 102 | buildItem('cdc'), 103 | buildItem('cdd'), 104 | ]), 105 | ]), 106 | buildItem('deep1', [ 107 | buildItem('deep2', [ 108 | buildItem('deep3', [buildItem('deep4', [buildItem('deep5')])]), 109 | ]), 110 | ]), 111 | buildItem('special', [ 112 | buildItem('cannot-move', undefined, { canMove: false }), 113 | buildItem('cannot-rename', undefined, { canRename: false }), 114 | ]), 115 | ]); 116 | 117 | const readTemplate = ( 118 | templateData: ConvenientItemData[], 119 | data: Record> = {} 120 | ) => { 121 | for (const item of templateData) { 122 | // eslint-disable-next-line no-param-reassign 123 | data[item.index] = { 124 | ...item, 125 | children: item.children?.map(child => child.index), 126 | }; 127 | 128 | if (item.children) { 129 | readTemplate(item.children, data); 130 | } 131 | } 132 | return data; 133 | }; 134 | 135 | export const buildTestTree = () => readTemplate([template]); 136 | -------------------------------------------------------------------------------- /packages/core/test/navigation.spec.tsx: -------------------------------------------------------------------------------- 1 | import { act } from '@testing-library/react'; 2 | import { TestUtil } from './helpers'; 3 | 4 | describe('navigation', () => { 5 | describe('moving focus', () => { 6 | it('can move focus between items', async () => { 7 | const test = await new TestUtil().renderOpenTree(); 8 | await test.focusTree(); 9 | await test.clickItem('target'); 10 | await test.pressKeys('ArrowDown'); 11 | await test.expectFocused('after'); 12 | await test.pressKeys('ArrowDown'); 13 | await test.expectFocused('a'); 14 | await test.pressKeys('ArrowDown'); 15 | await test.expectFocused('aa'); 16 | await test.pressKeys('ArrowDown'); 17 | await test.expectFocused('aaa'); 18 | await test.pressKeys('ArrowDown'); 19 | await test.expectFocused('aab'); 20 | await test.pressKeys('ArrowDown'); 21 | await test.expectFocused('aac'); 22 | await test.expectTreeUnchanged(); 23 | }); 24 | 25 | it('can jump to top and bottom', async () => { 26 | const test = await new TestUtil().renderOpenTree(); 27 | await test.focusTree(); 28 | await test.clickItem('target'); 29 | await test.pressKeys('home'); 30 | await test.expectFocused('target-parent'); 31 | await test.pressKeys('end'); 32 | await test.expectFocused('cannot-rename'); 33 | await test.expectTreeUnchanged(); 34 | }); 35 | }); 36 | 37 | describe('opening and closing folders', () => { 38 | it('can open and close folders', async () => { 39 | const test = await new TestUtil().renderOpenTree(); 40 | await test.focusTree(); 41 | await test.clickItem('target'); 42 | await test.pressKeys('ArrowLeft'); 43 | await test.expectItemsExpanded('target-parent'); 44 | await test.expectFocused('target-parent'); 45 | await test.pressKeys('ArrowLeft'); 46 | await test.expectItemsCollapsed('target-parent'); 47 | await test.expectFocused('target-parent'); 48 | await test.pressKeys('ArrowRight'); 49 | await test.expectItemsExpanded('target-parent'); 50 | await test.expectFocused('target-parent'); 51 | await test.pressKeys('ArrowRight'); 52 | await test.expectFocused('before'); 53 | await test.expectTreeUnchanged(); 54 | }); 55 | }); 56 | 57 | describe('expand and collapse all', () => { 58 | it('can expand and collapse all', async () => { 59 | const test = await new TestUtil().renderTree(); 60 | await test.waitForStableLinearItems(); 61 | await act(async () => { 62 | await test.treeRef?.expandAll(); 63 | }); 64 | await test.expectVisible('aaa', 'bbb', 'ccc', 'deep5'); 65 | // await waitFor(async () => { 66 | // await test.expectOpenViewState(); 67 | // }); 68 | await act(async () => { 69 | await test.treeRef?.collapseAll(); 70 | }); 71 | await test.expectNotVisible('aaa', 'aa', 'target', 'deep5'); 72 | await test.expectViewState({ 73 | expandedItems: [], 74 | focusedItem: 'target-parent', 75 | }); 76 | 77 | await test.expectTreeUnchanged(); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lukasbach/tsconfig/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib/cjs", 5 | "noImplicitAny": false 6 | }, 7 | "exclude": ["src/stories", "**/*.spec.*", "test"], 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as webpack from 'webpack'; 3 | 4 | const config: webpack.Configuration = { 5 | resolve: { 6 | extensions: ['.tsx', '.ts', '.js'], 7 | mainFields: ['main', 'module', 'browser'], 8 | }, 9 | entry: './src/index.ts', 10 | target: 'web', 11 | devtool: 'source-map', 12 | mode: 'production', 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|ts|tsx)$/, 17 | exclude: /node_modules/, 18 | use: 'ts-loader', 19 | }, 20 | ], 21 | }, 22 | output: { 23 | path: path.resolve(__dirname, 'lib'), 24 | filename: 'bundle.js', 25 | library: 'ReactComplexTree', 26 | libraryTarget: 'umd', 27 | }, 28 | externals: { 29 | 'react': 'React', 30 | 'react-dom': 'ReactDOM', 31 | }, 32 | }; 33 | 34 | export default config; 35 | -------------------------------------------------------------------------------- /packages/demodata/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /packages/demodata/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demodata", 3 | "version": "2.6.1", 4 | "main": "lib/index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "git@github.com:lukasbach/react-complex-tree.git", 8 | "directory": "packages/demodata" 9 | }, 10 | "author": "Lukas Bach ", 11 | "license": "MIT", 12 | "private": true, 13 | "devDependencies": { 14 | "@babel/core": "^7.14.0", 15 | "@babel/preset-env": "^7.14.1", 16 | "@babel/preset-react": "^7.13.13", 17 | "@babel/preset-typescript": "^7.13.0", 18 | "@lukasbach/tsconfig": "^0.1.0", 19 | "@types/jest": "^27.4.1", 20 | "@types/react": "^18.0.14", 21 | "@types/react-dom": "^18.0.7", 22 | "babel-jest": "^27.5.1", 23 | "babel-loader": "^9.1.0", 24 | "react": "^18.2.0", 25 | "react-dom": "^18.2.0", 26 | "react-test-renderer": "^18.2.0", 27 | "ts-node": "^10.7.0", 28 | "typescript": "4.9.3" 29 | }, 30 | "scripts": { 31 | "build": "tsc" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/demodata/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './treeData'; 2 | -------------------------------------------------------------------------------- /packages/demodata/src/treeData.ts: -------------------------------------------------------------------------------- 1 | const readTemplate = (template: any, data: any = { items: {} }): any => { 2 | for (const [key, value] of Object.entries(template)) { 3 | // eslint-disable-next-line no-param-reassign 4 | data.items[key] = { 5 | index: key, 6 | canMove: true, 7 | isFolder: value !== null, 8 | children: 9 | value !== null 10 | ? Object.keys(value as Record) 11 | : undefined, 12 | data: key, 13 | canRename: true, 14 | }; 15 | 16 | if (value !== null) { 17 | readTemplate(value, data); 18 | } 19 | } 20 | return data; 21 | }; 22 | 23 | const shortTreeTemplate = { 24 | root: { 25 | container: { 26 | item0: null, 27 | item1: null, 28 | item2: null, 29 | item3: { 30 | inner0: null, 31 | inner1: null, 32 | inner2: null, 33 | inner3: null, 34 | }, 35 | item4: null, 36 | item5: null, 37 | }, 38 | }, 39 | }; 40 | 41 | const longTreeTemplate = { 42 | root: { 43 | Fruit: { 44 | Apple: null, 45 | Orange: null, 46 | Lemon: null, 47 | Berries: { 48 | Red: { 49 | Strawberry: null, 50 | Raspberry: null, 51 | }, 52 | Blue: { 53 | Blueberry: null, 54 | }, 55 | Black: { 56 | Blackberry: null, 57 | }, 58 | }, 59 | Banana: null, 60 | }, 61 | Meals: { 62 | America: { 63 | SmashBurger: null, 64 | Chowder: null, 65 | Ravioli: null, 66 | MacAndCheese: null, 67 | Brownies: null, 68 | }, 69 | Europe: { 70 | Risotto: null, 71 | Spaghetti: null, 72 | Pizza: null, 73 | Weisswurst: null, 74 | Spargel: null, 75 | }, 76 | Asia: { 77 | Curry: null, 78 | PadThai: null, 79 | Jiaozi: null, 80 | Sushi: null, 81 | }, 82 | Australia: { 83 | PotatoWedges: null, 84 | PokeBowl: null, 85 | LemonCurd: null, 86 | KumaraFries: null, 87 | }, 88 | }, 89 | Desserts: { 90 | Cookies: null, 91 | IceCream: null, 92 | }, 93 | Drinks: { 94 | PinaColada: null, 95 | Cola: null, 96 | Juice: null, 97 | }, 98 | }, 99 | }; 100 | 101 | const autoDemoTemplate = { 102 | root: { 103 | Fruit: { 104 | Apple: null, 105 | Orange: null, 106 | Lemon: null, 107 | Berries: { 108 | Strawberry: null, 109 | Blueberry: null, 110 | }, 111 | Banana: null, 112 | }, 113 | Meals: { 114 | America: { 115 | SmashBurger: null, 116 | Chowder: null, 117 | Ravioli: null, 118 | MacAndCheese: null, 119 | Brownies: null, 120 | }, 121 | Europe: { 122 | Risotto: null, 123 | Spaghetti: null, 124 | Pizza: null, 125 | Weisswurst: null, 126 | Spargel: null, 127 | }, 128 | Asia: { 129 | Curry: null, 130 | PadThai: null, 131 | Jiaozi: null, 132 | Sushi: null, 133 | }, 134 | Australia: { 135 | PotatoWedges: null, 136 | PokeBowl: null, 137 | LemonCurd: null, 138 | KumaraFries: null, 139 | }, 140 | }, 141 | Desserts: { 142 | Cookies: null, 143 | IceCream: null, 144 | }, 145 | Drinks: { 146 | PinaColada: null, 147 | Cola: null, 148 | Juice: null, 149 | }, 150 | }, 151 | }; 152 | 153 | export const longTree = readTemplate(longTreeTemplate); 154 | export const shortTree = readTemplate(shortTreeTemplate); 155 | export const autoDemoTree = readTemplate(autoDemoTemplate); 156 | -------------------------------------------------------------------------------- /packages/demodata/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lukasbach/tsconfig/tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "lib" 5 | }, 6 | "exclude": ["src/stories"], 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | docs/api 11 | typedoc-sidebar.js 12 | 13 | # Misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /packages/docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 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 | ```console 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 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | 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. 34 | -------------------------------------------------------------------------------- /packages/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/docs/docs/advanced/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Advanced Guides", 3 | "position": 3 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Guides", 3 | "position": 2 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/accessibility.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 11.5 3 | --- 4 | 5 | import { StoryEmbed } from '../../src/components/StoryEmbed'; 6 | 7 | # Accessibility 8 | 9 | React Complex Tree provides several features to make the tree component accessible to all users. 10 | 11 | ## DOM structure compliant with W3C's recommendations 12 | 13 | The W3C defines specific 14 | [guidelines for how a tree view should be structured to be accessible](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-2/treeview-2a.html). 15 | The guideline is fulfilled by the default render methods that are provided by React Complex Tree. If you 16 | implement custom render methods, you will need to make sure that the general DOM structure still complies 17 | to the guidelines (i.e. by using nested `ul`-lists), however even then React Complex Tree does most of the work 18 | for you by providing most DOM attributes such as `aria`-tags, role attributes and event handlers as props for 19 | the render methods. [Look into the documentation on custom render hooks to find out more](/docs/guides/rendering). 20 | 21 | ## Complete Control via keybindings 22 | 23 | All features can be accessed via keybindings, including 24 | [drag and drop](/docs/guides/keyboard#keyboard-bound-drag-and-drop-sequences), searching and renaming. 25 | [Find out more in the documentation on keyboard bindings.](/docs/guides/keyboard) 26 | 27 | ## Right-To-Left Mode (RTL) 28 | 29 | The library doesn't make any assumptions about how you render your tree, and you can fairly easily implement 30 | RTL mode in custom renderers yourself. However, if you are using the built-in default renderers, you can also 31 | enable RTL mode by providing the renderers with an `rtl` flag. Add `{...createDefaultRenderers(10, true)}` 32 | to either the `Tree` or `TreeEnvironment` component to enable RTL mode. 33 | 34 | ```typescript jsx 35 | import { createDefaultRenderers } from 'react-complex-tree'; 36 | 37 | 38 | {...createDefaultRenderers(10, true)} 39 | > 40 | 41 | 42 | ``` 43 | 44 | 45 | 46 | ## Live descriptors 47 | 48 | A visually hidden live section is rendered at the top of the tree that explains the state of the tree 49 | to screen readers. Screen readers are notified about updates to the state of the tree. This is particularly 50 | important when using drag-and-drop features via keyboard interactions. 51 | 52 | 53 | 54 | Live descriptors are displayed by default. They can be turned off by providing a `showLiveDescription` prop to 55 | the environment with the value `false`. They can be further customized via additional props. 56 | 57 | ### Custom descriptor texts 58 | 59 | Provide a `liveDescriptors` prop to the environment to define custom descriptor texts. How descriptors are named 60 | is described [in the respective API document](/docs/api/interfaces/LiveDescriptors). This is helpful for 61 | localizing the descriptors to different languages. 62 | 63 | When defining descriptors, the following substrings can be used as variables that are replaced during runtime: 64 | 65 | - `{treeLabel}`: the label provided to the tree component 66 | - `{keybinding:bindingname}`: a specific keybinding. `bindingname` needs to be a key of the [`KeyboardBindings` interface](/docs/api/interfaces/KeyboardBindings). 67 | - `{dragTarget}`: If currently dragging, a description of the drag target. 68 | - `{dragItems}`: A list of item titles of items that are currently being dragged, seperated by commas. 69 | - `{renamingItem}`: If currently renaming an item, the title of the item that is being renamed. 70 | 71 | 72 | 73 | ### Custom Keybindings 74 | 75 | If custom keybindings are provided to the tree environment, live descriptors do not need to be updated as they 76 | automatically include the correct bindings. 77 | 78 | 79 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/autodemo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 13 3 | --- 4 | 5 | # Auto Demo Component 6 | 7 | :::note 8 | Documentation coming soon... 9 | ::: 10 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/blueprintjs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 12 3 | --- 4 | 5 | import { StoryEmbed } from '../../src/components/StoryEmbed'; 6 | 7 | # BlueprintJS Renderers 8 | 9 | We also provide default renderers to achieve a visual styling according to the BlueprintJS 10 | React framework. 11 | 12 | ```typescript jsx 13 | import { renderers as bpRenderers } from 'react-complex-tree-blueprintjs-renderers'; 14 | 15 | {/* trees etc... */}; 16 | ``` 17 | 18 | 22 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/controlled-environment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import { longTree } from 'demodata'; 6 | import { PropTable } from '../../src/components/PropTable'; 7 | 8 | # Controlled Environment 9 | 10 | A controlled environment provides more flexibility and more possibilities for customization, but 11 | you need to implement your own logic for managing and retaining the view state of each tree, i.e. 12 | which items are expanded, selected, focused etc. 13 | 14 | Furthermore, you need to provide all items directly and, if not all items are available at the start, 15 | implement custom logic to asynchronously load items if they need to be displayed. 16 | 17 | ```jsx live 18 | function App() { 19 | return ( 20 | item.data} viewState={{}}> 21 | 22 | 23 | ); 24 | } 25 | ``` 26 | 27 | As you can see from the example, you can focus the tree and even search for items, but not change 28 | the focused or selected item. 29 | 30 | ## Managing the view state of the tree 31 | 32 | To implement this, provide an implementation for the [TreeChangeHandlers](/docs/api/interfaces/TreeChangeHandlers) 33 | interface and provide it as spreaded props to the controlled environment, then provide a 34 | [viewState](/docs/api/interfaces/TreeViewState) prop that defines the visual state of each tree by providing 35 | a [individual viewState](/docs/api/modules#IndividualTreeViewState) for every tree in your environment. 36 | 37 | ```jsx live 38 | function App() { 39 | const [focusedItem, setFocusedItem] = useState(); 40 | const [expandedItems, setExpandedItems] = useState([]); 41 | const [selectedItems, setSelectedItems] = useState([]); 42 | return ( 43 | item.data} 46 | viewState={{ 47 | ['tree-2']: { 48 | focusedItem, 49 | expandedItems, 50 | selectedItems, 51 | }, 52 | }} 53 | onFocusItem={item => setFocusedItem(item.index)} 54 | onExpandItem={item => setExpandedItems([...expandedItems, item.index])} 55 | onCollapseItem={item => 56 | setExpandedItems(expandedItems.filter(expandedItemIndex => expandedItemIndex !== item.index)) 57 | } 58 | onSelectItems={items => setSelectedItems(items)} 59 | > 60 | 61 | 62 | ); 63 | } 64 | ``` 65 | 66 | ## Lazy loading items 67 | 68 | As with the uncontrolled environment, you can provide an incomplete tree structure with 69 | missing items that are referenced in other items as children. The `onMissingItems` handler 70 | will be invoked if an item is expanded whose children are not yet loaded, so you can 71 | implement logic to load the items if that handler is invoked, and provide them alongside existing 72 | children in the next render iteration. 73 | 74 | ```jsx live 75 | function App() { 76 | const [focusedItem, setFocusedItem] = useState(); 77 | const [expandedItems, setExpandedItems] = useState([]); 78 | return ( 79 | item.data} 99 | viewState={{ 100 | ['tree-3']: { 101 | focusedItem, 102 | expandedItems, 103 | }, 104 | }} 105 | onFocusItem={item => setFocusedItem(item.index)} 106 | onExpandItem={item => setExpandedItems([...expandedItems, item.index])} 107 | onMissingItems={items => alert(`We should now load the items ${items.join(', ')}...`)} 108 | > 109 | 110 | 111 | ); 112 | } 113 | ``` 114 | 115 | ## Component Props 116 | 117 | The props for the `ControlledTreeEnvironment` are as follows: 118 | 119 | 120 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/keyboard.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 9 3 | --- 4 | 5 | # Keyboard Bindings 6 | 7 | Almost all features provided by React Complex Tree are accessible by using keyboard interactions. Default 8 | bindings are provided by the framework, but can be customized via props on the tree environment by supplying 9 | a `keyboardBindings` prop to the environment, which implements the 10 | [`KeyboardBindings` interface](/docs/api/interfaces/KeyboardBindings). 11 | 12 | ## Default bindings 13 | 14 | :::info 15 | The default bindings are defined in the source code 16 | [here](https://github.com/lukasbach/react-complex-tree/blob/main/packages/core/src/hotkeys/defaultKeyboardBindings.ts). 17 | ::: 18 | 19 | | Interaction | Default Binding | Binding Symbol | Note | 20 | | ---------------------------------------------- | -------------------- | ------------------------- | --------------------------------------------------------------------- | 21 | | Expand Siblings | `CONTROL + *` | `expandSiblings` | Not yet implemented | 22 | | Move focus to first item in tree | `HOME` | `moveFocusToFirstItem` | | 23 | | Move focus to last item in tree | `END` | `moveFocusToLastItem` | | 24 | | Execute primary action for selected items | `ENTER` | `primaryAction` | Calls the `onPrimaryAction` hook provided to the environment. | 25 | | Start renaming focused item | `F2` | `renameItem` | Renaming is completed by submitting the form, i.e. by pressing enter. | 26 | | Abort renaming focused item | `ESCAPE` | `abortRenameItem` | Blurring the input also aborts renaming. | 27 | | Toggle the select-state of the focused item | `CONTROL + SPACE` | `toggleSelectItem` | | 28 | | Abort search and hide the search input | `ESCAPE` or `ENTER` | `abortSearch` | | 29 | | Bring up the search input and focus it | None | `startSearch` | By default, searching can be started by pressing any letter button. | 30 | | Select all items | `CONTROL + A` | `selectAll` | | 31 | | Start keyboard-bound Drag-and-Drop sequence | `CONTROL + SHIFT +D` | `startProgrammaticDnd` | | 32 | | Complete keyboard-bound Drag-and-Drop sequence | `ENTER` | `completeProgrammaticDnd` | | 33 | | Abort keyboard-bound Drag-and-Drop sequence | `ESCAPE` | `abortProgrammaticDnd` | | 34 | 35 | ## Keyboard-bound Drag-and-Drop sequences 36 | 37 | Drag and Drop is also controllable via keyboard. This sequence can be started by pressing the 38 | hotkey `startProgrammaticDnd` which is `CONTROL + SHIFT + D` by default. Then, the user can press the up or down keys 39 | to select a target location. Moving the focus to a different tree with Tab is also possible. To complete the drop, 40 | the hotkey `completeProgrammaticDnd` (`ENTER`) needs to be pressed. The drag can also be aborted with the hotkey 41 | `abortProgrammaticDnd` (`ESCAPE`). 42 | 43 | ## Programmatic interaction 44 | 45 | Most features, like moving the focus or selecting items, can programmatically be controlled by pulling a React Ref 46 | either from the tree environment or the tree itself, and acting on the Ref object. 47 | [Read the documentation on externally interacting with the tree via Refs](/docs/guides/refs) to find out more. 48 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/multiple-trees.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 8 3 | --- 4 | 5 | # Multiple Trees 6 | 7 | Multiple tree components can be added to a web app that can interact with one another. Drag and drop is possible 8 | across several tree components such that items can be dragged from one tree to another. Although trees maintain 9 | their own state on search and renaming, only one search input or renaming input is active at once across several 10 | trees. Furthermore, trees maintain a shared state which makes synchronisation easier. 11 | 12 | The requirement for multiple trees sharing drag-and-drop capabilities and state is that they are placed within the 13 | same tree environment, i.e. either a shared `ControlledTreeEnvironment` or an `UncontrolledTreeEnvironment`. 14 | 15 | ## Example 16 | 17 | ```jsx live 18 | ({ ...item, data }))} 23 | getItemTitle={item => item.data} 24 | viewState={{}} 25 | > 26 |
    34 |
    40 | 41 |
    42 |
    48 | 49 |
    50 |
    56 | 57 |
    58 |
    59 |
    60 | ``` 61 | 62 | ## Different root items per tree 63 | 64 | It is not possible for several trees within the same environment to keep distinct states. 65 | However, each tree can use a different item as root item, meaning that the trees can 66 | still show different contents. 67 | 68 | ```jsx live 69 | ({ ...item, data }))} 74 | getItemTitle={item => item.data} 75 | viewState={{}} 76 | > 77 |
    85 |
    91 | 92 |
    93 |
    99 | 100 |
    101 |
    107 | 108 |
    109 |
    110 |
    111 | ``` 112 | 113 | ## More than one environment 114 | 115 | If you want several tree environments on one page that do not share state and dnd capabilities, 116 | you can do that if several restrictions are obliged. 117 | 118 | Each tree must have a unique tree ID which is even unique to trees in other environments 119 | within the page. 120 | 121 | Furthermore, an environment may not contain another tree environment. If this may cause problems 122 | to your anticipated DOM structure, you can leverage React Portals to render the environments 123 | disjunct from one another and still be free in the DOM structure you want to achieve. 124 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/refs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 11 3 | --- 4 | 5 | # External Interaction via Refs 6 | 7 | React Complex Tree provides comprehensive imperative interaction capabilities via React Refs. 8 | You can pull a Ref either from the environment or the tree, and act on those to manipulate the 9 | state of the trees. 10 | 11 | :::info 12 | 13 | - A ref on the environment provides a [`TreeEnvironmentRef`](/docs/api/interfaces/TreeEnvironmentRef). 14 | - A ref on the tree provides a [`TreeRef`](/docs/api/interfaces/TreeRef). 15 | 16 | Look into the respective interfaces via the links above to see all capabilities. 17 | ::: 18 | 19 | Note: Both the `UncontrolledTreeEnvironment` and the `ControlledTreeEnvironment` yield a 20 | [`TreeEnvironmentRef`](/docs/api/interfaces/TreeEnvironmentRef). 21 | 22 | ## Example 23 | 24 | ```jsx live 25 | function App() { 26 | const environment = useRef(); 27 | const tree = useRef(); 28 | const getFocus = () => environment.current.viewState['tree-1'].focusedItem || 'Fruit'; 29 | console.log(environment); 30 | 31 | return ( 32 | ({ ...item, data }))} 38 | getItemTitle={item => item.data} 39 | viewState={{ 40 | 'tree-1': { 41 | expandedItems: ['Fruit'], 42 | }, 43 | }} 44 | > 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
    55 | 56 |
    57 | ); 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/renaming.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | --- 4 | 5 | # Renaming Functionality 6 | 7 | React Complex Tree provides native renaming capabilities which are enabled by default. They can be disabled 8 | by providing the `canRename` prop with the value `false`. 9 | 10 | If renaming is enabled, the user can hit the hotkey `F2`, and the title component of the focused item 11 | is replaced with an input. The rendering of that component can be customized with 12 | [custom render hooks](/docs/guides/rendering). The hotkey [can also be customized](/docs/guides/keyboard). 13 | 14 | When the input is blurred, i.e. the user clicks on somewhere else, or presses `escape`, the renaming is aborted, 15 | the input is replaced with the title component again and changes made to the title are omitted. If the user clicks 16 | on the submit button or submits the input by pressing enter, the name of the item is changed. 17 | 18 | ## Example 19 | 20 | ```jsx live 21 | function App() { 22 | return ( 23 | ({ ...item, data }))} 25 | getItemTitle={item => item.data} 26 | viewState={{ 27 | 'tree-1': { 28 | expandedItems: ['Fruit', 'Meals'], 29 | }, 30 | }} 31 | > 32 | 33 | 34 | ); 35 | } 36 | ``` 37 | 38 | ## Example with disabled renaming 39 | 40 | If `canRename` is set to false, renaming is disabled. 41 | 42 | ```jsx live 43 | function App() { 44 | return ( 45 | ({ ...item, data }))} 47 | getItemTitle={item => item.data} 48 | viewState={{ 49 | 'tree-2': { 50 | expandedItems: ['Fruit', 'Meals'], 51 | }, 52 | }} 53 | canRename={false} 54 | > 55 | 56 | 57 | ); 58 | } 59 | ``` 60 | 61 | ## Reacting to rename events 62 | 63 | ### Via `onRenameProp` 64 | 65 | Both the `UncontrolledTreeEnvironment` and `ControlledTreeEnvironment` provide a `onRenameItem` prop 66 | to which can be reacted to. 67 | 68 | ```jsx live 69 | function App() { 70 | return ( 71 | ({ ...item, data }))} 73 | getItemTitle={item => item.data} 74 | viewState={{ 75 | 'tree-3': { 76 | expandedItems: ['Fruit', 'Meals'], 77 | }, 78 | }} 79 | canRename={false} 80 | onRenameItem={(item, name) => alert(`${item.data} renamed to ${name}`)} 81 | > 82 | 83 | 84 | ); 85 | } 86 | ``` 87 | 88 | ### Via `StaticTreeDataProvider` 89 | 90 | The `StaticTreeDataProvider` also defines a method that is provided as the second argument to the constructor, 91 | which is invoked if a item is renamed. This method is expected to return the renamed item. 92 | 93 | ```jsx live 94 | function App() { 95 | return ( 96 | { 99 | alert(`${item.data} renamed to ${newName}`); 100 | return { ...item, data: newName }; 101 | }) 102 | } 103 | getItemTitle={item => item.data} 104 | viewState={{ 105 | 'tree-4': { 106 | expandedItems: ['Fruit', 'Meals'], 107 | }, 108 | }} 109 | canRename={false} 110 | > 111 | 112 | 113 | ); 114 | } 115 | ``` 116 | 117 | ### Via a custom tree data provider 118 | 119 | When implementing a custom tree data provider, a method `onRenameItem` can be implemented to react to rename 120 | events. [Read more on implementing custom tree data providers](/docs/guides/custom-data-provider) 121 | for more details. 122 | 123 | ## Programmatic interaction 124 | 125 | This feature can programmatically be controlled by pulling a React Ref either from the tree environment 126 | or the tree itself, and acting on the Ref object. [Read the documentation on externally interacting 127 | with the tree via Refs](/docs/guides/refs) to find out more. 128 | -------------------------------------------------------------------------------- /packages/docs/docs/guides/static-data-provider.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Static Tree Data Provider 6 | 7 | When using an uncontrolled environment, you need to provide your data by supplying a data provider. The 8 | easiest way to get started is using the [Static Tree Data Provider](/docs/api/classes/StaticTreeDataProvider). 9 | It allows you to provide your data as record which maps item ids to tree items, and gives you the possibility 10 | to react to changes in the tree structure, as well as inject your own changes through change events. 11 | 12 | :::info 13 | If you want to implement a custom data provider your own, you can find a comprehensive guide [here](/docs/guides/custom-data-provider). 14 | ::: 15 | 16 | The following example gives a good example of what is possible with static tree data providers. We will look 17 | into the details of the data provider below. 18 | 19 | ```jsx live 20 | function App() { 21 | const items = useMemo(() => ({ ...shortTree.items }), []); 22 | const dataProvider = useMemo( 23 | () => 24 | new StaticTreeDataProvider(items, (item, data) => ({ 25 | ...item, 26 | data, 27 | })), 28 | [items] 29 | ); 30 | 31 | const injectItem = () => { 32 | const rand = `${Math.random()}`; 33 | items[rand] = { data: 'New Item', index: rand }; 34 | items.root.children.push(rand); 35 | dataProvider.onDidChangeTreeDataEmitter.emit(['root']); 36 | }; 37 | 38 | const removeItem = () => { 39 | if (items.root.children.length === 0) return; 40 | items.root.children.pop(); 41 | dataProvider.onDidChangeTreeDataEmitter.emit(['root']); 42 | }; 43 | 44 | return ( 45 | item.data} 51 | viewState={{ 52 | 'tree-1': { 53 | expandedItems: [], 54 | }, 55 | }} 56 | > 57 | 60 | 63 | 64 | 65 | ); 66 | } 67 | ``` 68 | 69 | ## Creating the data provider with data 70 | 71 | First, create the data provider. You want to make sure it isn't recreated on re-renders, so memoize 72 | it in the component in which it is defined. 73 | 74 | ```tsx 75 | const dataProvider = useMemo( 76 | () => 77 | new StaticTreeDataProvider(items, (item, data) => ({ 78 | ...item, 79 | data, 80 | })), 81 | [items] 82 | ); 83 | ``` 84 | 85 | The items is a record mapping item ids to tree items, for example: 86 | 87 | ```typescript 88 | const items = [ 89 | { 90 | index: "item-id", 91 | data: { arbitraryData: 123, name: "Hello" }, 92 | children: ["item-id-1", "item-id-2"], 93 | isFolder: true 94 | } 95 | ] 96 | ``` 97 | 98 | Note that, whatever you provide to the `getItemTitle` prop is used to infer the item display name. 99 | 100 | ```ts jsx 101 | item.data.name} 103 | /> 104 | ``` 105 | 106 | ## Apply changes from outside 107 | 108 | You can apply changes to the underlying data source. Just make sure to let RCT know about that by 109 | emitting a change event on the affected items. Note that, if you add or remove items, the affected item 110 | is the parent item, not the added or removed items. 111 | 112 | ```ts 113 | const injectItem = () => { 114 | const rand = `${Math.random()}`; 115 | items[rand] = { data: 'New Item', index: rand }; 116 | items.root.children.push(rand); 117 | dataProvider.onDidChangeTreeDataEmitter.emit(['root']); 118 | }; 119 | 120 | const removeItem = () => { 121 | if (items.root.children.length === 0) return; 122 | items.root.children.pop(); 123 | dataProvider.onDidChangeTreeDataEmitter.emit(['root']); 124 | }; 125 | ``` 126 | 127 | ## Reacting to Drag Events 128 | 129 | Drag changes are always immediately applied to the visualization, so make sure to implement the `canDropAt` 130 | prop to customize if that should not work in all cases. The static tree data emits tree change events similar 131 | to the ones you would emit when applying changes from outside, so you can react to them in the same way. 132 | 133 | ```typescript 134 | dataProvider.onDidChangeTreeData(changedItemIds => { 135 | console.log(changedItemIds); 136 | }); 137 | ``` 138 | 139 | ## Reacting to Rename Events 140 | 141 | The second (optional) parameter of the static tree data provider lets you react to rename events. Note that 142 | you can customize whether renaming is possible in the first place through the `canRename` prop. 143 | 144 | ```typescript 145 | const dataProvider = new StaticTreeDataProvider(items, (item, newName) => { 146 | // Return the patched item with new item name here 147 | return { 148 | ...item, 149 | data: { ...item.data, name: newName }, 150 | }; 151 | }); 152 | ` 153 | ``` -------------------------------------------------------------------------------- /packages/docs/docs/guides/uncontrolled-environment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import { PropTable } from '../../src/components/PropTable'; 6 | 7 | # Uncontrolled Environment 8 | 9 | Using an uncontrolled environment to declare the state of your tree is mostly documented in 10 | the [Get Started](/docs/getstarted) document. You need to declare your data by implementing 11 | a [TreeDataProvider](/docs/api/interfaces/TreeDataProvider) that declares how tree items 12 | can be loaded by React Complex Tree. Alternatively, you can just provide a 13 | [StaticTreeDataProvider](/docs/api/classes/StaticTreeDataProvider) that contains a static 14 | reference to all available data. 15 | 16 | You can read more about implementing a custom TreeDataProvider 17 | [implementing a custom TreeDataProvider here](/docs/guides/custom-data-provider), as well as 18 | more details on how to use the static tree data provider. 19 | 20 | An example using a StaticTreeDataProvider looks like this: 21 | 22 | ```jsx live 23 | function App() { 24 | const items = { 25 | root: { 26 | index: 'root', 27 | isFolder: true, 28 | children: ['child1', 'child2'], 29 | data: 'Root item', 30 | }, 31 | child1: { 32 | index: 'child1', 33 | children: [], 34 | data: 'Child item 1', 35 | }, 36 | child2: { 37 | index: 'child2', 38 | isFolder: true, 39 | children: ['child3'], 40 | data: 'Child item 2', 41 | }, 42 | child3: { 43 | index: 'child3', 44 | children: [], 45 | data: 'Child item 3', 46 | }, 47 | }; 48 | 49 | return ( 50 | ({ ...item, data }))} 52 | getItemTitle={item => item.data} 53 | viewState={{}} 54 | > 55 | 56 | 57 | ); 58 | } 59 | ``` 60 | 61 | Note that for each item in the data structure representing the tree (`items` in this case), each object must have a field named `index` that has the same value as the key for that object. The `children` array can only refer to these values as well. 62 | 63 | ## Component Props 64 | 65 | The props for the `UncontrolledTreeEnvironment` are as follows: 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/docs/docs/react/ControlledTreeEnvironment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import { PropTable } from '../../src/components/PropTable'; 6 | 7 | # ControlledTreeEnvironment 8 | 9 | :::info 10 | More details on using the `ControlledTreeEnvironment` component are provided 11 | in the [Guide on Controlled Environments](/docs/guides/controlled-environment). 12 | ::: 13 | 14 | ## Import 15 | 16 | ```typescript 17 | import { ControlledTreeEnvironment } from "react-complex-tree"; 18 | ``` 19 | 20 | ## Props 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/docs/docs/react/Tree.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import { PropTable } from '../../src/components/PropTable'; 6 | 7 | # Tree 8 | 9 | ## Import 10 | 11 | ```typescript 12 | import { Tree } from "react-complex-tree"; 13 | ``` 14 | 15 | ## Props 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/docs/docs/react/UncontrolledTreeEnvironment.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import { PropTable } from '../../src/components/PropTable'; 6 | 7 | # UncontrolledTreeEnvironment 8 | 9 | :::info 10 | More details on using the `UncontrolledTreeEnvironment` component are provided 11 | in the [Guide on Uncontrolled Environments](/docs/guides/uncontrolled-environment). 12 | ::: 13 | 14 | ## Import 15 | 16 | ```typescript 17 | import { UncontrolledTreeEnvironment } from "react-complex-tree"; 18 | ``` 19 | 20 | ## Props 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/docs/docs/react/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "React Components", 3 | "position": 4 4 | } 5 | -------------------------------------------------------------------------------- /packages/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "2.6.1", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "2.2.0", 18 | "@docusaurus/preset-classic": "2.2.0", 19 | "@docusaurus/theme-live-codeblock": "^2.2.0", 20 | "@lukasbach/campaigns-react": "^0.1.0", 21 | "@mdx-js/react": "^1.6.21", 22 | "@svgr/webpack": "^6.5.1", 23 | "clsx": "^1.1.1", 24 | "demodata": "^2.6.1", 25 | "docusaurus-plugin-react-docgen-typescript": "^1.0.2", 26 | "docusaurus-plugin-typedoc": "^0.18.0", 27 | "file-loader": "^6.2.0", 28 | "iframe-resizer": "^4.3.2", 29 | "iframe-resizer-react": "^1.1.0", 30 | "prism-react-renderer": "^1.2.1", 31 | "react": "^18.2.0", 32 | "react-complex-tree": "^2.6.1", 33 | "react-complex-tree-autodemo": "^2.6.1", 34 | "react-complex-tree-blueprintjs-renderers": "^2.6.1", 35 | "react-docgen-typescript": "^2.2.2", 36 | "react-dom": "^18.2.0", 37 | "typedoc": "^0.23.18", 38 | "typedoc-plugin-markdown": "^3.13.6", 39 | "url-loader": "^4.1.1" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.5%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | module.exports = { 13 | // By default, Docusaurus generates a sidebar from the docs folder structure 14 | tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], 15 | 16 | // But you can create a sidebar manually 17 | /* 18 | tutorialSidebar: [ 19 | { 20 | type: 'category', 21 | label: 'Tutorial', 22 | items: ['hello'], 23 | }, 24 | ], 25 | */ 26 | }; 27 | -------------------------------------------------------------------------------- /packages/docs/src/components/CampaignBar.js: -------------------------------------------------------------------------------- 1 | import styles from './CampaignBar.module.css'; 2 | import React, { useState, useEffect } from 'react'; 3 | 4 | const localStorageKey = 'hide-campaign-bar'; 5 | const days = 1000 * 60 * 60 * 24; 6 | 7 | export default function CampaignBar() { 8 | const [hide, setHide] = useState(true); 9 | 10 | useEffect(() => { 11 | const hideDate = localStorage.getItem(localStorageKey); 12 | setHide(!!hideDate && parseInt(hideDate) > Date.now() - days * 3); 13 | }, []); 14 | 15 | // if (hide) { 16 | // return null; 17 | // } 18 | 19 | return ( 20 |
    21 | 22 |
    Headless Tree
    23 |
    A successor for react-complex-tree, Headless Tree, is now available!
    24 |
    25 | {/*
    { 29 | setHide(true); 30 | localStorage.setItem(localStorageKey, `${Date.now()}`); 31 | }} 32 | > 33 | × 34 |
    */} 35 |
    36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/docs/src/components/CampaignBar.module.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | display: flex; 3 | background-color: #597bac; 4 | color: white; 5 | font-size: 0.8em; 6 | padding: 6px 80px; 7 | } 8 | 9 | .bar:hover { 10 | background-color: #5273a3; 11 | } 12 | 13 | .bar a { 14 | color: inherit; 15 | } 16 | 17 | .bar a:hover { 18 | color: white !important; 19 | } 20 | 21 | .content { 22 | display: flex; 23 | align-self: center; 24 | flex-grow: 1; 25 | } 26 | 27 | .alsocheckout { 28 | margin-right: 30px; 29 | color: rgba(255, 255, 255, 0.7); 30 | } 31 | 32 | .title { 33 | margin-right: 8px; 34 | font-weight: bold; 35 | } 36 | 37 | .description { 38 | } 39 | 40 | .close { 41 | font-weight: bold; 42 | font-size: 1.4em; 43 | color: rgba(255, 255, 255, 0.7); 44 | } 45 | 46 | .close:hover { 47 | cursor: pointer; 48 | color: rgba(255, 255, 255, 1); 49 | } 50 | -------------------------------------------------------------------------------- /packages/docs/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | .features { 4 | display: flex; 5 | align-items: center; 6 | padding: 2rem 0; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /packages/docs/src/components/PropTable.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useDynamicImport } from 'docusaurus-plugin-react-docgen-typescript/dist/esm/hooks'; 3 | 4 | export const PropTable = ({ name }) => { 5 | const props = useDynamicImport(name); 6 | 7 | if (!props) { 8 | return null; 9 | } 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {Object.keys(props).map(key => { 24 | return ( 25 | 26 | 29 | 32 | 33 | 34 | 35 | 36 | ); 37 | })} 38 | 39 |
    NameTypeDefault ValueRequiredDescription
    27 | {key} 28 | 30 | {props[key].type?.name} 31 | {props[key].defaultValue && {props[key].defaultValue.value}}{props[key].required ? 'Yes' : 'No'}{props[key].description}
    40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/docs/src/components/StoryEmbed.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import IframeResizer from 'iframe-resizer-react'; 3 | 4 | export const StoryEmbed = ({ storyName, iframeProps }) => { 5 | const baseUrl = process.env.NODE_ENV === 'development' ? `http://localhost:6006` : `/storybook`; 6 | 7 | return ( 8 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | 10 | @import 'react-complex-tree/lib/style.css'; 11 | 12 | :root { 13 | --ifm-color-primary: #0366d6; 14 | --ifm-color-primary-dark: #035cc1; 15 | --ifm-color-primary-darker: #0357b6; 16 | --ifm-color-primary-darkest: #024796; 17 | --ifm-color-primary-light: #0370eb; 18 | --ifm-color-primary-lighter: #0375f6; 19 | --ifm-color-primary-lightest: #1e86fc; 20 | --ifm-code-font-size: 95%; 21 | } 22 | 23 | .docusaurus-highlight-code-line { 24 | background-color: rgba(0, 0, 0, 0.1); 25 | display: block; 26 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 27 | padding: 0 var(--ifm-pre-padding); 28 | } 29 | 30 | html[data-theme='dark'] .docusaurus-highlight-code-line { 31 | background-color: rgba(0, 0, 0, 0.3); 32 | } 33 | 34 | .hero--primary { 35 | --ifm-hero-background-color: #374e6c; 36 | } 37 | 38 | .footer a:not(.footer__link-item) { 39 | color: var(--ifm-color-primary-lightest); 40 | } 41 | 42 | div[class^='playgroundEditor'] { 43 | max-height: 500px; 44 | overflow-y: auto !important; 45 | } 46 | 47 | .rct-tree-root { 48 | color: #000; 49 | } 50 | .rct-tree-root li { 51 | margin-top: 0 !important; 52 | } 53 | .rct-tree-item-arrow { 54 | display: flex !important; 55 | } 56 | -------------------------------------------------------------------------------- /packages/docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import styles from './index.module.css'; 7 | import HomepageFeatures from '../components/HomepageFeatures'; 8 | import { StoryEmbed } from '../components/StoryEmbed'; 9 | import CampaignBar from '../components/CampaignBar'; 10 | import Head from '@docusaurus/Head'; 11 | 12 | function HomepageHeader() { 13 | const { siteConfig } = useDocusaurusContext(); 14 | return ( 15 |
    16 |
    17 |
    18 |
    19 |

    {siteConfig.title}

    20 |

    {siteConfig.tagline}

    21 |
    22 | 23 | Get Started 24 | 25 | 26 | More Demos 27 | 28 | 29 | API 30 | 31 |
    32 |
    33 |
    34 | 38 |
    39 |
    40 |
    41 |
    42 | ); 43 | } 44 | 45 | export default function Home() { 46 | const { siteConfig } = useDocusaurusContext(); 47 | return ( 48 | 49 | 50 | 51 |
    52 | 53 |
    54 | 55 | React Complex Tree 56 | 57 | 61 | 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 76 | 77 | 78 | 79 |
    80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /packages/docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | .heroBannerText { 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | color: white !important; 20 | } 21 | 22 | @media screen and (max-width: 966px) { 23 | .heroBanner { 24 | padding: 2rem; 25 | } 26 | } 27 | 28 | .buttons { 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | margin-bottom: 30px; 33 | } 34 | 35 | .buttons a, 36 | .buttons button { 37 | margin: 0 10px; 38 | } 39 | -------------------------------------------------------------------------------- /packages/docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /packages/docs/src/theme/ReactLiveScope/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | import { 10 | ControlledTreeEnvironment, 11 | StaticTreeDataProvider, 12 | Tree, 13 | UncontrolledTreeEnvironment, 14 | } from 'react-complex-tree'; 15 | import { longTree, shortTree } from 'demodata'; 16 | import { renderers as bpRenderers } from 'react-complex-tree-blueprintjs-renderers'; 17 | import { AutoDemo } from 'react-complex-tree-autodemo'; 18 | 19 | // Add react-live imports you need here 20 | const ReactLiveScope = { 21 | React, 22 | ...React, 23 | ControlledTreeEnvironment, 24 | UncontrolledTreeEnvironment, 25 | Tree, 26 | longTree, 27 | shortTree, 28 | StaticTreeDataProvider, 29 | AutoDemo, 30 | bpRenderers, 31 | }; 32 | 33 | export default ReactLiveScope; 34 | -------------------------------------------------------------------------------- /packages/docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/.nojekyll -------------------------------------------------------------------------------- /packages/docs/static/img/example/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/example/01.png -------------------------------------------------------------------------------- /packages/docs/static/img/example/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/example/02.png -------------------------------------------------------------------------------- /packages/docs/static/img/example/03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/example/03.png -------------------------------------------------------------------------------- /packages/docs/static/img/example/04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/example/04.png -------------------------------------------------------------------------------- /packages/docs/static/img/example/05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/example/05.png -------------------------------------------------------------------------------- /packages/docs/static/img/example/06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/example/06.png -------------------------------------------------------------------------------- /packages/docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/docs/static/img/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/logo.ai -------------------------------------------------------------------------------- /packages/docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/logo.png -------------------------------------------------------------------------------- /packages/docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /packages/docs/static/img/tutorial/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/tutorial/docsVersionDropdown.png -------------------------------------------------------------------------------- /packages/docs/static/img/tutorial/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lukasbach/react-complex-tree/1cd96090815b8b84090d554e8d1f77b561252546/packages/docs/static/img/tutorial/localeDropdown.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@lukasbach/tsconfig/tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "noImplicitAny": false 6 | }, 7 | "include": ["packages/**/src", "packages/**/test"] 8 | } 9 | --------------------------------------------------------------------------------