|
| 1 | +import React from 'react'; |
| 2 | +import { Canvas, Meta, Title, Subtitle, Unstyled } from '@storybook/blocks'; |
| 3 | +import { Callout } from '../../_storybook/components'; |
| 4 | +import * as stories from './col-filter-bar.stories'; |
| 5 | + |
| 6 | +<Meta of={stories} /> |
| 7 | + |
| 8 | +<div className="col-doc__wrapper"> |
| 9 | + <div className="col-doc__container"> |
| 10 | + <Unstyled> |
| 11 | + <Title>Filter Bar</Title> |
| 12 | + <div className="col-intro-text"> |
| 13 | + A Filter Bar is a reusable composition pattern built from Colibri components (not a single component). It provides search and filtering controls within a view, allowing users to select and apply filters to refine the displayed results. |
| 14 | + </div> |
| 15 | + |
| 16 | + <Subtitle>Table of contents</Subtitle> |
| 17 | + - [Overview](#overview) |
| 18 | + - [Usage](#usage) |
| 19 | + - [Example](#example) |
| 20 | + - [Variants](#variants) |
| 21 | + - [Accessibility](#accessibility) |
| 22 | + |
| 23 | + ## Overview |
| 24 | + <div> |
| 25 | + The Filter Bar provides filtering and search controls within a view, allowing users to select and apply filters to narrow the displayed results. |
| 26 | + It's commonly used above tables, lists, dashboards, and search results. |
| 27 | + |
| 28 | + Place a Filter Bar above data-heavy content such as tables, lists, dashboards, and search results. Configure quick filters directly in the bar and move advanced options into a secondary surface (e.g., a drawer) when necessary. Keep the composition consistent across pages to build user familiarity. |
| 29 | + |
| 30 | + <Callout variant="tip" icon="🧩"> |
| 31 | + This is a composition pattern using primitives such as `col-toolbar`, `col-search-bar`, `col-select`, `col-button`, `col-badge`, and (optionally) `col-drawer` for advanced filters. |
| 32 | + Treat it as a recipe you can copy and adapt rather than a single drop-in component. |
| 33 | + </Callout> |
| 34 | + </div> |
| 35 | + |
| 36 | + ## Usage |
| 37 | + 1. Install the library and styles: |
| 38 | + ```bash |
| 39 | + npm install @telesign/colibri |
| 40 | + ``` |
| 41 | + 2. Register components (all or the ones you use): |
| 42 | + ```ts |
| 43 | + import { registerAllComponents, registerColibriComponents } from '@telesign/colibri'; |
| 44 | + import '@telesign/colibri/styles/styles.css'; |
| 45 | + |
| 46 | + // Option A: everything |
| 47 | + registerAllComponents(); |
| 48 | + |
| 49 | + // Option B: only what's needed |
| 50 | + registerColibriComponents([ColToolbar, ColSearchBar, ColSelect, ColButton, ColBadge, ColDrawer, ColListMenu, ColListMenuItem]); |
| 51 | + ``` |
| 52 | + 3. Compose the Filter Bar using the building blocks below (see example): |
| 53 | + - Search input: `col-search-bar` |
| 54 | + - Filter controls: `col-select`, date range pickers, toggles, etc. |
| 55 | + - Actions: primary button(s), filter button with counter `col-badge` |
| 56 | + - Optional advanced filters panel: `col-drawer` with form fields |
| 57 | + |
| 58 | + ## Examples |
| 59 | + |
| 60 | + <Callout variant="info" icon="💡"> |
| 61 | + To help understand how to use this pattern, we provide helpful sample code, simply click `Show code` to copy/paste it and try it out. |
| 62 | + The examples below use inline scripts for simplicity. In a real project, you would typically separate the JavaScript logic into its own file/module. |
| 63 | + </Callout> |
| 64 | + |
| 65 | + ### Basic Example |
| 66 | + |
| 67 | + A simple Filter Bar with a search input and a few filter buttons: |
| 68 | + |
| 69 | + <Canvas of={stories.BasicFilterBar} /> |
| 70 | + |
| 71 | + Using `<col-toolbar>` to arrange the elements horizontally, the needed input components, like `<col-search-bar>` and `<col-select>`, you can quickly build a functional filter bar. |
| 72 | + |
| 73 | + In order to capture the submit event and associate the items to the form, you can use the `form` attribute on the inputs and buttons: |
| 74 | + |
| 75 | + ```html |
| 76 | + <!-- Create the form as a sibling component of the toolbar --> |
| 77 | + <form id="basic-form"></form> |
| 78 | + |
| 79 | + <!-- And then on each "input" add the form attribute passing the form's id --> |
| 80 | + <col-seach-bar form="basic-form" ...></col-search-bar> |
| 81 | + <col-select form="basic-form" ...></col-select> |
| 82 | + <col-button form="basic-form" type="submit">Search</col-button> |
| 83 | + ``` |
| 84 | + |
| 85 | + This way, we ensure the items are linked to the form without loosing the styles the toolbar provides. |
| 86 | + |
| 87 | + Then you can listen to the form's `submit` event to capture the values and perform the search/filtering logic. |
| 88 | + |
| 89 | + ```javascript |
| 90 | + const basicForm = document.getElementById('basic-form'); |
| 91 | + |
| 92 | + basicForm.addEventListener('submit', event => { |
| 93 | + event.preventDefault(); |
| 94 | + const formData = new FormData(basicForm); |
| 95 | + const filters = Object.fromEntries(formData.entries()); |
| 96 | + |
| 97 | + console.log('[Basic Search] Search with filters:', filters); |
| 98 | + // Perform search/filtering logic here |
| 99 | + }); |
| 100 | + ``` |
| 101 | + |
| 102 | + ### Advanced Search Example - Filter Button with Drawer |
| 103 | + |
| 104 | + Sometimes you may need more advanced filtering options that don't fit in the main bar. In this case, the Design System recommends using a "Filter Button": |
| 105 | + |
| 106 | + <Canvas of={stories.FilterButton} /> |
| 107 | + |
| 108 | + <Callout variant="info" icon="ℹ️"> |
| 109 | + If you set values in any of the advanced filters in the ColDrawer component, you can see the ColBadge counter update, try it out! |
| 110 | + </Callout> |
| 111 | + |
| 112 | + The Filter Button includes a badge that shows the number of active filters. When clicked, it opens a Drawer with additional filtering options. |
| 113 | + |
| 114 | + To create the filter button simply use a `col-button` with a `col-badge` inside, and add the logic to open the drawer when clicked: |
| 115 | + |
| 116 | + ```html |
| 117 | + <col-button id="filter-button" aria-label="Open filters" onclick="openDrawer()"> |
| 118 | + <col-icon name="filter" size="16"></col-icon> |
| 119 | + <col-badge id="filter-counter" aria-label="Active filters count">0</col-badge> |
| 120 | + </col-button> |
| 121 | + ``` |
| 122 | + |
| 123 | + Then, implement the logic to open/close the drawer and update the badge count based on the selected filters: |
| 124 | + |
| 125 | + ```javascript |
| 126 | + const drawer = document.querySelector('#filter-drawer'); |
| 127 | + const openDrawer = () => (drawer.active = true); |
| 128 | + |
| 129 | + // the closeDrawer can be called from the buttons inside the drawer |
| 130 | + const closeDrawer = () => (drawer.active = false); |
| 131 | + ``` |
| 132 | + |
| 133 | + An example of the JavaScript code needed to update the badge count when filters are applied: |
| 134 | + |
| 135 | + ```javascript |
| 136 | + |
| 137 | + // Grab drawer elements to monitor changes |
| 138 | + const exampleDrawerElements = document |
| 139 | + .querySelector('#example-drawer-content') |
| 140 | + .querySelectorAll('col-text-field, col-select, col-number-field'); |
| 141 | + |
| 142 | + // Centralize badge update logic |
| 143 | + const updateExampleBadge = () => { |
| 144 | + let total = 0; |
| 145 | + exampleDrawerElements.forEach(el => { |
| 146 | + if (el && typeof el.value !== 'undefined' && String(el.value || '').trim()) total += 1; |
| 147 | + }); |
| 148 | + const filterCounter = document.querySelector('#example-filter-counter'); |
| 149 | + if (filterCounter) filterCounter.textContent = String(total); |
| 150 | + }; |
| 151 | + |
| 152 | + // Listen to changes on drawer elements to update badge |
| 153 | + exampleDrawerElements.forEach(el => { |
| 154 | + el?.addEventListener('change', updateExampleBadge); |
| 155 | + el?.addEventListener('input', updateExampleBadge); |
| 156 | + }); |
| 157 | + |
| 158 | + // Handle Drawer open/close logic |
| 159 | + const exampleDrawer = document.querySelector('#example-filter-drawer'); |
| 160 | + const openExampleDrawer = () => (exampleDrawer.active = true); |
| 161 | + const closeExampleDrawer = () => (exampleDrawer.active = false); |
| 162 | + const closeAndClearExampleDrawer = () => { |
| 163 | + exampleDrawerElements.forEach(el => { |
| 164 | + if (el && typeof el.value !== 'undefined') el.value = ''; |
| 165 | + }); |
| 166 | + updateExampleBadge(); |
| 167 | + closeExampleDrawer(); |
| 168 | + }; |
| 169 | + exampleDrawer.addEventListener('overlay-click-outside', closeAndClearExampleDrawer); |
| 170 | + exampleDrawer.addEventListener('on-close', closeAndClearExampleDrawer); |
| 171 | + |
| 172 | + // Initialize |
| 173 | + updateExampleBadge(); |
| 174 | + ``` |
| 175 | +
|
| 176 | + ### Advanced Example - Full Filter Bar |
| 177 | +
|
| 178 | + Putting it all together we get an Advanced Filter Bar like this: |
| 179 | +
|
| 180 | + <Canvas of={stories.AdvancedFilterBar} /> |
| 181 | +
|
| 182 | + ## Helpful Tips |
| 183 | +
|
| 184 | + You can customize the width of the search bar and select inputs using the `custom-width` attribute to better fit your layout: |
| 185 | +
|
| 186 | + ```html |
| 187 | + <col-search-bar custom-width="150px" ...></col-search-bar> |
| 188 | + <col-select custom-width="200px" ...></col-select> |
| 189 | + ``` |
| 190 | +
|
| 191 | + There are times when the Filter Bar is placed in a container with limited horizontal space. In these cases, you can use the `wrap` attribute on the `col-toolbar` to allow the items to wrap onto multiple lines: |
| 192 | +
|
| 193 | + ```html |
| 194 | + <col-toolbar wrap ...> |
| 195 | + ... |
| 196 | + </col-toolbar> |
| 197 | + ``` |
| 198 | +
|
| 199 | + An example of the JavaScript needed to handle the form submission and capture the filter values: |
| 200 | +
|
| 201 | + ```javascript |
| 202 | + // Handle Form logic |
| 203 | + const form = document.getElementById('filter-form'); |
| 204 | + const toolbarElements = document |
| 205 | + .querySelector('#toolbar') |
| 206 | + .querySelectorAll('col-search-bar, col-select'); |
| 207 | + const drawerElements = document |
| 208 | + .querySelector('#drawer-content') |
| 209 | + .querySelectorAll('col-text-field, col-select, col-number-field'); |
| 210 | + |
| 211 | + const updateBadge = () => { |
| 212 | + let total = 0; |
| 213 | + toolbarElements.forEach(el => { |
| 214 | + if (el && typeof el.value !== 'undefined' && String(el.value || '').trim()) total += 1; |
| 215 | + }); |
| 216 | + drawerElements.forEach(el => { |
| 217 | + if (el && typeof el.value !== 'undefined' && String(el.value || '').trim()) total += 1; |
| 218 | + }); |
| 219 | + const filterCounter = document.querySelector('#filter-counter'); |
| 220 | + if (filterCounter) filterCounter.textContent = String(total); |
| 221 | + }; |
| 222 | + |
| 223 | + // Add event listeners to update badge on input changes |
| 224 | + [...toolbarElements, ...drawerElements].forEach(el => { |
| 225 | + el?.addEventListener('change', updateBadge); |
| 226 | + el?.addEventListener('input', updateBadge); |
| 227 | + }); |
| 228 | + |
| 229 | + // Functions |
| 230 | + const onSubmit = event => { |
| 231 | + event.preventDefault(); |
| 232 | + const formData = new FormData(form); |
| 233 | + const filters = Object.fromEntries(formData.entries()); |
| 234 | + console.log('[FilterBar] Search with filters:', filters); |
| 235 | + }; |
| 236 | + |
| 237 | + const clearForm = () => { |
| 238 | + toolbarElements.forEach(el => { |
| 239 | + if (el && typeof el.value !== 'undefined') el.value = ''; |
| 240 | + }); |
| 241 | + drawerElements.forEach(el => { |
| 242 | + if (el && typeof el.value !== 'undefined') el.value = ''; |
| 243 | + }); |
| 244 | + updateBadge(); |
| 245 | + }; |
| 246 | + |
| 247 | + const onReset = event => { |
| 248 | + event.preventDefault(); |
| 249 | + form.reset(); |
| 250 | + clearForm(); |
| 251 | + }; |
| 252 | + |
| 253 | + form.addEventListener('submit', onSubmit); |
| 254 | + form.addEventListener('reset', onReset); |
| 255 | + |
| 256 | + // Handle Drawer open/close logic |
| 257 | + const drawer = document.querySelector('#filter-drawer'); |
| 258 | + const openDrawer = () => (drawer.active = true); |
| 259 | + const closeDrawer = () => (drawer.active = false); |
| 260 | + const closeAndClearDrawer = () => { |
| 261 | + drawerElements.forEach(el => { |
| 262 | + if (el && typeof el.value !== 'undefined') el.value = ''; |
| 263 | + }); |
| 264 | + updateBadge(); |
| 265 | + closeDrawer(); |
| 266 | + }; |
| 267 | + drawer.addEventListener('overlay-click-outside', closeAndClearDrawer); |
| 268 | + drawer.addEventListener('on-close', closeAndClearDrawer); |
| 269 | + |
| 270 | + // Initialize |
| 271 | + updateBadge(); |
| 272 | + ``` |
| 273 | +
|
| 274 | + </Unstyled> |
| 275 | +
|
| 276 | + ## Variants |
| 277 | + The Filter Bar can be adapted to multiple contexts and information densities. Common variations include: |
| 278 | + - **Product Filtering:** Filter accounts, campaigns, users, and other entities. |
| 279 | + - **Content Filtering in Lists or Tables:** Filter data by date, status, type, and other attributes. |
| 280 | + - **Filters by Category:** Group filters into logical categories or sections. |
| 281 | + - **Multi-Selection Filters:** Allow selecting multiple options within a single filter. |
| 282 | + - **Search Filter:** Filter results based on user-entered terms. |
| 283 | + - **Dynamic Filters:** Reveal additional filters based on a primary selection. |
| 284 | + - **Range-Based Filters:** Set numeric or date ranges (e.g., date range selectors). |
| 285 | + - **Sort Filter:** Sort results by different criteria. |
| 286 | + - **Custom Filters:** Create domain-specific filters based on user needs. |
| 287 | +
|
| 288 | + ## Accessibility |
| 289 | + - Ensure all controls have accessible names/labels (e.g., `sub-label`, `label`) |
| 290 | + - Maintain logical tab order; make focus states clearly visible |
| 291 | + - Use ARIA where appropriate for custom patterns; avoid redundant roles |
| 292 | + - Announce state changes (e.g., update counter text content) without stealing focus |
| 293 | + - Provide keyboard access to open/close drawers and operate menus |
| 294 | +
|
| 295 | + </div> |
| 296 | +</div> |
0 commit comments