Skip to content

Commit

Permalink
fix: accordion is now a directive
Browse files Browse the repository at this point in the history
  • Loading branch information
elite-benni committed Nov 23, 2023
1 parent 8427360 commit 5fe8b90
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
} from '@spartan-ng/ui-accordion-brain';
import {
HlmAccordionContentDirective,
Expand All @@ -20,7 +20,7 @@ import {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
HlmAccordionDirective,
HlmAccordionItemDirective,
HlmAccordionTriggerDirective,
Expand All @@ -30,28 +30,28 @@ import {
template: `
<brn-accordion hlm>
<brn-accordion-item hlm>
<brn-accordion-trigger hlm>
<button hlmAccordionTrigger>
<span>Is it accessible?</span>
<hlm-accordion-icon />
</brn-accordion-trigger>
</button>
<brn-accordion-content hlm>Yes. It adheres to the WAI-ARIA design pattern.</brn-accordion-content>
</brn-accordion-item>
<brn-accordion-item hlm>
<brn-accordion-trigger hlm>
<button hlmAccordionTrigger>
<span>Is it styled</span>
<hlm-accordion-icon />
</brn-accordion-trigger>
</button>
<brn-accordion-content hlm>
Yes. It comes with default styles that match the other components' aesthetics.
</brn-accordion-content>
</brn-accordion-item>
<brn-accordion-item hlm>
<brn-accordion-trigger hlm>
<button hlmAccordionTrigger>
<span>Is it animated?</span>
<hlm-accordion-icon />
</brn-accordion-trigger>
</button>
<brn-accordion-content hlm>
Yes. It's animated by default, but you can disable it if you prefer.
</brn-accordion-content>
Expand All @@ -67,7 +67,7 @@ import {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
} from '@spartan-ng/ui-accordion-brain';
import {
HlmAccordionContentDirective,
Expand All @@ -84,7 +84,7 @@ import {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
HlmAccordionDirective,
HlmAccordionItemDirective,
HlmAccordionTriggerDirective,
Expand Down Expand Up @@ -131,7 +131,7 @@ import {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
} from '@spartan-ng/ui-accordion-brain';
import {
HlmAccordionContentDirective,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
} from '@spartan-ng/ui-accordion-brain';
import {
HlmAccordionContentDirective,
Expand Down Expand Up @@ -49,7 +49,7 @@ export const routeMeta: RouteMeta = {
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
HlmAccordionContentDirective,
HlmAccordionDirective,
HlmAccordionIconComponent,
Expand Down
16 changes: 8 additions & 8 deletions libs/ui/accordion/accordion.stories.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { BrnAccordionComponent, BrnAccordionImports } from './brain/src';
import { HlmAccordionImports } from './helm/src';
import { HlmAccordionImports, HlmAccordionTriggerDirective } from './helm/src';

const meta: Meta<BrnAccordionComponent> = {
title: 'Accordion',
component: BrnAccordionComponent,
decorators: [
moduleMetadata({
imports: [BrnAccordionImports, HlmAccordionImports],
imports: [BrnAccordionImports, HlmAccordionImports, HlmAccordionTriggerDirective],
}),
],
};
Expand All @@ -21,30 +21,30 @@ export const Default: Story = {
template: `
<brn-accordion hlm>
<brn-accordion-item hlm>
<brn-accordion-trigger hlm>
<button hlmAccordionTrigger>
<span>What is SPARTAN</span>
<hlm-accordion-icon />
</brn-accordion-trigger>
</button>
<brn-accordion-content hlm>
It is a collection of full-stack technologies that provide end-to-end type-safety.
</brn-accordion-content>
</brn-accordion-item>
<brn-accordion-item hlm>
<brn-accordion-trigger hlm>
<button hlmAccordionTrigger>
<span>What is SPARTAN Brain</span>
<hlm-accordion-icon />
</brn-accordion-trigger>
</button>
<brn-accordion-content hlm>
A collection of unstyled UI primitives that provide accessibility out of the box.
</brn-accordion-content>
</brn-accordion-item>
<brn-accordion-item hlm>
<brn-accordion-trigger hlm>
<button hlmAccordionTrigger>
<span>What is SPARTAN Helm</span>
<hlm-accordion-icon />
</brn-accordion-trigger>
</button>
<brn-accordion-content hlm>
Directives, sometimes additional components, that provide shadcn like styles for the Angular ecosystem.
</brn-accordion-content>
Expand Down
6 changes: 3 additions & 3 deletions libs/ui/accordion/brain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ import { NgModule } from '@angular/core';

import { BrnAccordionContentComponent } from './lib/brn-accordion-content.component';
import { BrnAccordionItemComponent } from './lib/brn-accordion-item.component';
import { BrnAccordionTriggerComponent } from './lib/brn-accordion-trigger.component';
import { BrnAccordionTriggerDirective } from './lib/brn-accordion-trigger.directive';
import { BrnAccordionComponent } from './lib/brn-accordion.component';

export * from './lib/brn-accordion-content.component';
export * from './lib/brn-accordion-item.component';
export * from './lib/brn-accordion-trigger.component';
export * from './lib/brn-accordion-trigger.directive';
export * from './lib/brn-accordion.component';

export const BrnAccordionImports = [
BrnAccordionComponent,
BrnAccordionContentComponent,
BrnAccordionItemComponent,
BrnAccordionTriggerComponent,
BrnAccordionTriggerDirective,
] as const;

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Component, ElementRef, inject, signal } from '@angular/core';
import { CustomElementClassSettable, provideCustomClassSettableExisting } from '@spartan-ng/ui-core';
import { AfterContentInit, Directive, ElementRef, inject } from '@angular/core';
import { rxHostPressedListener } from '@spartan-ng/ui-core';
import { BrnAccordionItemComponent } from './brn-accordion-item.component';
import { BrnAccordionComponent } from './brn-accordion.component';

@Component({
selector: 'brn-accordion-trigger',
@Directive({
selector: '[brnAccordionTrigger]',
standalone: true,
providers: [provideCustomClassSettableExisting(() => BrnAccordionTriggerComponent)],
host: {
'[attr.data-state]': 'state()',
'[attr.aria-expanded]': 'state() === "open"',
Expand All @@ -15,24 +14,17 @@ import { BrnAccordionComponent } from './brn-accordion.component';
'aria-level': '3',
'[id]': 'id',
},
template: `
<button [class]="btnClass()" [attr.data-state]="state()" (click)="toggleAccordionItem()">
<ng-content />
</button>
`,
})
export class BrnAccordionTriggerComponent implements CustomElementClassSettable {
export class BrnAccordionTriggerDirective implements AfterContentInit {
private _accordion = inject(BrnAccordionComponent);
private _item = inject(BrnAccordionItemComponent);
private _elementRef = inject(ElementRef);
private _HostPressedListener = rxHostPressedListener();

public state = this._item.state;
public id = 'brn-accordion-trigger-' + this._item.id;
public ariaControls = 'brn-accordion-content-' + this._item.id;

private readonly _btnClass = signal('');
public btnClass = this._btnClass.asReadonly();

constructor() {
if (!this._accordion) {
throw Error('Accordion trigger can only be used inside an Accordion. Add brnAccordion to ancestor.');
Expand All @@ -41,10 +33,12 @@ export class BrnAccordionTriggerComponent implements CustomElementClassSettable
if (!this._item) {
throw Error('Accordion trigger can only be used inside an AccordionItem. Add brnAccordionItem to parent.');
}
this._HostPressedListener.subscribe(() => {
this.toggleAccordionItem();
});
}

public setClassToCustomElement(classes: string) {
this._btnClass.set(classes);
ngAfterContentInit(): void {
console.log('BrnAccordionTriggerDirective.ngAfterContentInit');
}

public focus() {
Expand Down
28 changes: 23 additions & 5 deletions libs/ui/accordion/brain/src/lib/brn-accordion.component.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FocusKeyManager } from '@angular/cdk/a11y';
import { AfterContentInit, Component, computed, ContentChildren, Input, QueryList, signal } from '@angular/core';
import { rxHostListener } from '@spartan-ng/ui-core';
import { BrnAccordionTriggerComponent } from './brn-accordion-trigger.component';
import { BrnAccordionTriggerDirective } from './brn-accordion-trigger.directive';

@Component({
selector: 'brn-accordion',
Expand All @@ -25,17 +25,34 @@ export class BrnAccordionComponent implements AfterContentInit {
private readonly _openItemIds = signal<number[]>([]);
public openItemIds = this._openItemIds.asReadonly();
public state = computed(() => (this._openItemIds().length > 0 ? 'open' : 'closed'));
private _keyManager?: FocusKeyManager<BrnAccordionTriggerComponent>;
private _keyManager?: FocusKeyManager<BrnAccordionTriggerDirective>;
private _keyDownListener = rxHostListener('keydown');

@ContentChildren(BrnAccordionTriggerComponent, { descendants: true })
public triggers?: QueryList<BrnAccordionTriggerComponent>;
constructor() {
addEventListener('keydown', (event) => {
// if one of the triggers is focused, prevent default on certain keys
for (const trigger of this.triggers?.toArray() ?? []) {
if (trigger.id === document.activeElement?.id) {
if ('key' in event) {
const keys = ['ArrowUp', 'ArrowDown', 'PageDown', 'PageUp', 'Home', 'End', ' ', 'Enter'];
if (keys.includes(event.key as string)) {
event.preventDefault();
}
}
return;
}
}
});
}

@ContentChildren(BrnAccordionTriggerDirective, { descendants: true })
public triggers?: QueryList<BrnAccordionTriggerDirective>;

public ngAfterContentInit() {
if (!this.triggers) {
return;
}
this._keyManager = new FocusKeyManager<BrnAccordionTriggerComponent>(this.triggers)
this._keyManager = new FocusKeyManager<BrnAccordionTriggerDirective>(this.triggers)
.withHorizontalOrientation(this.dir)
.withHomeAndEnd()
.withPageUpDown()
Expand All @@ -50,6 +67,7 @@ export class BrnAccordionComponent implements AfterContentInit {
}

public toggleItem(id: number) {
this._keyManager?.setActiveItem(id);
if (this._openItemIds().includes(id)) {
this._openItemIds.update((ids) => ids.filter((openId) => id !== openId));
return;
Expand Down
20 changes: 5 additions & 15 deletions libs/ui/accordion/helm/src/lib/hlm-accordion-trigger.directive.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,25 @@
import { Directive, Input, computed, signal } from '@angular/core';
import { hlm, injectCustomClassSettable } from '@spartan-ng/ui-core';
import { BrnAccordionTriggerDirective } from '@spartan-ng/ui-accordion-brain';
import { hlm } from '@spartan-ng/ui-core';
import { ClassValue } from 'clsx';

@Directive({
selector: '[hlmAccordionTrigger],brn-accordion-trigger[hlm]',
selector: '[hlmAccordionTrigger]',
standalone: true,
host: {
'[style.--tw-ring-offset-shadow]': '"0 0 #000"',
'[class]': '_computedClass()',
},
hostDirectives: [BrnAccordionTriggerDirective],
})
export class HlmAccordionTriggerDirective {
private _host = injectCustomClassSettable({ optional: true });

constructor() {
this._host?.setClassToCustomElement(this._generateClass());
}

private readonly _userCls = signal<ClassValue>('');
@Input()
set class(inputs: ClassValue) {
this._userCls.set(inputs);
// cannot set in effect because it sets a signal
if (this._host) {
this._host.setClassToCustomElement(this._generateClass());
}
}

protected _computedClass = computed(() => {
return !this._host ? this._generateClass() : '';
});
protected _computedClass = computed(() => this._generateClass());
private _generateClass() {
return hlm(
'w-full focus-visible:outline-none text-sm focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-2 flex flex-1 items-center justify-between py-4 px-0.5 font-medium underline-offset-4 hover:underline [&[data-state=open]>hlm-accordion-icon]:rotate-180',
Expand Down

0 comments on commit 5fe8b90

Please sign in to comment.