Repository navigation
DistrictSelection
The DistrictSelection component is a flexible, reusable shell component designed for displaying and managing selectable lists of items (districts, clients, or other entities). It provides a consistent interface with search functionality, loading states, preference persistence, and a deselect option. This component is designed to be extended for different use cases by providing custom query hooks and filter functions.
DistrictSelection acts as a generic container component that handles:
- Search functionality (conditionally displayed based on item count)
- Loading states with skeleton placeholders
- User preference storage and retrieval
- Item filtering based on custom filter logic
- Selection state management with visual feedback
- Accessibility features (ARIA labels, keyboard navigation)
The component delegates item rendering to DistrictItem and expects data to conform to the DistrictType interface.
| Name | Type | Required | Description |
|---|---|---|---|
queryHook |
() => { data: DistrictType[] | undefined; isLoading: boolean } |
✅ | Custom hook that returns data and loading state for the list items |
preferenceKey |
string |
✅ | Key used to store/retrieve the selected item ID in user preferences |
deselectLabel |
string |
✅ | Label text for the deselect option (e.g., "Select no district") |
searchLabel |
string |
✅ | Placeholder and label text for the search input |
filterFn |
(item: DistrictType, keyword: string) => boolean |
✅ | Custom filtering function to determine if an item matches the search keyword |
isSelected |
(item: DistrictType, userPreferenceValue: T) => boolean |
❌ | Custom comparator for determining the selected item. Defaults to strict id equality |
districtTypeConverter |
(district: DistrictType) => T |
❌ | Converts a selected item to the value stored in preferences. Defaults to district.id
|
export type DistrictType = {
id: string; // Unique identifier for the item
name: string; // Display name for the item
kind: string; // Type/category code (used for icon mapping)
};import DistrictSelection from '@/components/core/DistrictSelection';
import { useQuery } from '@tanstack/react-query';
import APIs from '@/services/APIs';
const MyDistrictSelector = () => {
const filter = (item: DistrictType, keyword: string): boolean => {
return (
item.name.trim().toLowerCase().includes(keyword.toLowerCase()) ||
item.id.trim().toLowerCase() === keyword.toLowerCase()
);
};
const { data, isLoading } = useQuery({
queryKey: ['districtOptions'],
queryFn: async () => await APIs.codes.getDistricts(),
select: (data) =>
data.map((district) => ({
id: district.code,
name: district.description,
kind: 'D',
})),
});
return (
<DistrictSelection
queryHook={() => ({ data, isLoading })}
preferenceKey="selectedDistrict"
deselectLabel="Select no district"
searchLabel="Search by district name or code"
filterFn={filter}
/>
);
};import DistrictSelection from '@/components/core/DistrictSelection';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '@/context/auth/useAuth';
import APIs from '@/services/APIs';
const MyClientSelector = () => {
const { getClients } = useAuth();
const filter = (client: DistrictType, keyword: string): boolean => {
return (
client.name.trim().toLowerCase().includes(keyword.toLowerCase()) ||
client.id.trim().toLowerCase() === keyword.toLowerCase()
);
};
const { data, isLoading } = useQuery({
queryKey: ['forest-clients', getClients()],
queryFn: () => APIs.forestclient.searchByClientNumbers(getClients(), 0, getClients().length),
enabled: !!getClients().length,
select: (data) =>
data.map((client) => ({
id: client.clientNumber,
name: client.name ?? client.clientName,
kind: client.clientTypeCode?.code,
})),
});
return (
<DistrictSelection
queryHook={() => ({ data, isLoading })}
preferenceKey="selectedClient"
deselectLabel="Select no client"
searchLabel="Search by client name or ID"
filterFn={filter}
/>
);
};The search bar is only displayed when the number of items exceeds MIN_CLIENTS_SHOW_SEARCH (default: 4). This prevents unnecessary UI clutter for small lists.
Selected items are automatically stored in user preferences using the provided preferenceKey. The selection persists across sessions and page refreshes.
A special "deselect" option is always available at the top of the list, allowing users to clear their selection. This uses the DESELECT_CLIENT constant with an empty ID.
While data is loading, a skeleton placeholder is displayed to provide visual feedback to users.
Selected items are highlighted with:
- A border using
--border-interactivecolor - A background color using
--tag-background-blue - A checkmark icon (via
DistrictItem)
export const MIN_CLIENTS_SHOW_SEARCH = 4;Threshold for displaying the search bar. Search is shown only when item count exceeds this value.
export const DESELECT_CLIENT: DistrictType = {
id: '',
name: 'Select none',
kind: 'Z',
};Predefined object representing the "no selection" state.
The component uses Carbon Design System components and custom SCSS defined in index.scss:
.district-selection-grid {
padding: 0;
gap: 1rem;
height: 100%;
}
.district-list {
list-style: none;
overflow-y: auto;
height: 65vh;
}
.district-list-item-btn {
border-radius: 0.25rem;
width: 100%;
padding: 0.5rem;
display: flex;
flex-direction: column;
border: none;
background-color: transparent;
align-items: flex-start;
}
.selected-district {
border: 2px solid var(--border-interactive);
background-color: var(--tag-background-blue);
}- Selection border:
--border-interactive - Selection background:
--tag-background-blue - Hover background:
--layer-hover-01 - Primary text:
--text-primary - Secondary text:
--text-secondary
- All list items have proper
aria-labelandtitleattributes - The list itself has
aria-label="District list" - Buttons use semantic HTML with proper keyboard navigation
- Loading states use Carbon's accessible skeleton components
To create a new implementation of DistrictSelection for a different data source:
- Define your data query: Create a query using React Query or similar
-
Transform data: Map your data to match the
DistrictTypeinterface - Create a filter function: Implement custom logic for searching/filtering items
- Choose a preference key: Use a unique key for storing selections
- Provide labels: Customize the deselect and search labels
import DistrictSelection from '@/components/core/DistrictSelection';
import type { DistrictType } from '@/components/core/DistrictSelection/types';
const MyEntitySelector = () => {
// 1. Define your query
const { data, isLoading } = useQuery({
queryKey: ['myEntities'],
queryFn: fetchMyEntities,
select: (rawData) =>
// 2. Transform to DistrictType
rawData.map((entity) => ({
id: entity.entityId,
name: entity.displayName,
kind: entity.categoryCode, // Used for icon mapping
})),
});
// 3. Create filter function
const filter = (item: DistrictType, keyword: string): boolean => {
const searchText = keyword.toLowerCase().trim();
return (
item.name.toLowerCase().includes(searchText) ||
item.id.toLowerCase().includes(searchText)
);
};
// 4 & 5. Configure component
return (
<DistrictSelection
queryHook={() => ({ data, isLoading })}
preferenceKey="selectedEntity" // Unique key
deselectLabel="Select no entity" // Custom label
searchLabel="Search entities" // Custom label
filterFn={filter}
/>
);
};- Keep filter functions simple: Filter logic should be fast since it runs on every keystroke
- Use meaningful preference keys: Choose keys that won't conflict with other preferences
- Handle empty states: Ensure your query hook properly handles cases where no data is available
-
Map icons appropriately: Update
ClientTypeIconMapin constants.ts if adding newkindvalues - Consider accessibility: Ensure custom labels are descriptive and helpful
-
Optimize queries: Use proper
staleTimeandenabledflags to prevent unnecessary API calls
- DistrictItem: Renders individual list items with icons and selection states
- ClientListing: Concrete implementation for forest client selection
- DistrictListing: Concrete implementation for district selection
- The component depends on the
PreferenceProvidercontext for storing selections - Search filtering is case-insensitive by default
- The list has a fixed height of 65vh with vertical scrolling
- All items are rendered as buttons within list items for proper semantics
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository