fix: scope selector UX, ARIA, and code quality improvements

- removeScope preserves graph state when items remain (only resets on empty)
- Extract shared CheckIcon, CloseIcon, Spinner, ScopeListItem components
- Unify three handleSelect* into single handleToggle
- Fix shared mutable Set in reset state (factory function)
- Add MAX_SCOPE_ITEMS UI feedback (disabled button, warning banner)
- Correct ARIA: dialog > tablist + listbox > option structure
- Event listeners only attached when dropdown is open
- isSelected uses Set lookup via useMemo instead of O(n) scan
- Clear user search only on add, not on deselect
This commit is contained in:
Fringg
2026-03-20 02:44:29 +03:00
parent db76cd0c64
commit 2780898d1c
6 changed files with 230 additions and 229 deletions

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand';
import type { SelectedNode, NetworkFilters, ScopeSelection } from '@/types/referralNetwork';
const MAX_SCOPE_ITEMS = 50;
export const MAX_SCOPE_ITEMS = 50;
const DEFAULT_FILTERS: NetworkFilters = {
campaigns: [],
@@ -9,6 +9,15 @@ const DEFAULT_FILTERS: NetworkFilters = {
minReferrals: 0,
};
function resetGraphState() {
return {
selectedNode: null as SelectedNode,
hoveredNodeId: null as string | null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
};
}
interface ReferralNetworkState {
selectedNode: SelectedNode;
hoveredNodeId: string | null;
@@ -51,28 +60,22 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
if (state.scope.length >= MAX_SCOPE_ITEMS) return state;
return {
scope: [...state.scope, item],
selectedNode: null,
hoveredNodeId: null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
...resetGraphState(),
};
}),
removeScope: (type, id) =>
set((state) => ({
scope: state.scope.filter((s) => !(s.type === type && s.id === id)),
selectedNode: null,
hoveredNodeId: null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
})),
set((state) => {
const next = state.scope.filter((s) => !(s.type === type && s.id === id));
if (next.length === 0) {
return { scope: next, ...resetGraphState() };
}
return { scope: next };
}),
clearScope: () =>
set({
scope: [],
selectedNode: null,
hoveredNodeId: null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
...resetGraphState(),
}),
}));