Skip to content

DistrictSelection

Paulo Gomes da Cruz Junior edited this page Apr 13, 2026 · 1 revision

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.

Architecture

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.

Props

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

Types

DistrictType

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)
};

Usage

Basic Implementation (District Listing)

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}
    />
  );
};

Advanced Implementation (Client Listing)

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}
    />
  );
};

Features

Conditional Search Bar

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.

Preference Persistence

Selected items are automatically stored in user preferences using the provided preferenceKey. The selection persists across sessions and page refreshes.

Deselect Option

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.

Loading States

While data is loading, a skeleton placeholder is displayed to provide visual feedback to users.

Visual Selection Feedback

Selected items are highlighted with:

  • A border using --border-interactive color
  • A background color using --tag-background-blue
  • A checkmark icon (via DistrictItem)

Constants

MIN_CLIENTS_SHOW_SEARCH

export const MIN_CLIENTS_SHOW_SEARCH = 4;

Threshold for displaying the search bar. Search is shown only when item count exceeds this value.

DESELECT_CLIENT

export const DESELECT_CLIENT: DistrictType = {
  id: '',
  name: 'Select none',
  kind: 'Z',
};

Predefined object representing the "no selection" state.

Styling

The component uses Carbon Design System components and custom SCSS defined in index.scss:

Key SCSS Classes

.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);
}

Design Tokens

  • Selection border: --border-interactive
  • Selection background: --tag-background-blue
  • Hover background: --layer-hover-01
  • Primary text: --text-primary
  • Secondary text: --text-secondary

Accessibility

  • All list items have proper aria-label and title attributes
  • The list itself has aria-label="District list"
  • Buttons use semantic HTML with proper keyboard navigation
  • Loading states use Carbon's accessible skeleton components

Extension Guide

To create a new implementation of DistrictSelection for a different data source:

  1. Define your data query: Create a query using React Query or similar
  2. Transform data: Map your data to match the DistrictType interface
  3. Create a filter function: Implement custom logic for searching/filtering items
  4. Choose a preference key: Use a unique key for storing selections
  5. Provide labels: Customize the deselect and search labels

Example: Custom Entity Selector

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}
    />
  );
};

Best Practices

  1. Keep filter functions simple: Filter logic should be fast since it runs on every keystroke
  2. Use meaningful preference keys: Choose keys that won't conflict with other preferences
  3. Handle empty states: Ensure your query hook properly handles cases where no data is available
  4. Map icons appropriately: Update ClientTypeIconMap in constants.ts if adding new kind values
  5. Consider accessibility: Ensure custom labels are descriptive and helpful
  6. Optimize queries: Use proper staleTime and enabled flags to prevent unnecessary API calls

Related Components

  • DistrictItem: Renders individual list items with icons and selection states
  • ClientListing: Concrete implementation for forest client selection
  • DistrictListing: Concrete implementation for district selection

Notes

  • The component depends on the PreferenceProvider context 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

Waste Plus Wiki

Home

Architecture

Codes

Decisions

Knowledge Base

Development

Changes

Clone this wiki locally