Skip to content

Conversation

@dchyun
Copy link
Contributor

@dchyun dchyun commented Nov 24, 2025

📌 Summary

If merged, this PR would add a new FilterBar component to the library, and a FilterBar contextual component to the AdvancedTable.

Related spike PR: #3336

Preview links

🛠️ Detailed description

This PR adds support for filtering data through the new FilterBar component, and adds this as a contextual component to the AdvancedTable.

FilterBar

The FilterBar is a new component to the component library, which supports providing both a set of filter options through a dropdown with various contextual components, and tracking applied filters through the filters argument.

Filter types

The filters argument is used to track the various filters currently applied to a data set. A consumer will pass an object with various filters to this argument, and all filters will be displayed as dismissible tags.

The format of the object for the filters argument also matches the object that is bubbled up to the user when any filters are applied via the onFilter argument.

The following filter types are supported:

  • single-select
  • multi-select
  • numerical
  • date
  • datetime
  • time
  • custom / generic

The format of the object for the applied filters is as follows:

const FILTERS = {
  'column-single-select': {
    type: 'single-select',
    data: {
      value: '1',
      label: 'Option 1',
    }
  },
  'column-multi-select': {
    type: 'multi-select',
    data: [
      { value: '1', label: 'Option 1' },
      { value: '2', label: 'Option 2' },
    ]
  },
  'column-date': {
    type: 'date',
    data: {
      selector: 'before',
      value: '2025-01-01',
    }
  },
  'search': {
    type: 'search',
    data: {
      value: 'search term',
    }
  },
  'column-custom': {
    type: 'generic',
    dismissTagText: 'contains Custom Filter',
    data: {
      value: 'custom filter',
    },
  },
}

Filter contextual components
The options available for a user to select in the filters dropdown are provided via contextual Dropdown and FilterGroup components.

<HdsFilterBar @filters={{this.filters}} onFilter={{this.onFilter}} as |F|>
  <F.Dropdown as |D|>
    <D.FilterGroup
      @key="column-single-select"
      @text="Single-select"
      @type="single-select"
      as |F|
    >
      <F.Radio @value="1" @label="Option 1" />
      <F.Radio @value="2" @label="Option 2" />
      <F.Radio @value="3" @label="Option 3" />
    </D.FilterGroup>
    <D.FilterGroup
      @key="column-multi-select"
      @text="Multi-select"
      @type="multi-select"
      as |F|
    >
      <F.Checkbox @value="1" @label="Option 1" />
      <F.Checkbox @value="2" @label="Option 2" />
      <F.Checkbox @value="3" @label="Option 3" />
    </D.FilterGroup>
  </F.Dropdown>
</HdsFilterBar>

AdvancedTable

In the AdvancedTable filtering support has been added through a new named block :actions and contextual component FilterBar used inside that block, which uses the new FilterBar component.

Also a new named argument isEmpty and named block :emptyState have been added to support showing a block of content when no data is available. When isEmpty is true, the table body is not rendered, and the content inside the :emptyState is shown.

<HdsAdvancedTable @isEmpty={{this.isTableEmpty}}>
  <:actions as |A|>
    <A.FilterBar @filters={{this.filters}} onFilter={{this.onFilter}} as |F|>
      <F.Dropdown as |D|>
        <D.FilterGroup
          @key="column-single-select"
          @text="Single-select"
          @type="single-select"
          as |F|
        >
          <F.Radio @value="1" @label="Option 1" />
          <F.Radio @value="2" @label="Option 2" />
          <F.Radio @value="3" @label="Option 3" />
        </D.FilterGroup>
      </F.Dropdown>
    </A.FilterBar>
  </:actions>
  <:emptyState>
    <HdsTextDisplay>No data available</HdsTextDisplay>
  </:emptyState>
</HdsAdvancedTable>

📸 Screenshots

Filter Bar in AdvancedTable
Screenshot 2025-12-03 at 11 24 52 AM

AdvancedTable empty state
Screenshot 2025-12-03 at 11 25 07 AM

🔗 External links

Jira ticket: HDS-4591
Figma file: File


👀 Component checklist

💬 Please consider using conventional comments when reviewing this PR.

📋 PCI review checklist
  • If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
  • If applicable, I've worked with GRC to document the impact of any changes to security controls.
    Examples of changes to controls include access controls, encryption, logging, etc.
  • If applicable, I've worked with GRC to ensure compliance due to a significant change to the in-scope PCI environment.
    Examples include changes to operating systems, ports, protocols, services, cryptography-related components, PII processing code, etc.

@vercel
Copy link

vercel bot commented Nov 24, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
hds-showcase Ready Ready Preview Dec 8, 2025 2:24pm
hds-website Ready Ready Preview Dec 8, 2025 2:24pm

@github-actions
Copy link
Contributor

github-actions bot commented Nov 25, 2025

📦 RC Packages Published

Latest commit: aaa30cc

Published 1 packages

@hashicorp/design-system-components@5.1.0-rc-20251125211530

yarn up -C @hashicorp/design-system-components@rc

@dchyun dchyun added release-candidate Publishes release candidates to npm and removed release-candidate Publishes release candidates to npm labels Nov 25, 2025
@dchyun dchyun added the release-candidate Publishes release candidates to npm label Nov 25, 2025
@dchyun dchyun removed the release-candidate Publishes release candidates to npm label Nov 25, 2025
@dchyun dchyun mentioned this pull request Dec 2, 2025
5 tasks
@dchyun dchyun force-pushed the dchyun/filter-bar-component branch from 6078967 to 3b7d553 Compare December 3, 2025 16:00
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new FilterBar component to the design system library and integrates it as a contextual component within the AdvancedTable. The FilterBar supports multiple filter types (single-select, multi-select, numerical, date, datetime, time, and generic) and provides both immediate and deferred filtering modes. The AdvancedTable gains new filtering capabilities through an :actions named block and an :emptyState named block for displaying content when no data is available.

Key Changes

  • Added new FilterBar component with support for multiple filter types and live filtering
  • Integrated FilterBar as a contextual component in AdvancedTable via new :actions named block
  • Added isEmpty argument and :emptyState named block to AdvancedTable for empty state handling

Reviewed changes

Copilot reviewed 69 out of 69 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/components/src/components/hds/filter-bar/index.ts Main FilterBar component logic with filter management and state handling
packages/components/src/components/hds/filter-bar/types.ts TypeScript type definitions for all filter types and data structures
packages/components/src/styles/components/filter-bar.scss SCSS styles for FilterBar component and subcomponents
packages/components/src/components/hds/filter-bar/tabs/index.ts Tabs component for organizing filter groups
packages/components/src/components/hds/filter-bar/filter-group/index.ts FilterGroup component managing individual filter selections
packages/components/src/components/hds/filter-bar/dropdown.ts Dropdown wrapper for filter selection UI
packages/components/src/components/hds/advanced-table/index.ts Updated AdvancedTable to support FilterBar integration and empty state
packages/components/src/styles/components/advanced-table.scss Styles for FilterBar integration and empty state in AdvancedTable
showcase/app/mocks/run-data.ts Mock data for FilterBar demos and testing
showcase/app/components/page-components/filter-bar/index.gts Showcase page structure for FilterBar component
showcase/app/components/mock/app/main/generic-advanced-table.gts Updated demo with FilterBar integration and filtering logic
packages/components/translations/hds/components/filter-bar/en-us.yaml Translation strings for FilterBar component
packages/components/src/components.ts Export statements for new FilterBar components
packages/components/src/template-registry.ts Template registry entries for FilterBar components

Comment on lines +62 to +67
private _dropdownToggleElemenet!: HTMLDivElement;
private _appliedFiltersButtonId = 'applied-filters-button-' + guidFor(this);
private _appliedFiltersContentId = 'applied-filters-content-' + guidFor(this);

private _setUpFilterBar = modifier((element: HTMLDivElement) => {
this._dropdownToggleElemenet = element.querySelector(
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'Elemenet' to 'Element'.

Suggested change
private _dropdownToggleElemenet!: HTMLDivElement;
private _appliedFiltersButtonId = 'applied-filters-button-' + guidFor(this);
private _appliedFiltersContentId = 'applied-filters-content-' + guidFor(this);
private _setUpFilterBar = modifier((element: HTMLDivElement) => {
this._dropdownToggleElemenet = element.querySelector(
private _dropdownToggleElement!: HTMLDivElement;
private _appliedFiltersButtonId = 'applied-filters-button-' + guidFor(this);
private _appliedFiltersContentId = 'applied-filters-content-' + guidFor(this);
private _setUpFilterBar = modifier((element: HTMLDivElement) => {
this._dropdownToggleElement = element.querySelector(

Copilot uses AI. Check for mistakes.
<div class="hds-filter-bar__filter-group__search">
<Hds::Form::TextInput::Base
@type="search"
placeholder={{hds-t "components.filter-bar.filter-group.search-input-placeholder" default="Search"}}
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The translation key is inconsistent with other translation keys in the FilterBar component. It should use the 'hds.components' prefix like other keys (e.g., 'hds.components.filter-bar.filter-group.search-input-placeholder').

Suggested change
placeholder={{hds-t "components.filter-bar.filter-group.search-input-placeholder" default="Search"}}
placeholder={{hds-t "hds.components.filter-bar.filter-group.search-input-placeholder" default="Search"}}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@zamoore zamoore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a bunch of early feedback on the components. Still need to review the docs, styles, and do some QA testing.

Looking really nice!

<div class="hds-advanced-table__actions-container-wrapper">
{{#if (has-block "actions")}}
<div class="hds-advanced-table__actions">
{{yield (hash FilterBar=(component "hds/filter-bar")) to="actions"}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Is this actions block used in case we plan to add more yielded components or are we letting users add whatever they'd like here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was mainly used for organization since the rest of the component uses named blocks for other sections. It could also open up using more things inside there in the future, but they would probably be other contextual components we control. No need to allow people to add anything in.

Originally I did just have the FilterBar as a default contextual component, but because of the other named blocks a template like this doesn't work.

<HdsAdvancedTable as |T|>
  <T.FilterBar />
  <:body>
    ...
  </:body>
<HdsAdvancedTable>

The consumer has to use a <:default> named block, which isn't a pattern we've used in any other components.

<HdsAdvancedTable>
  <:default as |D|>
    <D.FilterBar />
  <:/default>
  <:body>
    ...
  </:body>
<HdsAdvancedTable>

I'm good with using a :default block if we want to go that way. This was more so just an organization and consistency choice.

{{/if}}
{{/unless}}

{{#if this.isEmpty}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Could this be an if/else with the condition above it?

if (isEmpty !== undefined) {
return isEmpty;
}
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]

I think return isEmpty ?? false my be a bit faster to read, but that's just a style thing.

HdsFilterBarData,
HdsFilterBarGenericFilterData,
} from './types.ts';
// import HdsDropdown from '../dropdown/index.ts';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code?


@tracked _isExpanded: boolean = this.hasActiveFilters;

private _dropdownToggleElemenet!: HTMLDivElement;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private _dropdownToggleElemenet!: HTMLDivElement;
private _dropdownToggleElement!: HTMLDivElement;

if (this.nodeIndex !== undefined && typeof onClick === 'function') {
onClick(event, this.nodeIndex);
} else {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could remove the else block unless you need to return false here.


export default class HdsFilterBarTabsPanel extends Component<HdsFilterBarTabsPanelSignature> {
private _panelId = 'panel-' + guidFor(this);
private _elementId?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No think it can be removed

this.internalFilters = newFilter;
} else {
if (Array.isArray(this.internalFilters)) {
this.internalFilters.push(newFilter);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Prefer non-mutative vhanges

Suggested change
this.internalFilters.push(newFilter);
this.internalFilters = [...this.internalFilters, newFilter];

}

private onSearch = (event: Event) => {
const listItems = this._panelElement.querySelectorAll(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] - Can we use the modifier registration pattern here rather than query for elements?

const text = item.textContent.toLowerCase();
const searchText = input.value.toLowerCase();
if (text.includes(searchText)) {
item.classList.remove(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way we can do this idiomatically in the template?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants