Skip to content

Commit

Permalink
Merge pull request #783 from hubmapconsortium/create-expansion-panel
Browse files Browse the repository at this point in the history
Create expansion panel
  • Loading branch information
axdanbol authored Oct 23, 2024
2 parents ff67556 + 6626b8a commit 614eaf6
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { render, screen } from '@testing-library/angular';
import { ToggleButtonSizeDirective } from './button-toggle-size.directive';

@Component({
template: `<div hraButtonToggleSize="large" data-testid="dir"></div>`,
imports: [ToggleButtonSizeDirective],
standalone: true,
})
class ButtonToggleComponent {}

describe('ButtonToggleSizeDirective', () => {
it('should apply the styles based on the directive', async () => {
await render(ButtonToggleComponent);

const directive = screen.getByTestId('dir');
const styles = window.getComputedStyle(directive);
const lineHeight = styles.getPropertyValue('--mat-standard-button-toggle-height');
const font = styles.getPropertyValue('font');
expect(font).toBe('var(--sys-label-large)');
expect(lineHeight).toBe('24px');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ export class ToggleButtonSizeDirective {
/** Size of icon button to use */
readonly size = input.required<IconButtonSize>({ alias: 'hraButtonToggleSize' });

/** Gets size of button in rem */
protected readonly buttonSize = computed(() => BUTTON_CONFIG[this.size()]);

/** Gets the font variable for the current button size */
protected readonly fontVar = computed(() => `var(${BUTTON_CONFIG[this.size()].font})`);

Expand Down
3 changes: 3 additions & 0 deletions libs/design-system/expansion-panel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @hra-ui/design-system/expansion-panel

Secondary entry point of `@hra-ui/design-system`. It can be used by importing from `@hra-ui/design-system/expansion-panel`.
5 changes: 5 additions & 0 deletions libs/design-system/expansion-panel/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
1 change: 1 addition & 0 deletions libs/design-system/expansion-panel/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lib/expansion-panel.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { animate, state, style, transition, trigger } from '@angular/animations';

/** Animation for the expansion panel */
export const EXPANSION_PANEL_ANIMATION_TIMING = '225ms cubic-bezier(0.4,0.0,0.2,1)';

/** Animation for Body Expansion */
export const BODY_EXPANSION = trigger('bodyExpansion', [
state('collapsed, void', style({ height: '0px', visibility: 'hidden' })),
state('expanded', style({ height: '*', visibility: '' })),
transition('expanded <=> collapsed, void => collapsed', animate(EXPANSION_PANEL_ANIMATION_TIMING)),
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<cdk-accordion>
<cdk-accordion-item
#accordionItem="cdkAccordionItem"
tabindex="0"
[attr.aria-expanded]="accordionItem.expanded"
[attr.aria-controls]="bodyId"
[expanded]="expanded"
>
<div class="header">
@if (!disabled()) {
<button mat-icon-button data-testid="toggle" (click)="accordionItem.toggle()">
<mat-icon>
{{ accordionItem.expanded ? 'remove' : 'add' }}
</mat-icon>
</button>
}

<span class="title" [attr.id]="titleId">
{{ title() }}
</span>

<span>
<ng-content select="hra-expansion-panel-actions"> </ng-content>
</span>
<div class="filler"></div>
<span>
<ng-content select="hra-expansion-panel-header-content"></ng-content>
</span>
</div>
<div
role="region"
class="content"
[attr.id]="bodyId"
[attr.aria-labelledby]="titleId"
#body
[@bodyExpansion]="accordionItem.expanded ? 'expanded' : 'collapsed'"
(@bodyExpansion.start)="animationStart($event)"
(@bodyExpansion.done)="animationDone($event)"
data-testid="body"
>
<div class="expansion-body">
<ng-content></ng-content>
</div>
</div>
</cdk-accordion-item>
</cdk-accordion>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
:host {
display: block;

.header {
display: flex;
align-items: center;
font: var(--sys-label-large);
color: var(--sys-secondary);
}

.filler {
flex-grow: 1;
}

.content {
font: var(--sys-label-large);
&[style*='visibility: hidden'] * {
visibility: hidden !important;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { AnimationDriver } from '@angular/animations/browser';
import { MockAnimationDriver, MockAnimationPlayer } from '@angular/animations/browser/testing';
import { provideDesignSystem } from '@hra-ui/design-system';
import { render, RenderComponentOptions, screen } from '@testing-library/angular';
import { userEvent } from '@testing-library/user-event';
import { ExpansionPanelComponent } from './expansion-panel.component';

describe('ExpansionPanelComponent', () => {
async function setup(options?: RenderComponentOptions<ExpansionPanelComponent>) {
return render(ExpansionPanelComponent, {
...options,
inputs: {
title: 'Test Title',
...options?.inputs,
},
providers: [
provideDesignSystem(),
{
provide: AnimationDriver,
useClass: MockAnimationDriver,
},
],
});
}

it('should render the expansion panel', async () => {
await setup();
const title = screen.getByText('Test Title');
expect(title).toBeInTheDocument();
});

it('should set inert attribute on body element when animation starts', async () => {
await setup();

const button = screen.getByTestId('toggle');
await userEvent.click(button);

const player = MockAnimationDriver.log.pop() as MockAnimationPlayer;
const element = player.element as HTMLElement;

expect(element).toHaveAttribute('inert');
player.finish();
expect(element).not.toHaveAttribute('inert');
expect(element.style.height).toBe('0px');
expect(element.style.visibility).toBe('hidden');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MatIconModule } from '@angular/material/icon';
import { provideDesignSystem } from '@hra-ui/design-system';
import { ButtonModule } from '@hra-ui/design-system/button';
import { applicationConfig, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular';
import {
ExpansionPanelActionsComponent,
ExpansionPanelComponent,
ExpansionPanelHeaderContentComponent,
} from './expansion-panel.component';

const meta: Meta = {
title: 'ExpansionPanel',
decorators: [
moduleMetadata({
imports: [
MatIconModule,
ButtonModule,
ExpansionPanelActionsComponent,
ExpansionPanelComponent,
ExpansionPanelHeaderContentComponent,
],
}),
applicationConfig({
providers: [provideDesignSystem()],
}),
],
render: (args) => ({
props: args,
template: `
<hra-expansion-panel [title]="'Title'">
<hra-expansion-panel-actions>
<button mat-icon-button>
<mat-icon class="material-symbols-rounded">
more_vert
</mat-icon>
</button>
</hra-expansion-panel-actions>
<hra-expansion-panel-header-content>
Additional Actions
</hra-expansion-panel-header-content>
Actual Content
</hra-expansion-panel>
`,
}),
};
export default meta;
type Story = StoryObj;

export const Default: Story = {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { AnimationEvent } from '@angular/animations';
import { CdkAccordionModule } from '@angular/cdk/accordion';
import { CommonModule } from '@angular/common';
import {
ANIMATION_MODULE_TYPE,
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
inject,
input,
Renderer2,
viewChild,
} from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { IconButtonSizeDirective } from '@hra-ui/design-system/icon-button';
import { BODY_EXPANSION } from './expansion-panel-animations';

/** Counter to keep track of distinct panels */
let idCounter = 0;

/** Expansion panel actions component */
@Component({
selector: 'hra-expansion-panel-actions',
standalone: true,
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExpansionPanelActionsComponent {}

/** Expansion panel header content component */
@Component({
selector: 'hra-expansion-panel-header-content',
standalone: true,
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExpansionPanelHeaderContentComponent {}

/** Expansion panel component */
@Component({
selector: 'hra-expansion-panel',
standalone: true,
imports: [CommonModule, CdkAccordionModule, IconButtonSizeDirective, MatIconButton, MatIconModule],
animations: [BODY_EXPANSION],
templateUrl: './expansion-panel.component.html',
styleUrl: './expansion-panel.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExpansionPanelComponent {
/** Title of the expansion panel */
readonly title = input.required<string>();

/** Flag to check if the body is expanded */
readonly expanded = input(true, { transform: booleanAttribute });

/** Flag to denote panel as disabled */
readonly disabled = input(false, { transform: booleanAttribute });

/** Increments the counter on every declaration */
protected readonly id = idCounter++;

/** Id attribute for title based on current id counter */
protected readonly titleId = `expansion-panel-title-${this.id}`;

/** Id attribute for body based on current id counter */
protected readonly bodyId = `expansion-panel-body-${this.id}`;

/** Instance of renderer */
private readonly renderer = inject(Renderer2);

/** Instance of body element */
private readonly bodyElementRef = viewChild.required<ElementRef<HTMLElement>>('body');

/** Actual body element */
private readonly body = computed(() => this.bodyElementRef().nativeElement);

/** Disable animations based on module type */
private readonly animationsDisabled = inject(ANIMATION_MODULE_TYPE) === 'NoopAnimations';

/** Sets attribute based on event state */
protected animationStart(event: AnimationEvent): void {
if (event.fromState !== 'void' && !this.animationsDisabled) {
this.renderer.setAttribute(this.body(), 'inert', '');
}
}

/** Removes attribute based on event state */
protected animationDone(event: AnimationEvent): void {
if (event.fromState !== 'void' && !this.animationsDisabled) {
this.renderer.removeAttribute(this.body(), 'inert');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import {
ExpansionPanelActionsComponent,
ExpansionPanelComponent,
ExpansionPanelHeaderContentComponent,
} from './expansion-panel.component';

/** Expansion panel module */
@NgModule({
imports: [ExpansionPanelActionsComponent, ExpansionPanelHeaderContentComponent, ExpansionPanelComponent],
exports: [ExpansionPanelActionsComponent, ExpansionPanelHeaderContentComponent, ExpansionPanelComponent],
})
export class ExpansionPanelModule {}
4 changes: 3 additions & 1 deletion libs/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"ngx-scrollbar": "^15.1.0",
"ngx-color-picker": "^17.0.0",
"@angular/router": "18.2.1",
"@angular/cdk": "18.2.1"
"@angular/cdk": "18.2.1",
"@angular/animations": "18.2.1",
"@angular/platform-browser": "18.2.1"
},
"dependencies": {},
"sideEffects": false
Expand Down
2 changes: 2 additions & 0 deletions libs/design-system/src/lib/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import { provideTable } from '@hra-ui/design-system/table';
import { provideSelect } from '@hra-ui/design-system/select';
import { provideInput } from '@hra-ui/design-system/input';
import { provideButtonToggle } from '@hra-ui/design-system/button-toggle';
import { provideAnimations } from '@angular/platform-browser/animations';

/**
* Returns design system providers
*/
export function provideDesignSystem(): EnvironmentProviders {
return makeEnvironmentProviders([
provideHttpClient(),
provideAnimations(),
provideIcons({
fontIcons: {
defaultClasses: ['material-symbols-rounded'],
Expand Down
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@
"@hra-ui/design-system/error-indicator": [
"libs/design-system/error-indicator/src/index.ts"
],
"@hra-ui/design-system/expansion-panel": [
"libs/design-system/expansion-panel/src/index.ts"
],
"@hra-ui/design-system/footer": [
"libs/design-system/footer/src/index.ts"
],
Expand Down

0 comments on commit 614eaf6

Please sign in to comment.