Enhanced Tag Column with Multi-Tag Selection and Dynamic Filtering

Summary

Implemented a comprehensive enhancement to the TagColumn component, enabling multi-tag selection with "OR" logic filtering, dynamic card sorting based on tag match count, and an improved user interface with sorting controls.

Why Care

This enhancement significantly improves the toolkit browsing experience by allowing users to filter content with multiple tags simultaneously. The intuitive tag selection mechanism, combined with dynamic card sorting based on relevance, makes finding specific tools much faster and more efficient, especially for users with large collections of tools.

Implementation

Changes Made

  • /Users/mpstaton/code/lossless-monorepo/site/src/components/tool-components/TagColumn.astro
    • Completely redesigned the component with a more sophisticated architecture
    • Added support for multiple tag selection with URL parameter tracking
    • Implemented dynamic card sorting based on tag match count
    • Added sorting controls (alphabetical and frequency)
    • Enhanced search functionality with improved UI
    • Added responsive design for mobile devices
    • Implemented accessibility improvements

Technical Details

Multi-Tag Selection with URL Parameter Tracking

typescript
// Function to toggle tag selection
function toggleTagSelection(tag: string) {
  if (selectedTags.includes(tag)) {
    // Remove tag if already selected
    selectedTags = selectedTags.filter(t => t !== tag);
  } else {
    // Add tag if not selected
    selectedTags.push(tag);
  }
  
  // Update URL to reflect selected tags
  updateURL();
  
  // Update the UI to reflect selected tags
  updateTagSelectionUI();
  
  // Filter content based on selected tags
  filterContent(selectedTags);
  
  // Reorder tags to show selected tags at the top
  reorderTags();
}

// Function to update the URL with selected tags
function updateURL() {
  const newUrl = new URL(window.location.href);
  if (selectedTags.length > 0) {
    newUrl.searchParams.set('tags', selectedTags.join(','));
  } else {
    newUrl.searchParams.delete('tags');
  }
  history.pushState({}, '', newUrl);
}

Dynamic Card Sorting Based on Tag Match Count

typescript
// Function to filter content based on selected tags
function filterContent(selectedTags: string[]) {
  // Get all tool cards
  const toolCards = document.querySelectorAll('.tool-card');
  
  // If no tags selected, show all cards
  if (selectedTags.length === 0) {
    toolCards.forEach(card => {
      (card as HTMLElement).style.display = '';
    });
    return;
  }
  
  // Create an array to track cards and their match counts
  const cardMatches: {card: HTMLElement, matchCount: number}[] = [];
  
  // Filter cards based on selected tags
  toolCards.forEach(card => {
    // Get the card's tags
    const cardTagsStr = (card as HTMLElement).dataset.tags;
    if (!cardTagsStr) return;
    
    const cardTags = JSON.parse(cardTagsStr);
    
    // Count how many selected tags match this card's tags
    const matchCount = selectedTags.filter(tag => cardTags.includes(tag)).length;
    
    // If the card has at least one matching tag, add it to our array with its match count
    if (matchCount > 0) {
      cardMatches.push({
        card: card as HTMLElement,
        matchCount
      });
    } else {
      // Hide cards with no matches
      (card as HTMLElement).style.display = 'none';
    }
  });
  
  // Sort cards by match count (descending)
  cardMatches.sort((a, b) => b.matchCount - a.matchCount);
  
  // Get the parent container of the cards
  const cardContainer = toolCards[0]?.parentElement;
  if (!cardContainer) return;
  
  // Remove all cards from the DOM
  toolCards.forEach(card => card.remove());
  
  // Add cards back in the new sorted order
  cardMatches.forEach(({card, matchCount}) => {
    // Show the card
    card.style.display = '';
    
    // Add a data attribute showing the match count
    card.setAttribute('data-match-count', matchCount.toString());
    
    // Add the card back to the container
    cardContainer.appendChild(card);
  });
}

Tag Sorting Controls

typescript
// Sort function
function getSortedTags(tags: string[], sortType: string, selectedTags: string[] = []): string[] {
  // First sort by selection status (selected tags first)
  return [...tags].sort((a, b) => {
    const aSelected = selectedTags.includes(a);
    const bSelected = selectedTags.includes(b);
    
    // Prioritize selected tags
    if (aSelected && !bSelected) return -1;
    if (!aSelected && bSelected) return 1;
    
    // If both tags have the same selection status, sort by the chosen criteria
    if (sortType === 'frequency-desc') return tagFrequencies[b] - tagFrequencies[a];
    if (sortType === 'frequency-asc') return tagFrequencies[a] - tagFrequencies[b];
    if (sortType === 'alpha-asc') return a.localeCompare(b);
    if (sortType === 'alpha-desc') return b.localeCompare(a);
    return 0; // Default fallback
  });
}

Improved UI with Accessibility Features

html
<!-- Tag search input with improved accessibility -->
<form class="tag-search-form" role="search">
  <label for="tag-search" class="visually-hidden">Search tags</label>
  <div class="search-input-wrapper">
    <input 
      type="search" 
      id="tag-search" 
      name="tag-search" 
      placeholder="Search tags..." 
      list="tag-options"
      autocomplete="off"
    />
    <!-- Left side search icon -->
    <IconListSearch class="search-icon" />
    <!-- Right side filter icon - made interactive -->
    <button type="button" class="filter-button" aria-label="Show tag options" title="Show tag options">
      <IconFilterDown class="filter-icon" />
    </button>
  </div>
  <datalist id="tag-options">
    {initialSortedTags.map(tag => (
      <option value={tag} />
    ))}
  </datalist>
</form>

Integration Points

  • The TagColumn component integrates with the ToolCard components through data attributes
  • The component uses URL parameters to maintain state across page loads
  • The tag filtering functionality works in conjunction with the CardGrid component
  • The component respects the site's design system with consistent styling

Documentation

  • The implementation follows the project's component architecture guidelines
  • The code includes comprehensive comments explaining the functionality
  • The component is fully responsive and works on mobile devices
  • Accessibility features include:
    • Proper ARIA attributes for interactive elements
    • Keyboard navigation support
    • Visually hidden labels for screen readers
    • Proper color contrast for text elements
    • Focus states for interactive elements