Skip to content

Latest commit

 

History

History
661 lines (521 loc) · 22.7 KB

File metadata and controls

661 lines (521 loc) · 22.7 KB

Floating Search and Filter Boxes Implementation Guide

Overview

This guide documents the current implementation of floating search and filter boxes in the Divemap application. The solution ensures that search boxes and filter controls float together as a cohesive unit below the navbar during scrolling, working consistently across desktop and mobile devices.

Current Implementation Status

Fully Implemented - This pattern is actively used in:

  • DiveSites page (/frontend/src/pages/DiveSites.js)
  • DivingCenters page (/frontend/src/pages/DivingCenters.js)
  • DiveTrips page (/frontend/src/pages/DiveTrips.js)

Requirements

  • Desktop view: Search box and filters must float together RIGHT BELOW the desktop navbar
  • Mobile view: Search box and filters must float together RIGHT BELOW the mobile navbar
  • No space between navbar and search box
  • No space between search box and filters box
  • Navbar must always be visible during scrolling
  • Responsive behavior must work on both device types

Current Implementation

1. CSS Classes and Variables

The implementation uses a custom CSS class .sticky-below-navbar defined in /frontend/src/index.css:

/* Sticky Positioning for Search and Filters */
.sticky-below-navbar {
  position: sticky;
  top: var(--sticky-offset-desktop);
  z-index: 40;
}

@media (max-width: 768px) {
  .sticky-below-navbar {
    top: var(--sticky-offset-mobile);
  }
}

CSS Variables:

:root {
  --navbar-height-desktop: 4rem; /* 64px - matches h-16 */
  --navbar-height-mobile: 4rem;  /* 64px - matches h-16 */
  --sticky-offset-desktop: calc(var(--navbar-height-desktop) + 0px); /* No gap */
  --sticky-offset-mobile: calc(var(--navbar-height-mobile) + 0px);  /* No gap */
}

2. HTML Structure Pattern

The current implementation uses this structure across all pages:

{/* Sticky Filter Bar - Mobile-First Responsive Design */}
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4'>
  {/* Desktop Search Bar - Only visible on desktop/tablet */}
  {!isMobile && (
    <DesktopSearchBar
      searchValue={filters.search_query}
      onSearchChange={value => handleFilterChange('search_query', value)}
      onSearchSelect={selectedItem => {
        handleFilterChange('search_query', selectedItem.name);
      }}
      data={diveSites?.results || []}
      configType='diveSites'
      placeholder='Search dive sites by name, country, region, or description...'
    />
  )}

  <ResponsiveFilterBar
    showFilters={showAdvancedFilters}
    onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
    onClearFilters={clearFilters}
    activeFiltersCount={getActiveFiltersCount()}
    filters={{ ...filters, availableTags, user }}
    onFilterChange={handleFilterChange}
    onQuickFilter={handleQuickFilter}
    quickFilter={quickFilter}
    variant='inline'
    showQuickFilters={true}
    showAdvancedToggle={true}
    searchQuery={filters.search_query}
    onSearchChange={value => handleFilterChange('search_query', value)}
    onSearchSubmit={() => {}}
    // Additional props for sorting and view modes
    sortBy={sortBy}
    sortOrder={sortOrder}
    sortOptions={getSortOptions('dive-sites')}
    onSortChange={handleSortChange}
    onReset={resetSorting}
    viewMode={viewMode}
    onViewModeChange={handleViewModeChange}
    compactLayout={compactLayout}
    onDisplayOptionChange={handleDisplayOptionChange}
  />
</div>

3. Critical CSS Classes

  • Container: sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4
  • Z-index: 40 (same as navbar, ensuring proper layering)
  • Positioning: Uses CSS variables for consistent top positioning

4. Key Implementation Details

Z-Index Management

  • Sticky elements z-index: 40 (used by .sticky-below-navbar and mobile sticky headers)
  • Sticky container z-index: 40 (same as other sticky elements for proper layering)
  • Search suggestions z-index: 50 (defined in FuzzySearchInput component to appear above filters)
  • Modal overlays z-index: 200 (for full-screen overlays and modals)

Positioning

  • Top value: Uses CSS variables --sticky-offset-desktop and --sticky-offset-mobile (both 64px)
  • Responsive: Same positioning for both mobile and desktop (64px from top)
  • Sticky behavior: Uses position: sticky for smooth scrolling

Spacing

  • Between search and filters: No borders or margins - uses padding only
  • Container padding: py-3 sm:py-4 for consistent responsive padding
  • Background: Solid white background with shadow for visual separation

Usage Examples

Example 1: DiveSites Page Implementation

// From /frontend/src/pages/DiveSites.js
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4'>
  {/* Desktop Search Bar - Only visible on desktop/tablet */}
  {!isMobile && (
    <DesktopSearchBar
      searchValue={filters.search_query}
      onSearchChange={value => handleFilterChange('search_query', value)}
      onSearchSelect={selectedItem => {
        handleFilterChange('search_query', selectedItem.name);
      }}
      data={diveSites?.results || []}
      configType='diveSites'
      placeholder='Search dive sites by name, country, region, or description...'
    />
  )}

  <ResponsiveFilterBar
    showFilters={showAdvancedFilters}
    onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
    onClearFilters={clearFilters}
    activeFiltersCount={getActiveFiltersCount()}
    filters={{ ...filters, availableTags, user }}
    onFilterChange={handleFilterChange}
    onQuickFilter={handleQuickFilter}
    quickFilter={quickFilter}
    variant='inline'
    showQuickFilters={true}
    showAdvancedToggle={true}
    searchQuery={filters.search_query}
    onSearchChange={value => handleFilterChange('search_query', value)}
    onSearchSubmit={() => {}}
    sortBy={sortBy}
    sortOrder={sortOrder}
    sortOptions={getSortOptions('dive-sites')}
    onSortChange={handleSortChange}
    onReset={resetSorting}
    viewMode={viewMode}
    onViewModeChange={handleViewModeChange}
    compactLayout={compactLayout}
    onDisplayOptionChange={handleDisplayOptionChange}
  />
</div>

Example 2: DivingCenters Page Implementation

// From /frontend/src/pages/DivingCenters.js
<div className='sticky-below-navbar bg-white shadow-lg border border-gray-200 rounded-lg mx-3 sm:mx-4 lg:mx-6 xl:mx-8 mb-6'>
  {/* Desktop Search Bar - Only visible on desktop/tablet */}
  {!isMobile && (
    <DivingCentersDesktopSearchBar
      searchValue={filters.search}
      onSearchChange={value => handleSearchChange({ target: { name: 'search', value } })}
      onSearchSelect={selectedItem => {
        handleSearchChange({ target: { name: 'search', value: selectedItem.name } });
      }}
      data={divingCenters || []}
      configType='divingCenters'
      placeholder='Search diving centers by name, location, or services...'
    />
  )}

  <DivingCentersResponsiveFilterBar
    showFilters={showAdvancedFilters}
    onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
    onClearFilters={clearFilters}
    activeFiltersCount={getActiveFiltersCount()}
    filters={{ ...filters, availableTags, user }}
    onFilterChange={handleFilterChange}
    onQuickFilter={handleQuickFilter}
    quickFilter={quickFilter}
    variant='inline'
    showQuickFilters={true}
    showAdvancedToggle={true}
    searchQuery={filters.search}
    onSearchChange={value => handleSearchChange({ target: { name: 'search', value } })}
    onSearchSubmit={() => {}}
    sortBy={sortBy}
    sortOrder={sortOrder}
    sortOptions={getSortOptions('diving-centers')}
    onSortChange={handleSortChange}
    onReset={resetSorting}
    viewMode={viewMode}
    onViewModeChange={handleViewModeChange}
    compactLayout={compactLayout}
    onDisplayOptionChange={handleDisplayOptionChange}
  />
</div>

Example 3: DiveTrips Page Implementation

// From /frontend/src/pages/DiveTrips.js
<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg px-3 sm:px-4 lg:px-6 xl:px-8 py-3 sm:py-4'>
  <div className='flex flex-col sm:flex-row gap-3 sm:gap-4 items-end'>
    {/* Smart Fuzzy Search Input - Enhanced search experience */}
    <div className='flex-1 w-full'>
      <label htmlFor='search-query' className='block text-sm font-medium text-gray-700 mb-2'>
        Search
      </label>
      <FuzzySearchInput
        data={trips || []}
        searchValue={filters.search_query}
        onSearchChange={value => handleFilterChange('search_query', value)}
        onSearchSelect={selectedItem => {
          handleFilterChange('search_query', selectedItem.name);
        }}
        configType='diveTrips'
        placeholder='Search dive trips by name, location, or description...'
        minQueryLength={2}
        maxSuggestions={8}
        debounceDelay={300}
        showSuggestions={true}
        highlightMatches={true}
        showScore={false}
        showClearButton={true}
        className='w-full'
        inputClassName='w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base'
        suggestionsClassName='absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-96 overflow-y-auto'
        highlightClass='bg-blue-100 font-medium'
      />
    </div>

    {/* Filter Controls */}
    <ResponsiveFilterBar
      showFilters={showAdvancedFilters}
      onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
      onClearFilters={clearFilters}
      activeFiltersCount={getActiveFiltersCount()}
      filters={{ ...filters, availableTags, user }}
      onFilterChange={handleFilterChange}
      onQuickFilter={handleQuickFilter}
      quickFilter={quickFilter}
      variant='inline'
      showQuickFilters={true}
      showAdvancedToggle={true}
      searchQuery={filters.search_query}
      onSearchChange={value => handleFilterChange('search_query', value)}
      onSearchSubmit={() => {}}
      sortBy={sortBy}
      sortOrder={sortOrder}
      sortOptions={getSortOptions('dive-trips')}
      onSortChange={handleSortChange}
      onReset={resetSorting}
      viewMode={viewMode}
      onViewModeChange={handleViewModeChange}
      compactLayout={compactLayout}
      onDisplayOptionChange={handleDisplayOptionChange}
    />
  </div>
</div>

Best Practices

1. CSS Variable Usage

  • Always use CSS variables for positioning: --sticky-offset-desktop and --sticky-offset-mobile
  • Maintain consistency with navbar height variables: --navbar-height-desktop and --navbar-height-mobile
  • Avoid hardcoded values - use the .sticky-below-navbar class instead of custom positioning

2. Component Composition

  • Use ResponsiveFilterBar for consistent filter functionality across pages
  • Use DesktopSearchBar for desktop search with fuzzy search capabilities
  • Use FuzzySearchInput directly for custom search implementations
  • Pass all required props to maintain functionality

3. Z-Index Management

  • Sticky container: Use z-index: 40 (same as other sticky elements)
  • Search suggestions: Use z-index: 50 or higher
  • Modal overlays: Use z-index: 200 or higher
  • Avoid conflicts by checking existing z-index values before adding new elements

Z-Index Hierarchy Reference

/* Z-Index Hierarchy in Divemap */
z-index: 10   /* Dropdown menus, tooltips */
z-index: 40   /* Sticky elements (navbar, search filters) */
z-index: 50   /* Search suggestions, floating elements */
z-index: 100  /* Fixed overlays */
z-index: 200  /* Modal dialogs, full-screen overlays */

4. Responsive Design

  • Mobile-first approach: Design for mobile first, then enhance for desktop
  • Consistent spacing: Use py-3 sm:py-4 for responsive padding
  • Conditional rendering: Use !isMobile for desktop-only components

5. State Management

  • Centralize filter state in the parent component
  • Use URL parameters for filter persistence
  • Implement proper cleanup for filter changes

6. Performance Considerations

  • Debounce search input to avoid excessive API calls (300ms recommended)
  • Use React.memo for filter components that don't change frequently
  • Lazy load filter options when possible
  • Optimize re-renders by using useCallback for event handlers

7. Accessibility Guidelines

  • Touch targets: Minimum 44px height for mobile touch targets
  • Keyboard navigation: Ensure all interactive elements are keyboard accessible
  • Screen readers: Use proper ARIA labels and roles
  • Focus management: Maintain focus order when toggling filters
  • Color contrast: Ensure sufficient contrast for text and interactive elements

Validation Checklist

Code Review

  • Sticky container uses .sticky-below-navbar class
  • Search box and filters are direct children of sticky container
  • No borders between search and filters sections
  • Z-index is set to 40 (same as navbar)
  • Background is solid white with appropriate shadow
  • CSS variables are used for positioning

Visual Testing

  • Search box and filters appear together below navbar
  • No gaps between navbar and search box
  • No gaps between search box and filters
  • Both elements remain visible when scrolling
  • Navbar remains visible during scrolling
  • Shadow provides proper visual separation

Responsive Testing

  • Desktop: Elements float at 64px from top (below navbar)
  • Mobile: Elements float at 64px from top (below navbar)
  • Both device types show identical floating behavior
  • No horizontal scrolling issues on mobile
  • Touch targets are appropriate for mobile (44px minimum)

Component Documentation

ResponsiveFilterBar Component

Location: /frontend/src/components/ResponsiveFilterBar.js

Purpose: Provides a responsive filter bar with search, sorting, and view mode controls.

Key Props:

  • showFilters: Boolean to control advanced filters visibility
  • onToggleFilters: Function to toggle advanced filters
  • onClearFilters: Function to clear all filters
  • activeFiltersCount: Number of active filters
  • filters: Object containing filter values and available options
  • onFilterChange: Function to handle filter changes
  • searchQuery: Current search query value
  • onSearchChange: Function to handle search input changes
  • sortBy, sortOrder, sortOptions: Sorting configuration
  • viewMode, onViewModeChange: View mode controls
  • compactLayout: Display option (thumbnails removed)

Usage Pattern:

<ResponsiveFilterBar
  showFilters={showAdvancedFilters}
  onToggleFilters={() => setShowAdvancedFilters(!showAdvancedFilters)}
  onClearFilters={clearFilters}
  activeFiltersCount={getActiveFiltersCount()}
  filters={{ ...filters, availableTags, user }}
  onFilterChange={handleFilterChange}
  searchQuery={filters.search_query}
  onSearchChange={value => handleFilterChange('search_query', value)}
  // ... other props
/>

DesktopSearchBar Component

Location: /frontend/src/components/DesktopSearchBar.js

Purpose: Provides desktop-specific search functionality with fuzzy search capabilities.

Key Props:

  • searchValue: Current search value
  • onSearchChange: Function to handle search input changes
  • onSearchSelect: Function to handle search result selection
  • data: Array of data to search through
  • configType: Configuration type for search behavior
  • placeholder: Search input placeholder text

Usage Pattern:

<DesktopSearchBar
  searchValue={filters.search_query}
  onSearchChange={value => handleFilterChange('search_query', value)}
  onSearchSelect={selectedItem => {
    handleFilterChange('search_query', selectedItem.name);
  }}
  data={diveSites?.results || []}
  configType='diveSites'
  placeholder='Search dive sites by name, country, region, or description...'
/>

CSS Class Reference

.sticky-below-navbar

Purpose: Positions elements to float below the navbar during scrolling.

CSS Properties:

  • position: sticky
  • top: var(--sticky-offset-desktop) (desktop)
  • top: var(--sticky-offset-mobile) (mobile)
  • z-index: 40

Usage:

<div className='sticky-below-navbar bg-white shadow-sm border-b border-gray-200'>
  {/* Content */}
</div>

Testing Methodology

Manual Testing

  1. Desktop view: Open page in desktop browser, scroll down to verify floating
  2. Mobile view: Resize browser to mobile dimensions (375x667), scroll to verify floating
  3. Scroll behavior: Scroll up/down to ensure smooth sticky behavior
  4. Content visibility: Verify no background content shows through floating elements
  5. Touch interaction: Test touch targets on mobile devices

Debugging Tips

Browser Dev Tools Inspection

  1. Check z-index values: Use "Computed" tab to verify actual z-index values
  2. Inspect stacking context: Look for position: relative or position: absolute that might create new stacking contexts
  3. Verify CSS variables: Check that --sticky-offset-desktop and --sticky-offset-mobile are properly defined
  4. Test responsive behavior: Use device emulation to test mobile vs desktop behavior

Common Debugging Commands

// Check if sticky positioning is working
const stickyElement = document.querySelector('.sticky-below-navbar');
const computedStyle = window.getComputedStyle(stickyElement);
console.log('Position:', computedStyle.position);
console.log('Top:', computedStyle.top);
console.log('Z-index:', computedStyle.zIndex);

// Check CSS variables
const root = document.documentElement;
const stickyOffset = getComputedStyle(root).getPropertyValue('--sticky-offset-desktop');
console.log('Sticky offset:', stickyOffset);

Automated Testing with Playwright

// Test floating behavior on mobile
await page.setViewportSize({ width: 375, height: 667 });
await page.evaluate(() => window.scrollTo(0, 300));

// Verify elements are visible after scrolling
const searchBox = await page.$('input[placeholder*="Search"]');
const filtersButton = await page.$('button');
const searchBoxVisible = await searchBox.isVisible();
const filtersButtonVisible = await filtersButton.isVisible();

// Both should be visible (floating)
assert(searchBoxVisible && filtersButtonVisible);

Position Validation

// Check element positions after scrolling
const searchBoxRect = await searchBox.boundingBox();
const filtersButtonRect = await filtersButton.boundingBox();

// Both should be above viewport top (floating)
assert(searchBoxRect.y >= 0);
assert(filtersButtonRect.y >= 0);

Common Issues and Solutions

Issue: Search box not floating with filters

Solution: Ensure both elements are direct children of the same sticky container using .sticky-below-navbar class

Issue: Elements appearing behind navbar

Solution: Check z-index values - sticky container should use z-index: 40 (same as other sticky elements). If elements still appear behind, check for conflicting z-index values in parent containers.

Issue: Gaps between elements

Solution: Remove borders and use consistent padding only (py-3 sm:py-4)

Issue: Different behavior on mobile vs desktop

Solution: Use CSS variables --sticky-offset-desktop and --sticky-offset-mobile - both are 64px

Issue: Search suggestions not visible

Solution: Ensure search suggestions use z-index: 50 or higher to appear above filters

Issue: Touch targets too small on mobile

Solution: Use min-h-[44px] class for touch targets and test on actual mobile devices

Issue: Z-index conflicts with other components

Solution:

  1. Check the z-index hierarchy reference above
  2. Use browser dev tools to inspect element stacking context
  3. Ensure parent containers don't create new stacking contexts
  4. Use z-index: 50 or higher for elements that need to appear above search filters

Issue: Search suggestions not appearing above other elements

Solution: Ensure search suggestions use z-index: 50 or higher. Check that the suggestions container doesn't have position: relative with a lower z-index that creates a new stacking context.

Implementation Checklist for New Pages

When implementing floating search and filters on a new page:

Setup

  • Import required components (ResponsiveFilterBar, DesktopSearchBar, FuzzySearchInput)
  • Add .sticky-below-navbar class to container div
  • Include proper background and shadow classes
  • Set up state management for filters and search

Configuration

  • Pass all required props to components
  • Implement debounced search (300ms recommended)
  • Set up URL parameter persistence
  • Configure proper z-index values

Testing

  • Test on both desktop and mobile viewports
  • Verify z-index layering is correct
  • Test keyboard navigation
  • Verify screen reader compatibility

Accessibility

  • Ensure touch targets meet 44px minimum
  • Add proper ARIA labels and roles
  • Test focus management
  • Verify color contrast ratios

Performance

  • Optimize re-renders with useCallback
  • Consider React.memo for static components
  • Implement lazy loading where appropriate
  • Test with slow network conditions

Reusable Component Pattern

const FloatingSearchFilters = ({ 
  searchComponent, 
  filtersComponent, 
  className = '' 
}) => {
  return (
    <div className={`sticky-below-navbar bg-white shadow-sm border-b border-gray-200 rounded-t-lg py-3 sm:py-4 ${className}`}>
        {searchComponent}
        {filtersComponent}
    </div>
  );
};

Success Criteria

The implementation is successful when:

  • Search box and filters float together as one unit
  • Both elements remain visible during scrolling
  • No gaps exist between navbar, search, and filters
  • Behavior is identical on desktop and mobile
  • Navbar remains visible and accessible
  • No content shows through the floating elements
  • Touch targets are appropriate for mobile devices
  • Z-index layering works correctly

Notes

  • This solution works because both mobile and desktop navbars have the same height (64px)
  • CSS variables ensure consistent positioning across devices
  • Z-index management prevents layering conflicts
  • The sticky container approach ensures both elements move together as one unit
  • Mobile-first responsive design ensures optimal experience on all devices

Related Documentation


Last Updated: September 13, 2025
Author: AI Assistant
Version: 2.1 - Enhanced with debugging tools, performance guidelines, and accessibility considerations