Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5d79307
Add more features to tune menu
edlu77 Oct 31, 2025
9368424
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Oct 31, 2025
eb6245b
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Nov 14, 2025
091a5fb
Rename to filter menu and other updates
edlu77 Nov 17, 2025
31b9f70
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Nov 17, 2025
aa4a51a
Add filter container from design system to menu
edlu77 Nov 17, 2025
7bfc6d3
Documentation
edlu77 Nov 17, 2025
9929865
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Nov 17, 2025
2364ceb
Update filter list flyout
edlu77 Nov 19, 2025
737db90
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Nov 21, 2025
72f5213
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Nov 21, 2025
c65dc18
Fixes and updates
edlu77 Nov 21, 2025
972bff3
Update index.ts
edlu77 Dec 1, 2025
be3e962
Update stories for filter menu
edlu77 Dec 1, 2025
ff0236b
Filter menu story styling updates
edlu77 Dec 1, 2025
c9127d1
Fixes
edlu77 Dec 1, 2025
81fda5b
Merge branch 'develop' into tune-menu-shell
edlu77 Dec 1, 2025
d1ea9fe
Merge branch 'develop' into tune-menu-shell
edlu77 Dec 2, 2025
5071965
Merge branch 'develop' of https://github.com/hubmapconsortium/hra-ui …
edlu77 Dec 3, 2025
757aeae
Merge branch 'tune-menu-shell' of https://github.com/hubmapconsortium…
edlu77 Dec 3, 2025
1e4c985
Add search list to filter container
edlu77 Dec 3, 2025
954c44b
Update chips on menu selection changes
edlu77 Dec 5, 2025
be683a7
Increase test coverage
edlu77 Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions libs/design-system/assets/logo/cns_header_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion libs/design-system/filter-container/src/index.ts

This file was deleted.

3 changes: 3 additions & 0 deletions libs/design-system/filter-menu/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './lib/filter-menu.component';

export { FilterContainerComponent, type FilterChip } from './lib/filter-container/filter-container.component';
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,25 @@
hraButtonVariant="secondary"
class="category-button"
color="primary"
(click)="actionClick.emit()"
cdkOverlayOrigin
(click)="toggleMenu()"
#desktopMenuOrigin="cdkOverlayOrigin"
>
{{ action() }}
</button>

<ng-template
cdkConnectedOverlay
cdkConnectedOverlayHasBackdrop="false"
cdkConnectedOverlayLockPosition="true"
cdkConnectedOverlayPush="true"
[cdkConnectedOverlayOpen]="menuActive()"
[cdkConnectedOverlayOrigin]="desktopMenuOrigin"
[cdkConnectedOverlayPositions]="desktopMenuPositions"
(overlayOutsideClick)="toggleMenu()"
>
<hra-search-list [options]="filter()?.options || []" [(selected)]="selected" />
</ng-template>
</div>

@if (chips().length > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@use '@angular/material' as mat;
@use '../../../styles/vars';
@use '../../../../styles/vars';

:host {
display: block;
Expand All @@ -9,7 +9,6 @@
.filter-container {
display: flex;
flex-direction: column;
gap: 0.25rem;

.top-row {
display: flex;
Expand All @@ -25,11 +24,15 @@
flex: 0 1 auto;
min-width: fit-content;
justify-content: flex-start;
width: 100%;

@include mat.button-overrides(
(
text-horizontal-padding: 0.75rem,
text-label-text-font: vars.$label-medium-font,
text-label-text-size: vars.$label-medium-size,
text-label-text-tracking: vars.$label-medium-tracking,
text-label-text-weight: vars.$label-medium-weight,
)
);
}
Expand Down Expand Up @@ -64,6 +67,6 @@
}

mat-divider {
margin-top: 0.5rem;
margin: 0.25rem 0;
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { render, screen } from '@testing-library/angular';
import userEvent from '@testing-library/user-event';
import { FilterContainerComponent, FilterChip } from './filter-container.component';
import { FilterContainerComponent } from './filter-container.component';
import { SearchListOption } from '@hra-ui/design-system/search-list';

describe('FilterContainerComponent', () => {
async function setup(options: {
action: string;
showTooltip?: boolean;
chips?: FilterChip[];
selected?: SearchListOption[];
enableDivider?: boolean;
}) {
return render(FilterContainerComponent, {
componentInputs: {
filter: {
options: [
{ id: 'option1', label: 'Option 1' },
{ id: 'option2', label: 'Option 2' },
{ id: 'option3', label: 'Option 3' },
],
},
action: options.action,
showTooltip: options.showTooltip ?? false,
chips: options.chips ?? [],
selected: options.selected ?? [],
enableDivider: options.enableDivider ?? false,
},
});
Expand Down Expand Up @@ -49,15 +57,18 @@ describe('FilterContainerComponent', () => {
});

it('should display chips', async () => {
const chips: FilterChip[] = [{ label: 'Chip 1' }, { label: 'Chip 2' }];
const selected: SearchListOption[] = [
{ id: 'option1', label: 'Option 1' },
{ id: 'option2', label: 'Option 2' },
];

await setup({
action: 'Test',
chips,
selected,
});

expect(screen.getByText('Chip 1')).toBeInTheDocument();
expect(screen.getByText('Chip 2')).toBeInTheDocument();
expect(screen.getByText('Option 1')).toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});

it('should emit actionClick when category button is clicked', async () => {
Expand All @@ -78,19 +89,44 @@ describe('FilterContainerComponent', () => {

it('should remove chip from model when chip remove button is clicked', async () => {
const user = userEvent.setup();
const chips: FilterChip[] = [{ label: 'Chip 1' }, { label: 'Chip 2' }];
const selected: SearchListOption[] = [
{ id: 'option1', label: 'Option 1' },
{ id: 'option2', label: 'Option 2' },
];

await setup({
action: 'Test',
chips,
selected,
});

const removeButton = screen.getByRole('button', { name: 'Remove Chip 1' });
const removeButton = screen.getByRole('button', { name: 'Remove Option 1' });
await user.click(removeButton);

// Check that the chip was removed from the DOM
expect(screen.queryByText('Chip 1')).not.toBeInTheDocument();
expect(screen.getByText('Chip 2')).toBeInTheDocument();
expect(screen.queryByText('Option 1')).not.toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});

it('should update chips when option in search list is clicked', async () => {
const user = userEvent.setup();
const selected: SearchListOption[] = [
{ id: 'option1', label: 'Option 1' },
{ id: 'option2', label: 'Option 2' },
];

await setup({
action: 'Test',
selected,
});

const button = screen.getByRole('button', { name: 'Test' });
await user.click(button);
const option = screen.getByRole('option', { name: 'Toggle option1' });
await user.click(option);

// Check that the chip was removed from the DOM
expect(screen.queryByText('Remove Option 1')).not.toBeInTheDocument();
expect(screen.getByText('Option 2')).toBeInTheDocument();
});

it('should show divider when enableDivider is true', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Meta, StoryObj } from '@storybook/angular';
import { FilterContainerComponent, FilterChip } from './filter-container.component';
import { SearchListOption } from '@hra-ui/design-system/search-list';

const meta: Meta<FilterContainerComponent<FilterChip>> = {
component: FilterContainerComponent,
title: 'Design System/Filter Container',
title: 'Design System/Filter Menu/Filter Container',
parameters: {
design: {
type: 'figma',
Expand Down Expand Up @@ -38,7 +39,11 @@ const meta: Meta<FilterContainerComponent<FilterChip>> = {
export default meta;
type Story = StoryObj<FilterContainerComponent<FilterChip>>;

const sampleChips: FilterChip[] = [{ label: 'Option 1' }, { label: 'Option 2' }, { label: 'Option 3' }];
const sampleOptions: SearchListOption[] = [
{ id: 'option1', label: 'Option 1' },
{ id: 'option2', label: 'Option 2' },
{ id: 'option3', label: 'Option 3' },
];

export const Default: Story = {
render: (args) => ({
Expand All @@ -65,9 +70,9 @@ export const WithInfoButton: Story = {

export const WithChipsAndInfo: Story = {
render: (args) => ({
props: { ...args, chips: sampleChips },
props: { ...args, selected: sampleOptions },
template: `
<hra-filter-container [action]="action" [showTooltip]="true" [chips]="chips" [enableDivider]="enableDivider" (actionClick)="actionClick($event)">
<hra-filter-container [action]="action" [showTooltip]="true" [selected]="selected" [enableDivider]="enableDivider" (actionClick)="actionClick($event)">
<p tooltipContent>This filter allows you to refine your search by selecting specific options from the available choices.</p>
<button mat-button color="accent" tooltipActions>
<a href="https://example.com" target="_blank" rel="noopener noreferrer" style="text-decoration: none; color: inherit;">
Expand All @@ -84,9 +89,9 @@ export const WithChipsAndInfo: Story = {

export const WithDivider: Story = {
render: (args) => ({
props: { ...args, chips: sampleChips },
props: { ...args, selected: sampleOptions },
template: `
<hra-filter-container [action]="action" [showTooltip]="true" [chips]="chips" [enableDivider]="enableDivider" (actionClick)="actionClick($event)">
<hra-filter-container [action]="action" [showTooltip]="true" [selected]="selected" [enableDivider]="enableDivider" (actionClick)="actionClick($event)">
<p tooltipContent>This filter allows you to refine your search by selecting specific options from the available choices.</p>
<button mat-button color="accent" tooltipActions>
<a href="https://example.com" target="_blank" rel="noopener noreferrer" style="text-decoration: none; color: inherit;">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { booleanAttribute, ChangeDetectionStrategy, Component, input, model, output } from '@angular/core';
import { booleanAttribute, ChangeDetectionStrategy, Component, effect, input, model, output } from '@angular/core';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatDividerModule } from '@angular/material/divider';
Expand All @@ -9,13 +9,21 @@ import {
InfoButtonTaglineDirective,
InfoButtonActionsDirective,
} from '@hra-ui/design-system/buttons/info-button';
import { FilterOptionCategory } from '../filter-menu.component';
import { ConnectedPosition, OverlayModule } from '@angular/cdk/overlay';
import { SearchListComponent, SearchListOption } from '@hra-ui/design-system/search-list';

/** A filter chip representing a selected filter option */
export interface FilterChip {
/** Label for the chip */
label: string;
}

/** Position of the desktop menu overlay */
const DESKTOP_MENU_POSITIONS: ConnectedPosition[] = [
{ originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top', offsetX: 16 },
];

/**
* Design system filter container component
*/
Expand All @@ -30,6 +38,8 @@ export interface FilterChip {
InfoButtonComponent,
InfoButtonTaglineDirective,
InfoButtonActionsDirective,
OverlayModule,
SearchListComponent,
],
templateUrl: './filter-container.component.html',
styleUrl: './filter-container.component.scss',
Expand All @@ -39,23 +49,58 @@ export class FilterContainerComponent<T extends FilterChip> {
/** tagline for the filter category */
readonly action = input.required<string>();

/** Filter option */
readonly filter = input<FilterOptionCategory>();

/** Whether to show the info button with tooltip */
readonly showTooltip = input(false, { transform: booleanAttribute });

/** Array of selected filter chips - two-way bindable */
readonly chips = model<T[]>([]);

/** Array of selected options */
readonly selected = model<SearchListOption[]>([]);

/** Whether the menu is active/open */
readonly menuActive = model<boolean>(false);

/** Whether to show a divider below the container */
readonly enableDivider = input(false, { transform: booleanAttribute });

/** Emits when the category button is clicked */
readonly actionClick = output<void>();

/** Overlay positions for the desktop menu */
protected readonly desktopMenuPositions = DESKTOP_MENU_POSITIONS;

/**
* Updates chips on selection change
*/
constructor() {
effect(() => {
const selected = this.selected();
this.chips.set(
selected.map((x) => {
return { label: x.label } as T;
}),
);
});
}

/**
* Handles the removal of a chip
* @param chip The chip to remove
*/
removeChip(chip: T): void {
this.chips.update((current) => current.filter((c) => c.label !== chip.label));
this.selected.update((current) => current?.filter((c) => c.label !== chip.label));
}

/**
* Toggles menu active state and emits action click event
*/
toggleMenu(): void {
this.menuActive.set(!this.menuActive());
this.actionClick.emit();
}
}
48 changes: 48 additions & 0 deletions libs/design-system/filter-menu/src/lib/filter-menu.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@if (tagline() || description() || enableClose()) {
<div hraFeature="header" class="header-section">
<div class="drawer-header">
<span class="drawer-title">{{ tagline() }}</span>
<div class="filler"></div>
@if (!isWideScreen() && enableClose()) {
<button
hraFeature="close"
hraClickEvent
hraHoverEvent
mat-icon-button
aria-label="Close filter menu"
(click)="closeClick.emit()"
>
<hra-icon class="menu-icon" fontIcon="close" />
</button>
}
</div>
<div class="header-subtext">{{ description() }}</div>
</div>
}

<ng-scrollbar hraScrollOverflowFade class="filter-options">
@if (controls()) {
<div class="customize-section">
<div class="customize-header">
<hra-icon class="customize-icon" fontIcon="tune" />
<span class="customize-title">Customize</span>
</div>
<ng-content />
</div>
}

<div class="filters-section">
<div class="filters-header">
<hra-icon class="filter-icon" fontIcon="filter_list" />
<span class="filters-title">Filters</span>
</div>
@for (filter of filters(); track $index) {
<hra-filter-container
enableDivider
[action]="filter.label"
[filter]="filter"
(actionClick)="filterChange.emit()"
/>
}
</div>
</ng-scrollbar>
Loading