Skip to content

Commit

Permalink
Merge pull request #2063 from HealthCatalyst/dev
Browse files Browse the repository at this point in the history
Cashmere 13.7.2
  • Loading branch information
andrew-frueh authored Feb 28, 2023
2 parents db3bbc2 + aba6d98 commit 90deb33
Show file tree
Hide file tree
Showing 24 changed files with 172 additions and 220 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<div class="tab-demo">
<hc-tab-set
direction="horizontal"
defaultTab="2"
[selectedTab]="currentSelected"
(selectedTabChange)="selectionChanged($event)"
>
Expand All @@ -24,7 +25,7 @@
<div class="tab-select-container">
<hc-form-field inline>
<hc-label>Tab Selected:</hc-label>
<hc-select [formControl]="tabSelected">
<hc-select [formControl]="tabSelected" (change)="controlChanged()">
<option value="0">First Tab</option>
<option value="1">Second Tab</option>
<option value="2">Third Tab</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
import {Component, OnDestroy, OnInit} from '@angular/core';
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { TabChangeEvent } from '@healthcatalyst/cashmere';
import { Subject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';

@Component({
selector: 'hc-selected-tab-input-example',
templateUrl: 'selected-tab-input-example.component.html',
// TODO: delete the SCSS file if you don't need it in the example
styleUrls: ['selected-tab-input-example.component.scss']
})
export class SelectedTabInputExampleComponent implements OnInit, OnDestroy {

tabSelected = new FormControl(1);
currentSelected = 1;
unsubscribe = new Subject<void>();
export class SelectedTabInputExampleComponent {
tabSelected = new FormControl(2);
currentSelected = 2;

selectionChanged(event: TabChangeEvent): void {
this.tabSelected.setValue(event.index);
}

ngOnInit(): void {
this.tabSelected.valueChanges.pipe(
map(value => {
if (value) {
return Number(value);
}

return 0;
}),
takeUntil(this.unsubscribe)
).subscribe(value => {
this.currentSelected = value;
});
}

ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
controlChanged(): void {
this.currentSelected = Number(this.tabSelected.value);
}
}
4 changes: 4 additions & 0 deletions projects/cashmere/src/lib/sass/tab.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
}
}

@mixin hc-tab-active() {
pointer-events: none;
}

@mixin hc-tab-vertical() {
background-color: $white;
color: $offblack;
Expand Down
84 changes: 41 additions & 43 deletions projects/cashmere/src/lib/tabs/tab-set/tab-set.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {QueryList} from '@angular/core';
import {EventEmitter, TemplateRef} from '@angular/core';
import {TabComponent} from '../tab/tab.component';
import {ActivatedRoute, Router} from '@angular/router';
import {Subject, interval, fromEvent, BehaviorSubject, Subscription} from 'rxjs';
import {Subject, interval, fromEvent, Subscription} from 'rxjs';
import {distinctUntilChanged, filter, take, takeUntil} from 'rxjs/operators';
import {parseBooleanAttribute} from '../../util';
import {HcPopoverAnchorDirective} from '../../pop';
Expand Down Expand Up @@ -72,7 +72,7 @@ export class TabSetComponent implements AfterContentInit {
private _tabsTotalWidth = 0;
public _collapse = false;
public _moreList: Array<TabComponent> = [];
private _selectedTabSubject = new BehaviorSubject<number | TabComponent>(0);
private _selectedTabSubject = new Subject<number | TabComponent>();
private unsubscribe = new Subject<void>();

/** The content to be displayed for the currently selected tab.
Expand Down Expand Up @@ -117,7 +117,8 @@ export class TabSetComponent implements AfterContentInit {
}

/** Zero-based numerical value specifying which tab to select by default, setting to `none` means no tab
* will be immediately selected. Defaults to 0 (the first tab). */
* will be immediately selected. Defaults to 0 (the first tab).
* For tabs using routing, the default tab will be set by the url and use this value as a fallback if no tab routerLinks match the url. */
@Input()
get defaultTab(): string | number {
return this._defaultTab;
Expand Down Expand Up @@ -178,14 +179,27 @@ export class TabSetComponent implements AfterContentInit {
const selectedChanged$ = this._selectedTabSubject.pipe(distinctUntilChanged(),filter(nextTab => nextTab !== this.selectedTab));
selectedChanged$.pipe(takeUntil(this.unsubscribe)).subscribe(selectedTab => {
setTimeout(() => {
this.selectTab(selectedTab, false, false);
this.selectTab(selectedTab, false);
this.changeDetector.detectChanges();
});
});

this.setUpTabs(false);
this.refreshTabWidths();

// If using routing and routerLinkActive hasn't set any active tabs, default to the first route
if ( this._routerEnabled ) {
setTimeout(() => {
const noActiveTabs = !this._tabs.some(t => t._active);
if ( this.defaultTab !== 'none' && noActiveTabs ) {
const tabArray = this._tabs.toArray();
if ( tabArray[Number(this.defaultTab)] ) {
this.selectTab( tabArray[Number(this.defaultTab)] );
}
}
});
}

// If links are added dynamically, recheck the tab widths
this._tabs.changes.pipe(takeUntil(this.unsubscribe)).subscribe(() => {
this.setUpTabs(true);
Expand Down Expand Up @@ -362,10 +376,12 @@ export class TabSetComponent implements AfterContentInit {
throw tabComponentMissing();
}

if (this.defaultTab !== 'none' && !changeEvent) {
this.checkForRouterUse();

if (!this._routerEnabled && this.defaultTab !== 'none' && !changeEvent) {
this.defaultToFirstTab();
}
this.checkForRouterUse();

this.setTabDirection();
this.subscribeToTabEvents();
}
Expand Down Expand Up @@ -402,6 +418,14 @@ export class TabSetComponent implements AfterContentInit {
this._stopTabSubscriptionSubject.next();
this._tabs.forEach(t => t.tabClick.pipe(takeUntil(this._stopTabSubscriptionSubject)).subscribe(() => this._setActive(t)));

// If using tabs with routing, listen for changes to the routerLinkActive state
if ( this._routerEnabled ) {
this._tabs.forEach(t => t._routerActiveChange.pipe(takeUntil(this._stopTabSubscriptionSubject)).subscribe(tab => {
this._selectedTab = tab;
this._setActive(tab, false);
}));
}

// Watch for changes to a tab's [hidden] param to remove it from overflow calculations
this._tabs.forEach(t => t._tabHideChange.pipe(takeUntil(this._stopTabSubscriptionSubject)).subscribe(() => {
if ( t.hidden && t._active ) {
Expand All @@ -414,12 +438,12 @@ export class TabSetComponent implements AfterContentInit {

this.refreshTabWidths();
this.changeDetector.detectChanges();
}));
}));
}

/** Sets the currently selected tab by either its numerical index or `TabComponent` object.
* Passing a value of -1 or a hidden tab will deselect all tabs in the set. */
selectTab(tab: number | TabComponent, shouldEmit = true, scrollIntoView = true): void {
selectTab(tab: number | TabComponent, shouldEmit = true): void {
this._selectedTab = tab;
const activeTab = typeof tab === 'number' ? this._tabs.toArray()[tab] : tab;
if ( tab === -1 || activeTab._hideOverride ) {
Expand All @@ -436,13 +460,14 @@ export class TabSetComponent implements AfterContentInit {
}
} else {
if ( this._routerEnabled ) {
this.router.navigate([activeTab.routerLink], {relativeTo: this.route});
const routeArray = Array.isArray(activeTab.routerLink) ? activeTab.routerLink : [activeTab.routerLink];
this.router.navigate(routeArray, {relativeTo: this.route, queryParams: activeTab.queryParams});
}
this._setActive(activeTab, shouldEmit, scrollIntoView);
this._setActive(activeTab, shouldEmit);
}
}

_setActive(tab: TabComponent, shouldEmit = true, scrollIntoView = true): void {
_setActive(tab: TabComponent, shouldEmit = true): void {
let activeIndex = 0;
this._tabs.toArray().forEach((t, index) => {
t._active = false;
Expand All @@ -455,7 +480,7 @@ export class TabSetComponent implements AfterContentInit {
this._routerDeselected = false;

// For horizontal tabs with arrows overflow, scroll the selected tab into view if it's outside the scroll area
if ( this.overflowStyle === 'arrows' && this.direction === 'horizontal' && this._collapse && scrollIntoView ) {
if ( this.overflowStyle === 'arrows' && this.direction === 'horizontal' && this._collapse ) {
tab.el.nativeElement.scrollIntoView({ block: 'nearest' });
}

Expand All @@ -476,7 +501,10 @@ export class TabSetComponent implements AfterContentInit {
// is triggered
const tabArray = this._tabs.toArray();
if (tabArray[Number(this.defaultTab)]) {
setTimeout(() => this._setActive(tabArray[Number(this.defaultTab)]));
setTimeout(() => {
this._selectedTab = tabArray[Number(this.defaultTab)];
this._setActive(tabArray[Number(this.defaultTab)]);
});
} else {
invalidDefaultTab(this.defaultTab);
}
Expand All @@ -492,37 +520,7 @@ export class TabSetComponent implements AfterContentInit {

if (countUsingRouter === this._tabs.length) {
this._routerEnabled = true;
if (this._defaultTab !== 'none') {
this.defaultToFirstRoute();
}
}
}

private defaultToFirstRoute() {
const foundRoute = this._tabs
.map(tab => tab.routerLink)
.map(routerLink => this.mapRouterLinkToString(routerLink))
.find(routerLink => {
const currentRoute = this.router.url;
return currentRoute === routerLink || currentRoute.indexOf(`${routerLink}/`) > -1;
});

if (foundRoute) {
return;
}

const tabArray = this._tabs.toArray();
if (tabArray[Number(this.defaultTab)]) {
const firstRoute = this.mapRouterLinkToString(tabArray[Number(this.defaultTab)].routerLink);
this.router.navigate([firstRoute], {relativeTo: this.route});
}
}

private mapRouterLinkToString(routerLink: string | string[]): string {
if (routerLink instanceof Array) {
routerLink = routerLink.join('/').replace('//', '/');
}
return routerLink;
}

ngOnDestroy(): void {
Expand Down
2 changes: 1 addition & 1 deletion projects/cashmere/src/lib/tabs/tab/tab.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
*ngIf="routerLink; else tabWithoutRouting"
class="hc-tab-{{ _direction }} hc-text-ellipsis"
[class.hc-tab-tight]="_tight"
[class.active]="_active"
[style.max-width]="maxWidth"
[routerLink]="routerLink"
routerLinkActive="active"
(isActiveChange)="_isActiveChange($event)"
[queryParams]="queryParams"
[routerLinkActiveOptions]="exactRouteMatch ? {exact: true} : {}"
(click)="tabClickHandler($event)"
Expand Down
4 changes: 4 additions & 0 deletions projects/cashmere/src/lib/tabs/tab/tab.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
.hc-tab {
@include hc-tab();

&.hc-tab-active {
@include hc-tab-active();
}

// tabs have an anchor element embedded, but we want the user to focus on the parent tab rather than the achor
a:focus {
@include hc-tab-embedded-anchor-focus();
Expand Down
12 changes: 11 additions & 1 deletion projects/cashmere/src/lib/tabs/tab/tab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {parseBooleanAttribute} from '../../util';
})
export class TabComponent implements AfterContentInit {
@HostBinding('class.hc-tab') _hostClass = true;
@HostBinding('class.hc-tab-active') _active = false;

/** Plain text title of the tab; for HTML support include a `hc-tab-title` element */
@Input()
Expand Down Expand Up @@ -55,6 +56,9 @@ export class TabComponent implements AfterContentInit {
@Output()
_tabHideChange: EventEmitter<Event> = new EventEmitter();

@Output()
_routerActiveChange: EventEmitter<TabComponent> = new EventEmitter();

/** Emits when this tab is selected; use instead of `(click)` for click binding */
@Output()
tabClick: EventEmitter<Event> = new EventEmitter();
Expand All @@ -68,7 +72,6 @@ export class TabComponent implements AfterContentInit {
tabContent: TemplateRef<unknown>;

_direction: string;
_active = false;
_tight = false;
_hidden = false;
_htmlTitle: HcTabTitleComponent;
Expand Down Expand Up @@ -100,6 +103,13 @@ export class TabComponent implements AfterContentInit {
return this.el.nativeElement.scrollWidth;
}

// Listens for changes to routerLinkActive and reports to TabSet
_isActiveChange( state: boolean ): void {
if ( state ) {
this._routerActiveChange.emit(this);
}
}

/** Disable visibility of component from view */
hide(): void {
this._hidden = true;
Expand Down
Loading

0 comments on commit 90deb33

Please sign in to comment.