diff --git a/packages/controls/package.json b/packages/controls/package.json index 271d2967..f34be8e5 100644 --- a/packages/controls/package.json +++ b/packages/controls/package.json @@ -39,7 +39,8 @@ }, "dependencies": { "@reactflow/core": "workspace:*", - "classcat": "^5.0.3" + "classcat": "^5.0.3", + "zustand": "^4.3.1" }, "peerDependencies": { "react": ">=17", @@ -55,7 +56,9 @@ }, "rollup": { "globals": { - "classcat": "cc" + "classcat": "cc", + "zustand": "Zustand", + "zustand/shallow": "zustandShallow" }, "name": "ReactFlowControls" } diff --git a/packages/controls/src/Controls.tsx b/packages/controls/src/Controls.tsx index 7c529a9e..cf8a246f 100644 --- a/packages/controls/src/Controls.tsx +++ b/packages/controls/src/Controls.tsx @@ -1,5 +1,6 @@ import { memo, useEffect, useState, type FC, type PropsWithChildren } from 'react'; import cc from 'classcat'; +import { shallow } from 'zustand/shallow'; import { useStore, useStoreApi, useReactFlow, Panel, type ReactFlowState } from '@reactflow/core'; import PlusIcon from './Icons/Plus'; @@ -11,7 +12,11 @@ import ControlButton from './ControlButton'; import type { ControlProps } from './types'; -const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable || s.nodesConnectable || s.elementsSelectable; +const selector = (s: ReactFlowState) => ({ + isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable, + minZoomReached: s.transform[2] <= s.minZoom, + maxZoomReached: s.transform[2] >= s.maxZoom, +}); const Controls: FC> = ({ style, @@ -29,7 +34,7 @@ const Controls: FC> = ({ }) => { const store = useStoreApi(); const [isVisible, setIsVisible] = useState(false); - const isInteractive = useStore(isInteractiveSelector); + const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow); const { zoomIn, zoomOut, fitView } = useReactFlow(); useEffect(() => { @@ -79,6 +84,7 @@ const Controls: FC> = ({ className="react-flow__controls-zoomin" title="zoom in" aria-label="zoom in" + disabled={maxZoomReached} > @@ -87,6 +93,7 @@ const Controls: FC> = ({ className="react-flow__controls-zoomout" title="zoom out" aria-label="zoom out" + disabled={minZoomReached} > diff --git a/packages/controls/src/style.css b/packages/controls/src/style.css index 289c5060..92d55b1f 100644 --- a/packages/controls/src/style.css +++ b/packages/controls/src/style.css @@ -24,5 +24,13 @@ max-width: 12px; max-height: 12px; } + + &:disabled { + pointer-events: none; + + svg { + fill-opacity: 0.4; + } + } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1b82b62..cb704934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,9 +132,11 @@ importers: '@types/react': '>=17' classcat: ^5.0.3 typescript: ^4.9.4 + zustand: ^4.3.1 dependencies: '@reactflow/core': link:../core classcat: registry.npmjs.org/classcat/5.0.4 + zustand: registry.npmjs.org/zustand/4.3.1 devDependencies: '@reactflow/eslint-config': link:../../tooling/eslint-config '@reactflow/rollup-config': link:../../tooling/rollup-config @@ -7445,6 +7447,14 @@ packages: punycode: registry.npmjs.org/punycode/2.1.1 dev: true + registry.npmjs.org/use-sync-external-store/1.2.0: + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz} + name: use-sync-external-store + version: 1.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dev: false + registry.npmjs.org/use-sync-external-store/1.2.0_react@18.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz} id: registry.npmjs.org/use-sync-external-store/1.2.0 @@ -7786,6 +7796,23 @@ packages: engines: {node: '>=10'} dev: true + registry.npmjs.org/zustand/4.3.1: + resolution: {integrity: sha512-EVyo/eLlOTcJm/X5M00rwtbYFXwRVTaRteSvhtbTZUCQFJkNfIyHPiJ6Ke68MSWzcKHpPzvqNH4gC2ZS/sbNqw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/zustand/-/zustand-4.3.1.tgz} + name: zustand + version: 4.3.1 + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + use-sync-external-store: registry.npmjs.org/use-sync-external-store/1.2.0 + dev: false + registry.npmjs.org/zustand/4.3.1_react@18.2.0: resolution: {integrity: sha512-EVyo/eLlOTcJm/X5M00rwtbYFXwRVTaRteSvhtbTZUCQFJkNfIyHPiJ6Ke68MSWzcKHpPzvqNH4gC2ZS/sbNqw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/zustand/-/zustand-4.3.1.tgz} id: registry.npmjs.org/zustand/4.3.1