Skip to content

Commit f134f79

Browse files
committed
Feat: Filter bar tabs component
1 parent 72ce94b commit f134f79

File tree

13 files changed

+580
-20
lines changed

13 files changed

+580
-20
lines changed

packages/components/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@
242242
"./components/hds/filter-bar.js": "./dist/_app_/components/hds/filter-bar.js",
243243
"./components/hds/filter-bar/radio.js": "./dist/_app_/components/hds/filter-bar/radio.js",
244244
"./components/hds/filter-bar/range.js": "./dist/_app_/components/hds/filter-bar/range.js",
245+
"./components/hds/filter-bar/tabs.js": "./dist/_app_/components/hds/filter-bar/tabs.js",
246+
"./components/hds/filter-bar/tabs/panel.js": "./dist/_app_/components/hds/filter-bar/tabs/panel.js",
247+
"./components/hds/filter-bar/tabs/tab.js": "./dist/_app_/components/hds/filter-bar/tabs/tab.js",
245248
"./components/hds/flyout/body.js": "./dist/_app_/components/hds/flyout/body.js",
246249
"./components/hds/flyout/description.js": "./dist/_app_/components/hds/flyout/description.js",
247250
"./components/hds/flyout/footer.js": "./dist/_app_/components/hds/flyout/footer.js",

packages/components/src/components.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export { default as HdsFilterBarFiltersDropdown } from './components/hds/filter-
137137
export { default as HdsFilterBarFilterGroup } from './components/hds/filter-bar/filter-group.ts';
138138
export { default as HdsFilterBarRadio } from './components/hds/filter-bar/radio.ts';
139139
export { default as HdsFilterBarRange } from './components/hds/filter-bar/range.ts';
140+
export { default as HdsFilterBarTabs } from './components/hds/filter-bar/tabs/index.ts';
141+
export { default as HdsFilterBarTabsPanel } from './components/hds/filter-bar/tabs/panel.ts';
142+
export { default as HdsFilterBarTabsTab } from './components/hds/filter-bar/tabs/tab.ts';
140143
export * from './components/hds/filter-bar/types.ts';
141144

142145
// Flyout

packages/components/src/components/hds/filter-bar/filter-group.hbs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,33 @@
33
SPDX-License-Identifier: MPL-2.0
44
}}
55
{{#let @tab as |Tab|}}
6-
<Tab class="hds-filter-bar__filters-dropdown__filter-tab">
7-
<Hds::Text::Body>{{@text}}</Hds::Text::Body>
6+
<Tab>
7+
{{@text}}
88
{{#unless (eq this.numFilters 0)}}
9-
<Hds::Text::Body>{{this.numFilters}}</Hds::Text::Body>
9+
<Hds::BadgeCount
10+
@text={{this.numFilters}}
11+
@type="outlined"
12+
@size="small"
13+
class="hds-filter-bar__filters-dropdown__filter-group__filters-count"
14+
/>
1015
{{/unless}}
1116
</Tab>
1217
{{/let}}
1318
{{#let @panel as |Panel|}}
14-
<Panel class="hds-filter-bar__filters-dropdown__filter-options" {{this._setUpFilterPanel}}>
19+
<Panel {{this._setUpFilterPanel}}>
1520
{{#if @searchEnabled}}
16-
<div class="hds-filter-bar__filters-dropdown__filter-options__search">
21+
<div class="hds-filter-bar__filters-dropdown__filter-group__search">
1722
<Hds::Form::TextInput::Base
1823
@type="search"
19-
placeholder={{hds-t "components.filter-bar.filter-options.search-input-placeholder" default="Search"}}
24+
placeholder={{hds-t "components.filter-bar.filter-group.search-input-placeholder" default="Search"}}
2025
{{on "input" this.onSearch}}
2126
/>
2227
</div>
2328
{{/if}}
2429
{{#if (eq @type "range")}}
2530
<Hds::FilterBar::Range @keyFilter={{this.keyFilter}} @onChange={{this.onRangeChange}} />
2631
{{else}}
27-
<ul class="hds-filter-bar__filters-dropdown__filter-options__list">
32+
<ul class="hds-filter-bar__filters-dropdown__filter-group__list">
2833
{{yield
2934
(hash
3035
Checkbox=(component "hds/filter-bar/checkbox" keyFilter=this.keyFilter onChange=this.onSelectionChange)

packages/components/src/components/hds/filter-bar/filter-group.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { tracked } from '@glimmer/tracking';
99
import { modifier } from 'ember-modifier';
1010
import type { WithBoundArgs } from '@glint/template';
1111

12-
import HdsTabsTab from '../tabs/tab.ts';
13-
import HdsTabsPanel from '../tabs/panel.ts';
12+
import HdsFilterBarTabsTab from './tabs/tab.ts';
13+
import HdsFilterBarTabsPanel from './tabs/panel.ts';
1414
import type { HdsTabsPanelSignature } from '../tabs/panel.ts';
1515

1616
import HdsFilterBarCheckbox from './checkbox.ts';
@@ -28,8 +28,8 @@ import type {
2828

2929
export interface HdsFilterBarFilterGroupSignature {
3030
Args: {
31-
tab?: WithBoundArgs<typeof HdsTabsTab, never>;
32-
panel?: WithBoundArgs<typeof HdsTabsPanel, never>;
31+
tab?: WithBoundArgs<typeof HdsFilterBarTabsTab, never>;
32+
panel?: WithBoundArgs<typeof HdsFilterBarTabsPanel, never>;
3333
key: string;
3434
text: string;
3535
type?: HdsFilterBarFilterType;
@@ -192,7 +192,7 @@ export default class HdsFilterBarFilterGroup extends Component<HdsFilterBarFilte
192192
}
193193

194194
get classNames(): string {
195-
const classes = ['hds-filter-bar__filter-options'];
195+
const classes = ['hds-filter-bar__filter-group'];
196196

197197
classes.push(`hds-filter-bar__dropdown--type-${this.type}`);
198198

packages/components/src/components/hds/filter-bar/filters-dropdown.hbs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
}}
55
<Hds::Dropdown
66
@listPosition="bottom-left"
7-
@height="500px"
8-
@width="500px"
7+
@height="600px"
8+
@width="600px"
99
class={{this.classNames}}
1010
@onClose={{this._onClose}}
1111
{{this._syncFilters @filters}}
1212
as |D|
1313
>
1414
<D.ToggleButton @icon="filter" @text="Filter" @color="secondary" @size="small" />
1515
<D.Generic>
16-
<Hds::Tabs as |T|>
16+
<Hds::FilterBar::Tabs @ariaLabel="Filter bar tabs" as |T|>
1717
{{yield
1818
(hash
1919
FilterGroup=(component
@@ -22,7 +22,7 @@
2222
close=D.close
2323
)
2424
}}
25-
</Hds::Tabs>
25+
</Hds::FilterBar::Tabs>
2626
</D.Generic>
2727
<D.Footer @hasDivider={{true}}>
2828
<Hds::Layout::Flex @direction="row" @justify="space-between" @align="center" as |LF|>
@@ -44,7 +44,12 @@
4444
"hds.components.filter-bar.filters-dropdown.selected-filters"
4545
default="Filters selected"
4646
}}:</Hds::Text::Body>
47-
<Hds::BadgeCount @text={{this.numFilters}} @type="outlined" />
47+
<Hds::BadgeCount
48+
@text={{this.numFilters}}
49+
@type="outlined"
50+
@size="small"
51+
class="hds-filter-bar__filters-dropdown__filters-count"
52+
/>
4853
</LF.Item>
4954
</Hds::Layout::Flex>
5055
</D.Footer>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
<div class="hds-filter-bar__tabs" ...attributes {{this._setUpFilterBarTabs}}>
6+
<ol class="hds-filter-bar__tabs__list" aria-label={{@ariaLabel}} role="tablist">
7+
{{yield
8+
(hash
9+
Tab=(component
10+
"hds/filter-bar/tabs/tab"
11+
selectedTabIndex=this._selectedTabIndex
12+
tabIds=this._tabIds
13+
panelIds=this._panelIds
14+
didInsertNode=this.didInsertTab
15+
willDestroyNode=this.willDestroyTab
16+
onClick=this.onClick
17+
onKeyUp=this.onKeyUp
18+
)
19+
)
20+
}}
21+
</ol>
22+
{{yield
23+
(hash
24+
Panel=(component
25+
"hds/filter-bar/tabs/panel"
26+
selectedTabIndex=this._selectedTabIndex
27+
tabIds=this._tabIds
28+
panelIds=this._panelIds
29+
didInsertNode=this.didInsertPanel
30+
willDestroyNode=this.willDestroyPanel
31+
)
32+
)
33+
}}
34+
</div>
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
import Component from '@glimmer/component';
7+
import { tracked } from '@glimmer/tracking';
8+
import { action } from '@ember/object';
9+
import { schedule } from '@ember/runloop';
10+
import { modifier } from 'ember-modifier';
11+
import type { WithBoundArgs } from '@glint/template';
12+
import HdsFilterBarTabsTabComponent from './tab.ts';
13+
import HdsFilterBarTabsPanelComponent from './panel.ts';
14+
15+
const TAB_ELEMENT_SELECTOR = '.hds-filter-bar__tabs__tab__button';
16+
const PANEL_ELEMENT_SELECTOR = '.hds-filter-bar__tabs__panel';
17+
18+
export interface HdsFilterBarTabsSignature {
19+
Args: {
20+
selectedTabIndex?: number;
21+
ariaLabel: string;
22+
onClickTab?: (event: MouseEvent, tabIndex: number) => void;
23+
};
24+
Blocks: {
25+
default: [
26+
{
27+
Tab?: WithBoundArgs<
28+
typeof HdsFilterBarTabsTabComponent,
29+
| 'selectedTabIndex'
30+
| 'tabIds'
31+
| 'panelIds'
32+
| 'didInsertNode'
33+
| 'willDestroyNode'
34+
| 'onClick'
35+
| 'onKeyUp'
36+
>;
37+
Panel?: WithBoundArgs<
38+
typeof HdsFilterBarTabsPanelComponent,
39+
| 'selectedTabIndex'
40+
| 'tabIds'
41+
| 'panelIds'
42+
| 'didInsertNode'
43+
| 'willDestroyNode'
44+
>;
45+
},
46+
];
47+
};
48+
Element: HTMLDivElement;
49+
}
50+
51+
export default class HdsFilterBarTabs extends Component<HdsFilterBarTabsSignature> {
52+
@tracked private _tabIds: string[] = [];
53+
@tracked private _tabNodes: HTMLElement[] = [];
54+
@tracked private _panelNodes: HTMLElement[] = [];
55+
@tracked private _panelIds: string[] = [];
56+
@tracked private _selectedTabIndex: number = 0;
57+
@tracked private _selectedTabId?: string;
58+
59+
private _element!: HTMLDivElement;
60+
61+
private _setUpFilterBarTabs = modifier((element: HTMLDivElement) => {
62+
const { selectedTabIndex } = this.args;
63+
64+
if (selectedTabIndex) {
65+
this._selectedTabIndex = selectedTabIndex;
66+
}
67+
68+
this._element = element;
69+
70+
return () => {};
71+
});
72+
73+
@action
74+
didInsertTab(): void {
75+
// eslint-disable-next-line ember/no-runloop
76+
schedule('afterRender', (): void => {
77+
this.updateTabs();
78+
});
79+
}
80+
81+
@action
82+
willDestroyTab(element: HTMLElement): void {
83+
// eslint-disable-next-line ember/no-runloop
84+
schedule('afterRender', (): void => {
85+
this._tabNodes = this._tabNodes.filter(
86+
(node): boolean => node.id !== element.id
87+
);
88+
this._tabIds = this._tabIds.filter(
89+
(tabId): boolean => tabId !== element.id
90+
);
91+
});
92+
}
93+
94+
@action
95+
didInsertPanel(): void {
96+
// eslint-disable-next-line ember/no-runloop
97+
schedule('afterRender', (): void => {
98+
this.updatePanels();
99+
});
100+
}
101+
102+
@action
103+
willDestroyPanel(element: HTMLElement): void {
104+
// eslint-disable-next-line ember/no-runloop
105+
schedule('afterRender', (): void => {
106+
this._panelNodes = this._panelNodes.filter(
107+
(node): boolean => node.id !== element.id
108+
);
109+
this._panelIds = this._panelIds.filter(
110+
(panelId): boolean => panelId !== element.id
111+
);
112+
});
113+
}
114+
115+
@action
116+
onClick(event: MouseEvent, tabIndex: number): void {
117+
console.log('onClick event in Tabs:', event, tabIndex);
118+
this._selectedTabIndex = tabIndex;
119+
120+
// invoke the callback function if it's provided as argument
121+
if (typeof this.args.onClickTab === 'function') {
122+
this.args.onClickTab(event, tabIndex);
123+
}
124+
}
125+
126+
@action
127+
onKeyUp(event: KeyboardEvent, tabIndex: number): void {
128+
const leftArrow = 'ArrowLeft';
129+
const rightArrow = 'ArrowRight';
130+
const upArrow = 'ArrowUp';
131+
const downArrow = 'ArrowDown';
132+
const enterKey = 'Enter';
133+
const spaceKey = ' ';
134+
135+
if (event.key === rightArrow || event.key === downArrow) {
136+
const nextTabIndex = (tabIndex + 1) % this._tabIds.length;
137+
this.focusTab(nextTabIndex, event);
138+
} else if (event.key === leftArrow || event.key === upArrow) {
139+
const prevTabIndex =
140+
(tabIndex + this._tabIds.length - 1) % this._tabIds.length;
141+
this.focusTab(prevTabIndex, event);
142+
} else if (event.key === enterKey || event.key === spaceKey) {
143+
this._selectedTabIndex = tabIndex;
144+
}
145+
// scroll selected tab into view (it may be out of view when activated using a keyboard with `prev/next`)
146+
const parentNode = this._tabNodes[this._selectedTabIndex]?.parentNode;
147+
if (parentNode instanceof HTMLElement) {
148+
parentNode.scrollIntoView({
149+
behavior: 'smooth',
150+
block: 'nearest',
151+
inline: 'nearest',
152+
});
153+
}
154+
}
155+
156+
// Focus tab for keyboard & mouse navigation:
157+
focusTab(tabIndex: number, event: KeyboardEvent): void {
158+
event.preventDefault();
159+
this._tabNodes[tabIndex]?.focus();
160+
}
161+
162+
// Update the tab arrays based on how they are ordered in the DOM
163+
private updateTabs(): void {
164+
const tabs = this._element.querySelectorAll(TAB_ELEMENT_SELECTOR);
165+
let newTabIds: string[] = [];
166+
let newTabNodes: HTMLElement[] = [];
167+
tabs.forEach((tab) => {
168+
newTabIds = [...newTabIds, tab.id];
169+
newTabNodes = [...newTabNodes, tab as HTMLElement];
170+
});
171+
this._tabIds = newTabIds;
172+
this._tabNodes = newTabNodes;
173+
}
174+
175+
// Update the panel arrays based on how they are ordered in the DOM
176+
private updatePanels(): void {
177+
const panels = this._element.querySelectorAll(PANEL_ELEMENT_SELECTOR);
178+
let newPanelIds: string[] = [];
179+
let newPanelNodes: HTMLElement[] = [];
180+
panels.forEach((panel) => {
181+
newPanelIds = [...newPanelIds, panel.id];
182+
newPanelNodes = [...newPanelNodes, panel as HTMLElement];
183+
});
184+
this._panelIds = newPanelIds;
185+
this._panelNodes = newPanelNodes;
186+
}
187+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{{!
2+
Copyright (c) HashiCorp, Inc.
3+
SPDX-License-Identifier: MPL-2.0
4+
}}
5+
<section
6+
class="hds-filter-bar__tabs__panel"
7+
...attributes
8+
role="tabpanel"
9+
id={{this._panelId}}
10+
hidden={{not this.isVisible}}
11+
aria-labelledby={{this.coupledTabId}}
12+
{{this._setUpPanel this.didInsertNode this.willDestroyNode}}
13+
>
14+
{{#if this.isVisible}}
15+
{{yield}}
16+
{{/if}}
17+
</section>

0 commit comments

Comments
 (0)