diff --git a/change/@ni-ok-angular-4d6dfd1a-0b9b-4e15-b821-bac9233fa617.json b/change/@ni-ok-angular-4d6dfd1a-0b9b-4e15-b821-bac9233fa617.json new file mode 100644 index 0000000000..9fb1ba4941 --- /dev/null +++ b/change/@ni-ok-angular-4d6dfd1a-0b9b-4e15-b821-bac9233fa617.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Add FV Angular wrappers for ok components", + "packageName": "@ni/ok-angular", + "email": "1458528+fredvisser@users.noreply.github.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@ni-ok-components-f79eb8e1-0a6a-42ca-9be6-b282c8a59251.json b/change/@ni-ok-components-f79eb8e1-0a6a-42ca-9be6-b282c8a59251.json new file mode 100644 index 0000000000..bfdd988e95 --- /dev/null +++ b/change/@ni-ok-components-f79eb8e1-0a6a-42ca-9be6-b282c8a59251.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "component-review skill updates", + "packageName": "@ni/ok-components", + "email": "1458528+fredvisser@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-accordion-item-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-accordion-item-section.component.ts index 5a24f7059d..a04699fc60 100644 --- a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-accordion-item-section.component.ts +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-accordion-item-section.component.ts @@ -4,10 +4,10 @@ import { Component } from '@angular/core'; selector: 'example-fv-accordion-item-section', template: ` - + Calibration assets can expose operator-facing status, location, and ownership details. - + This section starts collapsed to show the default interaction state. diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-card-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-card-section.component.ts new file mode 100644 index 0000000000..9e25ee40a0 --- /dev/null +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-card-section.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'example-fv-card-section', + template: ` + + + Approved + Updated now + 4 alerts + + + `, + styles: [` + ok-fv-card { max-width: 300px; } + `], + standalone: false +}) +export class FvCardSectionComponent {} \ No newline at end of file diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-chip-selector-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-chip-selector-section.component.ts new file mode 100644 index 0000000000..a3d7331e03 --- /dev/null +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-chip-selector-section.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'example-fv-chip-selector-section', + template: ` + + + + `, + standalone: false +}) +export class FvChipSelectorSectionComponent {} \ No newline at end of file diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-context-help-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-context-help-section.component.ts new file mode 100644 index 0000000000..4a47243672 --- /dev/null +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-context-help-section.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'example-fv-context-help-section', + template: ` + +
+ Support code + +
+
+ `, + standalone: false +}) +export class FvContextHelpSectionComponent {} \ No newline at end of file diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.component.ts index e8ba70eb13..838077c74f 100644 --- a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.component.ts +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.component.ts @@ -4,7 +4,13 @@ import { Component } from '@angular/core'; selector: 'example-fv-section', template: ` + + + + + + `, standalone: false }) diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.module.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.module.ts index 29784c56bb..1fa48b3dba 100644 --- a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.module.ts +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-section.module.ts @@ -1,14 +1,51 @@ import { NgModule } from '@angular/core'; +import { NimbleMenuItemModule, NimbleMenuModule } from '@ni/nimble-angular'; import { OkFvAccordionItemModule } from '@ni/ok-angular/fv/accordion-item'; +import { OkFvCardModule } from '@ni/ok-angular/fv/card'; +import { OkFvChipSelectorModule } from '@ni/ok-angular/fv/chip-selector'; +import { OkFvContextHelpModule } from '@ni/ok-angular/fv/context-help'; import { OkFvSearchInputModule } from '@ni/ok-angular/fv/search-input'; +import { OkFvSplitButtonAnchorModule } from '@ni/ok-angular/fv/split-button-anchor'; +import { OkFvSplitButtonModule } from '@ni/ok-angular/fv/split-button'; +import { OkFvSummaryPanelModule } from '@ni/ok-angular/fv/summary-panel'; +import { OkFvSummaryPanelTileModule } from '@ni/ok-angular/fv/summary-panel-tile'; import { FvAccordionItemSectionComponent } from './fv-accordion-item-section.component'; +import { FvCardSectionComponent } from './fv-card-section.component'; +import { FvChipSelectorSectionComponent } from './fv-chip-selector-section.component'; +import { FvContextHelpSectionComponent } from './fv-context-help-section.component'; import { FvSearchInputSectionComponent } from './fv-search-input-section.component'; import { FvSectionComponent } from './fv-section.component'; +import { FvSplitButtonAnchorSectionComponent } from './fv-split-button-anchor-section.component'; +import { FvSplitButtonSectionComponent } from './fv-split-button-section.component'; +import { FvSummaryPanelSectionComponent } from './fv-summary-panel-section.component'; import { SubContainerModule } from '../sub-container/sub-container.module'; @NgModule({ - declarations: [FvSectionComponent, FvAccordionItemSectionComponent, FvSearchInputSectionComponent], - imports: [OkFvAccordionItemModule, OkFvSearchInputModule, SubContainerModule], + declarations: [ + FvSectionComponent, + FvAccordionItemSectionComponent, + FvCardSectionComponent, + FvChipSelectorSectionComponent, + FvContextHelpSectionComponent, + FvSearchInputSectionComponent, + FvSplitButtonSectionComponent, + FvSplitButtonAnchorSectionComponent, + FvSummaryPanelSectionComponent + ], + imports: [ + NimbleMenuModule, + NimbleMenuItemModule, + OkFvAccordionItemModule, + OkFvCardModule, + OkFvChipSelectorModule, + OkFvContextHelpModule, + OkFvSearchInputModule, + OkFvSplitButtonModule, + OkFvSplitButtonAnchorModule, + OkFvSummaryPanelModule, + OkFvSummaryPanelTileModule, + SubContainerModule + ], exports: [FvSectionComponent] }) export class FvSectionModule { } diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-split-button-anchor-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-split-button-anchor-section.component.ts new file mode 100644 index 0000000000..a8960369b8 --- /dev/null +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-split-button-anchor-section.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'example-fv-split-button-anchor-section', + template: ` + + + + Open item + Open details + Copy link + + + + `, + standalone: false +}) +export class FvSplitButtonAnchorSectionComponent {} \ No newline at end of file diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-split-button-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-split-button-section.component.ts new file mode 100644 index 0000000000..5dd41d13e5 --- /dev/null +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-split-button-section.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'example-fv-split-button-section', + template: ` + + + + Open item + Create copy + Archive selection + + + + `, + standalone: false +}) +export class FvSplitButtonSectionComponent {} \ No newline at end of file diff --git a/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-summary-panel-section.component.ts b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-summary-panel-section.component.ts new file mode 100644 index 0000000000..2f2ee495c8 --- /dev/null +++ b/packages/angular-workspace/example-client-app/src/app/customapp/fv/fv-summary-panel-section.component.ts @@ -0,0 +1,17 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'example-fv-summary-panel-section', + template: ` + + + + + + + + + `, + standalone: false +}) +export class FvSummaryPanelSectionComponent {} \ No newline at end of file diff --git a/packages/angular-workspace/ok-angular/fv/card/ng-package.json b/packages/angular-workspace/ok-angular/fv/card/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/card/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/card/ok-fv-card.directive.ts b/packages/angular-workspace/ok-angular/fv/card/ok-fv-card.directive.ts new file mode 100644 index 0000000000..4040343487 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/card/ok-fv-card.directive.ts @@ -0,0 +1,86 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvCard } from '@ni/ok-components/dist/esm/fv/card'; +import { fvCardTag } from '@ni/ok-components/dist/esm/fv/card'; +import { FvCardAppearance, FvCardInteractionMode } from '@ni/ok-components/dist/esm/fv/card/types'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvCard }; +export { fvCardTag }; +export { FvCardAppearance, FvCardInteractionMode }; + +/** + * Directive to provide Angular integration for the card. + */ +@Directive({ + selector: 'ok-fv-card', + standalone: false +}) +export class OkFvCardDirective { + public get title(): string { + return this.elementRef.nativeElement.title; + } + + @Input('card-title') + public set title(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'title', value); + } + + public get subtitle(): string { + return this.elementRef.nativeElement.subtitle; + } + + @Input() + public set subtitle(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'subtitle', value); + } + + public get description(): string { + return this.elementRef.nativeElement.description; + } + + @Input() + public set description(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'description', value); + } + + public get appearance(): FvCardAppearance { + return this.elementRef.nativeElement.appearance; + } + + @Input() + public set appearance(value: FvCardAppearance) { + this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value); + } + + public get interactionMode(): FvCardInteractionMode { + return this.elementRef.nativeElement.interactionMode; + } + + @Input('interaction-mode') + public set interactionMode(value: FvCardInteractionMode) { + this.renderer.setProperty(this.elementRef.nativeElement, 'interactionMode', value); + } + + public get disabled(): boolean { + return this.elementRef.nativeElement.disabled; + } + + @Input() + public set disabled(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', toBooleanProperty(value)); + } + + public get initials(): string { + return this.elementRef.nativeElement.initials; + } + + @Input() + public set initials(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'initials', value); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/card/ok-fv-card.module.ts b/packages/angular-workspace/ok-angular/fv/card/ok-fv-card.module.ts new file mode 100644 index 0000000000..a668f65871 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/card/ok-fv-card.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvCardDirective } from './ok-fv-card.directive'; + +import '@ni/ok-components/dist/esm/fv/card'; + +@NgModule({ + declarations: [OkFvCardDirective], + imports: [CommonModule], + exports: [OkFvCardDirective] +}) +export class OkFvCardModule { } diff --git a/packages/angular-workspace/ok-angular/fv/card/public-api.ts b/packages/angular-workspace/ok-angular/fv/card/public-api.ts new file mode 100644 index 0000000000..65cc037f53 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/card/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-card.directive'; +export * from './ok-fv-card.module'; diff --git a/packages/angular-workspace/ok-angular/fv/card/tests/ok-fv-card.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/card/tests/ok-fv-card.directive.spec.ts new file mode 100644 index 0000000000..c6c0943fea --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/card/tests/ok-fv-card.directive.spec.ts @@ -0,0 +1,272 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvCard, FvCardAppearance, FvCardInteractionMode, OkFvCardDirective } from '../ok-fv-card.directive'; +import { OkFvCardModule } from '../ok-fv-card.module'; + +describe('Ok fv card', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvCardModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-card')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('card', { read: OkFvCardDirective }) public directive: OkFvCardDirective; + @ViewChild('card', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvCardDirective; + let nativeElement: FvCard; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvCardModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for title', () => { + expect(directive.title).toBe(''); + expect(nativeElement.title).toBe(''); + }); + + it('has expected defaults for subtitle', () => { + expect(directive.subtitle).toBe(''); + expect(nativeElement.subtitle).toBe(''); + }); + + it('has expected defaults for description', () => { + expect(directive.description).toBe(''); + expect(nativeElement.description).toBe(''); + }); + + it('has expected defaults for appearance', () => { + expect(directive.appearance).toBe(FvCardAppearance.outline); + expect(nativeElement.appearance).toBe(FvCardAppearance.outline); + }); + + it('has expected defaults for interactionMode', () => { + expect(directive.interactionMode).toBe(FvCardInteractionMode.static); + expect(nativeElement.interactionMode).toBe(FvCardInteractionMode.static); + }); + + it('has expected defaults for disabled', () => { + expect(directive.disabled).toBe(false); + expect(nativeElement.disabled).toBe(false); + }); + + it('has expected defaults for initials', () => { + expect(directive.initials).toBe(''); + expect(nativeElement.initials).toBe(''); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('card', { read: OkFvCardDirective }) public directive: OkFvCardDirective; + @ViewChild('card', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvCardDirective; + let nativeElement: FvCard; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvCardModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for title', () => { + expect(directive.title).toBe('Device health'); + expect(nativeElement.title).toBe('Device health'); + }); + + it('will use template string values for subtitle', () => { + expect(directive.subtitle).toBe('Cell A'); + expect(nativeElement.subtitle).toBe('Cell A'); + }); + + it('will use template string values for description', () => { + expect(directive.description).toBe('Track operator-facing status and alerts.'); + expect(nativeElement.description).toBe('Track operator-facing status and alerts.'); + }); + + it('will use template string values for appearance', () => { + expect(directive.appearance).toBe(FvCardAppearance.block); + expect(nativeElement.appearance).toBe(FvCardAppearance.block); + }); + + it('will use template string values for interactionMode', () => { + expect(directive.interactionMode).toBe(FvCardInteractionMode.card); + expect(nativeElement.interactionMode).toBe(FvCardInteractionMode.card); + }); + + it('will use template string values for disabled', () => { + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('will use template string values for initials', () => { + expect(directive.initials).toBe('DH'); + expect(nativeElement.initials).toBe('DH'); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('card', { read: OkFvCardDirective }) public directive: OkFvCardDirective; + @ViewChild('card', { read: ElementRef }) public elementRef: ElementRef; + public title = 'Device health'; + public subtitle = 'Cell A'; + public description = 'Track operator-facing status and alerts.'; + public appearance: FvCardAppearance = FvCardAppearance.outline; + public interactionMode: FvCardInteractionMode = FvCardInteractionMode.static; + public disabled = false; + public initials = 'DH'; + } + + let fixture: ComponentFixture; + let directive: OkFvCardDirective; + let nativeElement: FvCard; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvCardModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for title', () => { + expect(directive.title).toBe('Device health'); + expect(nativeElement.title).toBe('Device health'); + + fixture.componentInstance.title = 'Operator queue'; + fixture.detectChanges(); + + expect(directive.title).toBe('Operator queue'); + expect(nativeElement.title).toBe('Operator queue'); + }); + + it('can be configured with property binding for subtitle', () => { + expect(directive.subtitle).toBe('Cell A'); + expect(nativeElement.subtitle).toBe('Cell A'); + + fixture.componentInstance.subtitle = 'Cell B'; + fixture.detectChanges(); + + expect(directive.subtitle).toBe('Cell B'); + expect(nativeElement.subtitle).toBe('Cell B'); + }); + + it('can be configured with property binding for description', () => { + expect(directive.description).toBe('Track operator-facing status and alerts.'); + expect(nativeElement.description).toBe('Track operator-facing status and alerts.'); + + fixture.componentInstance.description = 'Review open work items for the next shift.'; + fixture.detectChanges(); + + expect(directive.description).toBe('Review open work items for the next shift.'); + expect(nativeElement.description).toBe('Review open work items for the next shift.'); + }); + + it('can be configured with property binding for appearance', () => { + expect(directive.appearance).toBe(FvCardAppearance.outline); + expect(nativeElement.appearance).toBe(FvCardAppearance.outline); + + fixture.componentInstance.appearance = FvCardAppearance.block; + fixture.detectChanges(); + + expect(directive.appearance).toBe(FvCardAppearance.block); + expect(nativeElement.appearance).toBe(FvCardAppearance.block); + }); + + it('can be configured with property binding for interactionMode', () => { + expect(directive.interactionMode).toBe(FvCardInteractionMode.static); + expect(nativeElement.interactionMode).toBe(FvCardInteractionMode.static); + + fixture.componentInstance.interactionMode = FvCardInteractionMode.card; + fixture.detectChanges(); + + expect(directive.interactionMode).toBe(FvCardInteractionMode.card); + expect(nativeElement.interactionMode).toBe(FvCardInteractionMode.card); + }); + + it('can be configured with property binding for disabled', () => { + expect(directive.disabled).toBeFalse(); + expect(nativeElement.disabled).toBeFalse(); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('can be configured with property binding for initials', () => { + expect(directive.initials).toBe('DH'); + expect(nativeElement.initials).toBe('DH'); + + fixture.componentInstance.initials = 'OP'; + fixture.detectChanges(); + + expect(directive.initials).toBe('OP'); + expect(nativeElement.initials).toBe('OP'); + }); + }); +}); diff --git a/packages/angular-workspace/ok-angular/fv/chip-selector/ng-package.json b/packages/angular-workspace/ok-angular/fv/chip-selector/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/chip-selector/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/chip-selector/ok-fv-chip-selector.directive.ts b/packages/angular-workspace/ok-angular/fv/chip-selector/ok-fv-chip-selector.directive.ts new file mode 100644 index 0000000000..5d72dee286 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/chip-selector/ok-fv-chip-selector.directive.ts @@ -0,0 +1,84 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvChipSelector } from '@ni/ok-components/dist/esm/fv/chip-selector'; +import { fvChipSelectorTag } from '@ni/ok-components/dist/esm/fv/chip-selector'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvChipSelector }; +export { fvChipSelectorTag }; + +/** + * Directive to provide Angular integration for the chip selector. + */ +@Directive({ + selector: 'ok-fv-chip-selector', + standalone: false +}) +export class OkFvChipSelectorDirective { + public get disabled(): boolean { + return this.elementRef.nativeElement.disabled; + } + + @Input() + public set disabled(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', toBooleanProperty(value)); + } + + public get open(): boolean { + return this.elementRef.nativeElement.open; + } + + @Input() + public set open(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'open', toBooleanProperty(value)); + } + + public get label(): string { + return this.elementRef.nativeElement.label; + } + + @Input() + public set label(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'label', value); + } + + public get selectedValues(): string { + return this.elementRef.nativeElement.selectedValues; + } + + @Input('selected-values') + public set selectedValues(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'selectedValues', value); + } + + public get options(): string { + return this.elementRef.nativeElement.options; + } + + @Input() + public set options(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'options', value); + } + + public get placeholder(): string { + return this.elementRef.nativeElement.placeholder; + } + + @Input() + public set placeholder(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'placeholder', value); + } + + public get allowCustomValues(): boolean { + return this.elementRef.nativeElement.allowCustomValues; + } + + @Input('allow-custom-values') + public set allowCustomValues(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'allowCustomValues', toBooleanProperty(value)); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/chip-selector/ok-fv-chip-selector.module.ts b/packages/angular-workspace/ok-angular/fv/chip-selector/ok-fv-chip-selector.module.ts new file mode 100644 index 0000000000..f0ad848df0 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/chip-selector/ok-fv-chip-selector.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvChipSelectorDirective } from './ok-fv-chip-selector.directive'; + +import '@ni/ok-components/dist/esm/fv/chip-selector'; + +@NgModule({ + declarations: [OkFvChipSelectorDirective], + imports: [CommonModule], + exports: [OkFvChipSelectorDirective] +}) +export class OkFvChipSelectorModule { } diff --git a/packages/angular-workspace/ok-angular/fv/chip-selector/public-api.ts b/packages/angular-workspace/ok-angular/fv/chip-selector/public-api.ts new file mode 100644 index 0000000000..92d5a5fdea --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/chip-selector/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-chip-selector.directive'; +export * from './ok-fv-chip-selector.module'; diff --git a/packages/angular-workspace/ok-angular/fv/chip-selector/tests/ok-fv-chip-selector.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/chip-selector/tests/ok-fv-chip-selector.directive.spec.ts new file mode 100644 index 0000000000..95f6aef919 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/chip-selector/tests/ok-fv-chip-selector.directive.spec.ts @@ -0,0 +1,266 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvChipSelector, OkFvChipSelectorDirective } from '../ok-fv-chip-selector.directive'; +import { OkFvChipSelectorModule } from '../ok-fv-chip-selector.module'; + +describe('Ok fv chip selector', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvChipSelectorModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-chip-selector')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('chipSelector', { read: OkFvChipSelectorDirective }) public directive: OkFvChipSelectorDirective; + @ViewChild('chipSelector', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvChipSelectorDirective; + let nativeElement: FvChipSelector; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvChipSelectorModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for disabled', () => { + expect(directive.disabled).toBe(false); + expect(nativeElement.disabled).toBe(false); + }); + + it('has expected defaults for open', () => { + expect(directive.open).toBe(false); + expect(nativeElement.open).toBe(false); + }); + + it('has expected defaults for label', () => { + expect(directive.label).toBe(''); + expect(nativeElement.label).toBe(''); + }); + + it('has expected defaults for selectedValues', () => { + expect(directive.selectedValues).toBe(''); + expect(nativeElement.selectedValues).toBe(''); + }); + + it('has expected defaults for options', () => { + expect(directive.options).toBe(''); + expect(nativeElement.options).toBe(''); + }); + + it('has expected defaults for placeholder', () => { + expect(directive.placeholder).toBe('Select values'); + expect(nativeElement.placeholder).toBe('Select values'); + }); + + it('has expected defaults for allowCustomValues', () => { + expect(directive.allowCustomValues).toBe(false); + expect(nativeElement.allowCustomValues).toBe(false); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('chipSelector', { read: OkFvChipSelectorDirective }) public directive: OkFvChipSelectorDirective; + @ViewChild('chipSelector', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvChipSelectorDirective; + let nativeElement: FvChipSelector; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvChipSelectorModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for disabled', () => { + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('will use template string values for label', () => { + expect(directive.label).toBe('Selected assets'); + expect(nativeElement.label).toBe('Selected assets'); + }); + + it('will use template string values for selectedValues', () => { + expect(directive.selectedValues).toBe('PXI-1'); + expect(nativeElement.selectedValues).toBe('PXI-1'); + }); + + it('will use template string values for options', () => { + expect(directive.options).toBe('PXI-1,DAQ-1'); + expect(nativeElement.options).toBe('PXI-1,DAQ-1'); + }); + + it('will use template string values for placeholder', () => { + expect(directive.placeholder).toBe('Select assets'); + expect(nativeElement.placeholder).toBe('Select assets'); + }); + + it('will use template string values for allowCustomValues', () => { + expect(directive.allowCustomValues).toBeTrue(); + expect(nativeElement.allowCustomValues).toBeTrue(); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('chipSelector', { read: OkFvChipSelectorDirective }) public directive: OkFvChipSelectorDirective; + @ViewChild('chipSelector', { read: ElementRef }) public elementRef: ElementRef; + public disabled = false; + public open = false; + public label = 'Selected assets'; + public selectedValues = 'PXI-1'; + public options = 'PXI-1,DAQ-1'; + public placeholder = 'Select assets'; + public allowCustomValues = false; + } + + let fixture: ComponentFixture; + let directive: OkFvChipSelectorDirective; + let nativeElement: FvChipSelector; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvChipSelectorModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for disabled', () => { + expect(directive.disabled).toBeFalse(); + expect(nativeElement.disabled).toBeFalse(); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('can be configured with property binding for open', () => { + expect(directive.open).toBeFalse(); + expect(nativeElement.open).toBeFalse(); + + fixture.componentInstance.open = true; + fixture.detectChanges(); + + expect(directive.open).toBeTrue(); + expect(nativeElement.open).toBeTrue(); + }); + + it('can be configured with property binding for label', () => { + expect(directive.label).toBe('Selected assets'); + expect(nativeElement.label).toBe('Selected assets'); + + fixture.componentInstance.label = 'Selected systems'; + fixture.detectChanges(); + + expect(directive.label).toBe('Selected systems'); + expect(nativeElement.label).toBe('Selected systems'); + }); + + it('can be configured with property binding for selectedValues', () => { + expect(directive.selectedValues).toBe('PXI-1'); + expect(nativeElement.selectedValues).toBe('PXI-1'); + + fixture.componentInstance.selectedValues = 'DAQ-1,PXI-1'; + fixture.detectChanges(); + + expect(directive.selectedValues).toBe('DAQ-1,PXI-1'); + expect(nativeElement.selectedValues).toBe('DAQ-1,PXI-1'); + }); + + it('can be configured with property binding for options', () => { + expect(directive.options).toBe('PXI-1,DAQ-1'); + expect(nativeElement.options).toBe('PXI-1,DAQ-1'); + + fixture.componentInstance.options = 'PXI-1,DAQ-1,DMM-1'; + fixture.detectChanges(); + + expect(directive.options).toBe('PXI-1,DAQ-1,DMM-1'); + expect(nativeElement.options).toBe('PXI-1,DAQ-1,DMM-1'); + }); + + it('can be configured with property binding for placeholder', () => { + expect(directive.placeholder).toBe('Select assets'); + expect(nativeElement.placeholder).toBe('Select assets'); + + fixture.componentInstance.placeholder = 'Choose assets'; + fixture.detectChanges(); + + expect(directive.placeholder).toBe('Choose assets'); + expect(nativeElement.placeholder).toBe('Choose assets'); + }); + + it('can be configured with property binding for allowCustomValues', () => { + expect(directive.allowCustomValues).toBeFalse(); + expect(nativeElement.allowCustomValues).toBeFalse(); + + fixture.componentInstance.allowCustomValues = true; + fixture.detectChanges(); + + expect(directive.allowCustomValues).toBeTrue(); + expect(nativeElement.allowCustomValues).toBeTrue(); + }); + }); +}); diff --git a/packages/angular-workspace/ok-angular/fv/context-help/ng-package.json b/packages/angular-workspace/ok-angular/fv/context-help/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/context-help/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/context-help/ok-fv-context-help.directive.ts b/packages/angular-workspace/ok-angular/fv/context-help/ok-fv-context-help.directive.ts new file mode 100644 index 0000000000..4b02f0d003 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/context-help/ok-fv-context-help.directive.ts @@ -0,0 +1,59 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvContextHelp } from '@ni/ok-components/dist/esm/fv/context-help'; +import { fvContextHelpTag } from '@ni/ok-components/dist/esm/fv/context-help'; +import type { FvContextHelpSeverity } from '@ni/ok-components/dist/esm/fv/context-help/types'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvContextHelp }; +export { fvContextHelpTag }; +export type { FvContextHelpSeverity }; + +/** + * Directive to provide Angular integration for the context help. + */ +@Directive({ + selector: 'ok-fv-context-help', + standalone: false +}) +export class OkFvContextHelpDirective { + public get text(): string { + return this.elementRef.nativeElement.text; + } + + @Input() + public set text(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'text', value); + } + + public get triggerLabel(): string { + return this.elementRef.nativeElement.triggerLabel; + } + + @Input('trigger-label') + public set triggerLabel(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'triggerLabel', value); + } + + public get severity(): FvContextHelpSeverity { + return this.elementRef.nativeElement.severity; + } + + @Input() + public set severity(value: FvContextHelpSeverity) { + this.renderer.setProperty(this.elementRef.nativeElement, 'severity', value); + } + + public get iconVisible(): boolean { + return this.elementRef.nativeElement.iconVisible; + } + + @Input('icon-visible') + public set iconVisible(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'iconVisible', toBooleanProperty(value)); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/context-help/ok-fv-context-help.module.ts b/packages/angular-workspace/ok-angular/fv/context-help/ok-fv-context-help.module.ts new file mode 100644 index 0000000000..4565a72be1 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/context-help/ok-fv-context-help.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvContextHelpDirective } from './ok-fv-context-help.directive'; + +import '@ni/ok-components/dist/esm/fv/context-help'; + +@NgModule({ + declarations: [OkFvContextHelpDirective], + imports: [CommonModule], + exports: [OkFvContextHelpDirective] +}) +export class OkFvContextHelpModule { } diff --git a/packages/angular-workspace/ok-angular/fv/context-help/public-api.ts b/packages/angular-workspace/ok-angular/fv/context-help/public-api.ts new file mode 100644 index 0000000000..165dc8fbb0 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/context-help/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-context-help.directive'; +export * from './ok-fv-context-help.module'; diff --git a/packages/angular-workspace/ok-angular/fv/context-help/tests/ok-fv-context-help.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/context-help/tests/ok-fv-context-help.directive.spec.ts new file mode 100644 index 0000000000..a4a62e7d0c --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/context-help/tests/ok-fv-context-help.directive.spec.ts @@ -0,0 +1,200 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvContextHelp, OkFvContextHelpDirective } from '../ok-fv-context-help.directive'; +import { OkFvContextHelpModule } from '../ok-fv-context-help.module'; + +describe('Ok fv context help', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvContextHelpModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-context-help')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('contextHelp', { read: OkFvContextHelpDirective }) public directive: OkFvContextHelpDirective; + @ViewChild('contextHelp', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvContextHelpDirective; + let nativeElement: FvContextHelp; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvContextHelpModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for text', () => { + expect(directive.text).toBe(''); + expect(nativeElement.text).toBe(''); + }); + + it('has expected defaults for triggerLabel', () => { + expect(directive.triggerLabel).toBe('Show help'); + expect(nativeElement.triggerLabel).toBe('Show help'); + }); + + it('has expected defaults for iconVisible', () => { + expect(directive.iconVisible).toBe(false); + expect(nativeElement.iconVisible).toBe(false); + }); + + it('has expected defaults for severity', () => { + expect(directive.severity).toBeUndefined(); + expect(nativeElement.severity).toBeUndefined(); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('contextHelp', { read: OkFvContextHelpDirective }) public directive: OkFvContextHelpDirective; + @ViewChild('contextHelp', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvContextHelpDirective; + let nativeElement: FvContextHelp; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvContextHelpModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for text', () => { + expect(directive.text).toBe('Use the code shown on the device label.'); + expect(nativeElement.text).toBe('Use the code shown on the device label.'); + }); + + it('will use template string values for triggerLabel', () => { + expect(directive.triggerLabel).toBe('Show support code help'); + expect(nativeElement.triggerLabel).toBe('Show support code help'); + }); + + it('will use template string values for severity', () => { + expect(directive.severity).toBe('error'); + expect(nativeElement.severity).toBe('error'); + }); + + it('will use template string values for iconVisible', () => { + expect(directive.iconVisible).toBeTrue(); + expect(nativeElement.iconVisible).toBeTrue(); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('contextHelp', { read: OkFvContextHelpDirective }) public directive: OkFvContextHelpDirective; + @ViewChild('contextHelp', { read: ElementRef }) public elementRef: ElementRef; + public text = 'Use the code shown on the device label.'; + public triggerLabel = 'Show support code help'; + public severity: 'information' | 'error' | undefined = 'information'; + public iconVisible = false; + } + + let fixture: ComponentFixture; + let directive: OkFvContextHelpDirective; + let nativeElement: FvContextHelp; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvContextHelpModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for text', () => { + expect(directive.text).toBe('Use the code shown on the device label.'); + expect(nativeElement.text).toBe('Use the code shown on the device label.'); + + fixture.componentInstance.text = 'Reference the inline help when the label is not visible.'; + fixture.detectChanges(); + + expect(directive.text).toBe('Reference the inline help when the label is not visible.'); + expect(nativeElement.text).toBe('Reference the inline help when the label is not visible.'); + }); + + it('can be configured with property binding for triggerLabel', () => { + expect(directive.triggerLabel).toBe('Show support code help'); + expect(nativeElement.triggerLabel).toBe('Show support code help'); + + fixture.componentInstance.triggerLabel = 'Show operator guidance'; + fixture.detectChanges(); + + expect(directive.triggerLabel).toBe('Show operator guidance'); + expect(nativeElement.triggerLabel).toBe('Show operator guidance'); + }); + + it('can be configured with property binding for severity', () => { + expect(directive.severity).toBe('information'); + expect(nativeElement.severity).toBe('information'); + + fixture.componentInstance.severity = 'error'; + fixture.detectChanges(); + + expect(directive.severity).toBe('error'); + expect(nativeElement.severity).toBe('error'); + }); + + it('can be configured with property binding for iconVisible', () => { + expect(directive.iconVisible).toBeFalse(); + expect(nativeElement.iconVisible).toBeFalse(); + + fixture.componentInstance.iconVisible = true; + fixture.detectChanges(); + + expect(directive.iconVisible).toBeTrue(); + expect(nativeElement.iconVisible).toBeTrue(); + }); + }); +}); diff --git a/packages/angular-workspace/ok-angular/fv/search-input/tests/ok-fv-search-input.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/search-input/tests/ok-fv-search-input.directive.spec.ts index 78e16975de..ef2c71ed85 100644 --- a/packages/angular-workspace/ok-angular/fv/search-input/tests/ok-fv-search-input.directive.spec.ts +++ b/packages/angular-workspace/ok-angular/fv/search-input/tests/ok-fv-search-input.directive.spec.ts @@ -16,6 +16,96 @@ describe('Ok Fv Search Input', () => { }); }); + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('searchInput', { read: OkFvSearchInputDirective }) public directive: OkFvSearchInputDirective; + @ViewChild('searchInput', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSearchInputDirective; + let nativeElement: FvSearchInput; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSearchInputModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for appearance', () => { + expect(directive.appearance).toBe(FvSearchInputAppearance.outline); + expect(nativeElement.appearance).toBe(FvSearchInputAppearance.outline); + }); + + it('has expected defaults for placeholder', () => { + expect(directive.placeholder).toBeUndefined(); + expect(nativeElement.placeholder).toBeUndefined(); + }); + + it('has expected defaults for value', () => { + expect(directive.value).toBe(''); + expect(nativeElement.value).toBe(''); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('searchInput', { read: OkFvSearchInputDirective }) public directive: OkFvSearchInputDirective; + @ViewChild('searchInput', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSearchInputDirective; + let nativeElement: FvSearchInput; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSearchInputModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for appearance', () => { + expect(directive.appearance).toBe(FvSearchInputAppearance.frameless); + expect(nativeElement.appearance).toBe(FvSearchInputAppearance.frameless); + }); + + it('will use template string values for placeholder', () => { + expect(directive.placeholder).toBe('Search assets'); + expect(nativeElement.placeholder).toBe('Search assets'); + }); + + it('will use template string values for value', () => { + expect(directive.value).toBe('PXI'); + expect(nativeElement.value).toBe('PXI'); + }); + }); + describe('with property bound values', () => { @Component({ template: ` diff --git a/packages/angular-workspace/ok-angular/fv/split-button-anchor/ng-package.json b/packages/angular-workspace/ok-angular/fv/split-button-anchor/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button-anchor/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/split-button-anchor/ok-fv-split-button-anchor.directive.ts b/packages/angular-workspace/ok-angular/fv/split-button-anchor/ok-fv-split-button-anchor.directive.ts new file mode 100644 index 0000000000..68b35907bf --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button-anchor/ok-fv-split-button-anchor.directive.ts @@ -0,0 +1,104 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvSplitButtonAnchor } from '@ni/ok-components/dist/esm/fv/split-button-anchor'; +import { fvSplitButtonAnchorTag } from '@ni/ok-components/dist/esm/fv/split-button-anchor'; +import { FvSplitButtonAnchorAppearance, FvSplitButtonAnchorAppearanceVariant } from '@ni/ok-components/dist/esm/fv/split-button-anchor/types'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvSplitButtonAnchor }; +export { fvSplitButtonAnchorTag }; +export { FvSplitButtonAnchorAppearance, FvSplitButtonAnchorAppearanceVariant }; + +/** + * Directive to provide Angular integration for the split button anchor. + */ +@Directive({ + selector: 'ok-fv-split-button-anchor', + standalone: false +}) +export class OkFvSplitButtonAnchorDirective { + public get label(): string { + return this.elementRef.nativeElement.label; + } + + @Input() + public set label(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'label', value); + } + + public get href(): string { + return this.elementRef.nativeElement.href; + } + + @Input() + public set href(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'href', value); + } + + public get target(): string { + return this.elementRef.nativeElement.target; + } + + @Input() + public set target(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'target', value); + } + + public get rel(): string { + return this.elementRef.nativeElement.rel; + } + + @Input() + public set rel(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'rel', value); + } + + public get download(): string { + return this.elementRef.nativeElement.download; + } + + @Input() + public set download(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'download', value); + } + + public get disabled(): boolean { + return this.elementRef.nativeElement.disabled; + } + + @Input() + public set disabled(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', toBooleanProperty(value)); + } + + public get open(): boolean { + return this.elementRef.nativeElement.open; + } + + @Input() + public set open(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'open', toBooleanProperty(value)); + } + + public get appearance(): FvSplitButtonAnchorAppearance { + return this.elementRef.nativeElement.appearance; + } + + @Input() + public set appearance(value: FvSplitButtonAnchorAppearance) { + this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value); + } + + public get appearanceVariant(): FvSplitButtonAnchorAppearanceVariant { + return this.elementRef.nativeElement.appearanceVariant; + } + + @Input('appearance-variant') + public set appearanceVariant(value: FvSplitButtonAnchorAppearanceVariant) { + this.renderer.setProperty(this.elementRef.nativeElement, 'appearanceVariant', value); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/split-button-anchor/ok-fv-split-button-anchor.module.ts b/packages/angular-workspace/ok-angular/fv/split-button-anchor/ok-fv-split-button-anchor.module.ts new file mode 100644 index 0000000000..2ed300c8b2 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button-anchor/ok-fv-split-button-anchor.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvSplitButtonAnchorDirective } from './ok-fv-split-button-anchor.directive'; + +import '@ni/ok-components/dist/esm/fv/split-button-anchor'; + +@NgModule({ + declarations: [OkFvSplitButtonAnchorDirective], + imports: [CommonModule], + exports: [OkFvSplitButtonAnchorDirective] +}) +export class OkFvSplitButtonAnchorModule { } diff --git a/packages/angular-workspace/ok-angular/fv/split-button-anchor/public-api.ts b/packages/angular-workspace/ok-angular/fv/split-button-anchor/public-api.ts new file mode 100644 index 0000000000..a8686d6394 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button-anchor/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-split-button-anchor.directive'; +export * from './ok-fv-split-button-anchor.module'; diff --git a/packages/angular-workspace/ok-angular/fv/split-button-anchor/tests/ok-fv-split-button-anchor.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/split-button-anchor/tests/ok-fv-split-button-anchor.directive.spec.ts new file mode 100644 index 0000000000..9eee4611ec --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button-anchor/tests/ok-fv-split-button-anchor.directive.spec.ts @@ -0,0 +1,314 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvSplitButtonAnchor, FvSplitButtonAnchorAppearance, FvSplitButtonAnchorAppearanceVariant, OkFvSplitButtonAnchorDirective } from '../ok-fv-split-button-anchor.directive'; +import { OkFvSplitButtonAnchorModule } from '../ok-fv-split-button-anchor.module'; + +describe('Ok fv split button anchor', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvSplitButtonAnchorModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-split-button-anchor')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('splitButtonAnchor', { read: OkFvSplitButtonAnchorDirective }) public directive: OkFvSplitButtonAnchorDirective; + @ViewChild('splitButtonAnchor', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSplitButtonAnchorDirective; + let nativeElement: FvSplitButtonAnchor; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSplitButtonAnchorModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for label', () => { + expect(directive.label).toBe('Primary function'); + expect(nativeElement.label).toBe('Primary function'); + }); + + it('has expected defaults for href', () => { + expect(directive.href).toBe(''); + expect(nativeElement.href).toBe(''); + }); + + it('has expected defaults for target', () => { + expect(directive.target).toBe(''); + expect(nativeElement.target).toBe(''); + }); + + it('has expected defaults for rel', () => { + expect(directive.rel).toBe(''); + expect(nativeElement.rel).toBe(''); + }); + + it('has expected defaults for download', () => { + expect(directive.download).toBe(''); + expect(nativeElement.download).toBe(''); + }); + + it('has expected defaults for disabled', () => { + expect(directive.disabled).toBe(false); + expect(nativeElement.disabled).toBe(false); + }); + + it('has expected defaults for open', () => { + expect(directive.open).toBe(false); + expect(nativeElement.open).toBe(false); + }); + + it('has expected defaults for appearance', () => { + expect(directive.appearance).toBe(FvSplitButtonAnchorAppearance.outline); + expect(nativeElement.appearance).toBe(FvSplitButtonAnchorAppearance.outline); + }); + + it('has expected defaults for appearanceVariant', () => { + expect(directive.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.default); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.default); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('splitButtonAnchor', { read: OkFvSplitButtonAnchorDirective }) public directive: OkFvSplitButtonAnchorDirective; + @ViewChild('splitButtonAnchor', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSplitButtonAnchorDirective; + let nativeElement: FvSplitButtonAnchor; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSplitButtonAnchorModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for label', () => { + expect(directive.label).toBe('Open report'); + expect(nativeElement.label).toBe('Open report'); + }); + + it('will use template string values for href', () => { + expect(directive.href).toBe('https://example.com/report'); + expect(nativeElement.href).toBe('https://example.com/report'); + }); + + it('will use template string values for target', () => { + expect(directive.target).toBe('_blank'); + expect(nativeElement.target).toBe('_blank'); + }); + + it('will use template string values for rel', () => { + expect(directive.rel).toBe('noopener'); + expect(nativeElement.rel).toBe('noopener'); + }); + + it('will use template string values for download', () => { + expect(directive.download).toBe('report.csv'); + expect(nativeElement.download).toBe('report.csv'); + }); + + it('will use template string values for disabled', () => { + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('will use template string values for appearance', () => { + expect(directive.appearance).toBe(FvSplitButtonAnchorAppearance.block); + expect(nativeElement.appearance).toBe(FvSplitButtonAnchorAppearance.block); + }); + + it('will use template string values for appearanceVariant', () => { + expect(directive.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.primary); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.primary); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('splitButtonAnchor', { read: OkFvSplitButtonAnchorDirective }) public directive: OkFvSplitButtonAnchorDirective; + @ViewChild('splitButtonAnchor', { read: ElementRef }) public elementRef: ElementRef; + public label = 'Open report'; + public href = 'https://example.com/report'; + public target = '_blank'; + public rel = 'noopener'; + public download = 'report.csv'; + public disabled = false; + public open = false; + public appearance: FvSplitButtonAnchorAppearance = FvSplitButtonAnchorAppearance.outline; + public appearanceVariant: FvSplitButtonAnchorAppearanceVariant = FvSplitButtonAnchorAppearanceVariant.default; + } + + let fixture: ComponentFixture; + let directive: OkFvSplitButtonAnchorDirective; + let nativeElement: FvSplitButtonAnchor; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSplitButtonAnchorModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for label', () => { + expect(directive.label).toBe('Open report'); + expect(nativeElement.label).toBe('Open report'); + + fixture.componentInstance.label = 'Open dashboard'; + fixture.detectChanges(); + + expect(directive.label).toBe('Open dashboard'); + expect(nativeElement.label).toBe('Open dashboard'); + }); + + it('can be configured with property binding for href', () => { + expect(directive.href).toBe('https://example.com/report'); + expect(nativeElement.href).toBe('https://example.com/report'); + + fixture.componentInstance.href = 'https://example.com/dashboard'; + fixture.detectChanges(); + + expect(directive.href).toBe('https://example.com/dashboard'); + expect(nativeElement.href).toBe('https://example.com/dashboard'); + }); + + it('can be configured with property binding for target', () => { + expect(directive.target).toBe('_blank'); + expect(nativeElement.target).toBe('_blank'); + + fixture.componentInstance.target = '_self'; + fixture.detectChanges(); + + expect(directive.target).toBe('_self'); + expect(nativeElement.target).toBe('_self'); + }); + + it('can be configured with property binding for rel', () => { + expect(directive.rel).toBe('noopener'); + expect(nativeElement.rel).toBe('noopener'); + + fixture.componentInstance.rel = 'noreferrer'; + fixture.detectChanges(); + + expect(directive.rel).toBe('noreferrer'); + expect(nativeElement.rel).toBe('noreferrer'); + }); + + it('can be configured with property binding for download', () => { + expect(directive.download).toBe('report.csv'); + expect(nativeElement.download).toBe('report.csv'); + + fixture.componentInstance.download = 'summary.csv'; + fixture.detectChanges(); + + expect(directive.download).toBe('summary.csv'); + expect(nativeElement.download).toBe('summary.csv'); + }); + + it('can be configured with property binding for disabled', () => { + expect(directive.disabled).toBeFalse(); + expect(nativeElement.disabled).toBeFalse(); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('can be configured with property binding for open', () => { + expect(directive.open).toBeFalse(); + expect(nativeElement.open).toBeFalse(); + + fixture.componentInstance.open = true; + fixture.detectChanges(); + + expect(directive.open).toBeTrue(); + expect(nativeElement.open).toBeTrue(); + }); + + it('can be configured with property binding for appearance', () => { + expect(directive.appearance).toBe(FvSplitButtonAnchorAppearance.outline); + expect(nativeElement.appearance).toBe(FvSplitButtonAnchorAppearance.outline); + + fixture.componentInstance.appearance = FvSplitButtonAnchorAppearance.block; + fixture.detectChanges(); + + expect(directive.appearance).toBe(FvSplitButtonAnchorAppearance.block); + expect(nativeElement.appearance).toBe(FvSplitButtonAnchorAppearance.block); + }); + + it('can be configured with property binding for appearanceVariant', () => { + expect(directive.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.default); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.default); + + fixture.componentInstance.appearanceVariant = FvSplitButtonAnchorAppearanceVariant.primary; + fixture.detectChanges(); + + expect(directive.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.primary); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAnchorAppearanceVariant.primary); + }); + }); +}); diff --git a/packages/angular-workspace/ok-angular/fv/split-button/ng-package.json b/packages/angular-workspace/ok-angular/fv/split-button/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/split-button/ok-fv-split-button.directive.ts b/packages/angular-workspace/ok-angular/fv/split-button/ok-fv-split-button.directive.ts new file mode 100644 index 0000000000..a3e81c847a --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button/ok-fv-split-button.directive.ts @@ -0,0 +1,68 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvSplitButton } from '@ni/ok-components/dist/esm/fv/split-button'; +import { fvSplitButtonTag } from '@ni/ok-components/dist/esm/fv/split-button'; +import { FvSplitButtonAppearance, FvSplitButtonAppearanceVariant } from '@ni/ok-components/dist/esm/fv/split-button/types'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvSplitButton }; +export { fvSplitButtonTag }; +export { FvSplitButtonAppearance, FvSplitButtonAppearanceVariant }; + +/** + * Directive to provide Angular integration for the split button. + */ +@Directive({ + selector: 'ok-fv-split-button', + standalone: false +}) +export class OkFvSplitButtonDirective { + public get label(): string { + return this.elementRef.nativeElement.label; + } + + @Input() + public set label(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'label', value); + } + + public get disabled(): boolean { + return this.elementRef.nativeElement.disabled; + } + + @Input() + public set disabled(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', toBooleanProperty(value)); + } + + public get open(): boolean { + return this.elementRef.nativeElement.open; + } + + @Input() + public set open(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'open', toBooleanProperty(value)); + } + + public get appearance(): FvSplitButtonAppearance { + return this.elementRef.nativeElement.appearance; + } + + @Input() + public set appearance(value: FvSplitButtonAppearance) { + this.renderer.setProperty(this.elementRef.nativeElement, 'appearance', value); + } + + public get appearanceVariant(): FvSplitButtonAppearanceVariant { + return this.elementRef.nativeElement.appearanceVariant; + } + + @Input('appearance-variant') + public set appearanceVariant(value: FvSplitButtonAppearanceVariant) { + this.renderer.setProperty(this.elementRef.nativeElement, 'appearanceVariant', value); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/split-button/ok-fv-split-button.module.ts b/packages/angular-workspace/ok-angular/fv/split-button/ok-fv-split-button.module.ts new file mode 100644 index 0000000000..77c84e9f2e --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button/ok-fv-split-button.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvSplitButtonDirective } from './ok-fv-split-button.directive'; + +import '@ni/ok-components/dist/esm/fv/split-button'; + +@NgModule({ + declarations: [OkFvSplitButtonDirective], + imports: [CommonModule], + exports: [OkFvSplitButtonDirective] +}) +export class OkFvSplitButtonModule { } diff --git a/packages/angular-workspace/ok-angular/fv/split-button/public-api.ts b/packages/angular-workspace/ok-angular/fv/split-button/public-api.ts new file mode 100644 index 0000000000..93277791ce --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-split-button.directive'; +export * from './ok-fv-split-button.module'; diff --git a/packages/angular-workspace/ok-angular/fv/split-button/tests/ok-fv-split-button.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/split-button/tests/ok-fv-split-button.directive.spec.ts new file mode 100644 index 0000000000..870ba60dec --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/split-button/tests/ok-fv-split-button.directive.spec.ts @@ -0,0 +1,219 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvSplitButton, FvSplitButtonAppearance, FvSplitButtonAppearanceVariant, OkFvSplitButtonDirective } from '../ok-fv-split-button.directive'; +import { OkFvSplitButtonModule } from '../ok-fv-split-button.module'; + +describe('Ok fv split button', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvSplitButtonModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-split-button')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('splitButton', { read: OkFvSplitButtonDirective }) public directive: OkFvSplitButtonDirective; + @ViewChild('splitButton', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSplitButtonDirective; + let nativeElement: FvSplitButton; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSplitButtonModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for label', () => { + expect(directive.label).toBe('Primary function'); + expect(nativeElement.label).toBe('Primary function'); + }); + + it('has expected defaults for disabled', () => { + expect(directive.disabled).toBe(false); + expect(nativeElement.disabled).toBe(false); + }); + + it('has expected defaults for open', () => { + expect(directive.open).toBe(false); + expect(nativeElement.open).toBe(false); + }); + + it('has expected defaults for appearance', () => { + expect(directive.appearance).toBe(FvSplitButtonAppearance.outline); + expect(nativeElement.appearance).toBe(FvSplitButtonAppearance.outline); + }); + + it('has expected defaults for appearanceVariant', () => { + expect(directive.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.default); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.default); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('splitButton', { read: OkFvSplitButtonDirective }) public directive: OkFvSplitButtonDirective; + @ViewChild('splitButton', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSplitButtonDirective; + let nativeElement: FvSplitButton; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSplitButtonModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for label', () => { + expect(directive.label).toBe('Run action'); + expect(nativeElement.label).toBe('Run action'); + }); + + it('will use template string values for disabled', () => { + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('will use template string values for appearance', () => { + expect(directive.appearance).toBe(FvSplitButtonAppearance.block); + expect(nativeElement.appearance).toBe(FvSplitButtonAppearance.block); + }); + + it('will use template string values for appearanceVariant', () => { + expect(directive.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.primary); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.primary); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('splitButton', { read: OkFvSplitButtonDirective }) public directive: OkFvSplitButtonDirective; + @ViewChild('splitButton', { read: ElementRef }) public elementRef: ElementRef; + public label = 'Run action'; + public disabled = false; + public open = false; + public appearance: FvSplitButtonAppearance = FvSplitButtonAppearance.outline; + public appearanceVariant: FvSplitButtonAppearanceVariant = FvSplitButtonAppearanceVariant.default; + } + + let fixture: ComponentFixture; + let directive: OkFvSplitButtonDirective; + let nativeElement: FvSplitButton; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSplitButtonModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for label', () => { + expect(directive.label).toBe('Run action'); + expect(nativeElement.label).toBe('Run action'); + + fixture.componentInstance.label = 'Queue action'; + fixture.detectChanges(); + + expect(directive.label).toBe('Queue action'); + expect(nativeElement.label).toBe('Queue action'); + }); + + it('can be configured with property binding for disabled', () => { + expect(directive.disabled).toBeFalse(); + expect(nativeElement.disabled).toBeFalse(); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + + expect(directive.disabled).toBeTrue(); + expect(nativeElement.disabled).toBeTrue(); + }); + + it('can be configured with property binding for open', () => { + expect(directive.open).toBeFalse(); + expect(nativeElement.open).toBeFalse(); + + fixture.componentInstance.open = true; + fixture.detectChanges(); + + expect(directive.open).toBeTrue(); + expect(nativeElement.open).toBeTrue(); + }); + + it('can be configured with property binding for appearance', () => { + expect(directive.appearance).toBe(FvSplitButtonAppearance.outline); + expect(nativeElement.appearance).toBe(FvSplitButtonAppearance.outline); + + fixture.componentInstance.appearance = FvSplitButtonAppearance.block; + fixture.detectChanges(); + + expect(directive.appearance).toBe(FvSplitButtonAppearance.block); + expect(nativeElement.appearance).toBe(FvSplitButtonAppearance.block); + }); + + it('can be configured with property binding for appearanceVariant', () => { + expect(directive.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.default); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.default); + + fixture.componentInstance.appearanceVariant = FvSplitButtonAppearanceVariant.primary; + fixture.detectChanges(); + + expect(directive.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.primary); + expect(nativeElement.appearanceVariant).toBe(FvSplitButtonAppearanceVariant.primary); + }); + }); +}); diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ng-package.json b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ok-fv-summary-panel-tile.directive.ts b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ok-fv-summary-panel-tile.directive.ts new file mode 100644 index 0000000000..3c1ed80fff --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ok-fv-summary-panel-tile.directive.ts @@ -0,0 +1,68 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvSummaryPanelTile } from '@ni/ok-components/dist/esm/fv/summary-panel-tile'; +import { fvSummaryPanelTileTag } from '@ni/ok-components/dist/esm/fv/summary-panel-tile'; +import { FvSummaryPanelTileTextPosition } from '@ni/ok-components/dist/esm/fv/summary-panel-tile/types'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvSummaryPanelTile }; +export { fvSummaryPanelTileTag }; +export { FvSummaryPanelTileTextPosition }; + +/** + * Directive to provide Angular integration for the summary panel tile. + */ +@Directive({ + selector: 'ok-fv-summary-panel-tile', + standalone: false +}) +export class OkFvSummaryPanelTileDirective { + public get count(): string { + return this.elementRef.nativeElement.count; + } + + @Input() + public set count(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'count', value); + } + + public get label(): string { + return this.elementRef.nativeElement.label; + } + + @Input() + public set label(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'label', value); + } + + public get legacyStyle(): boolean { + return this.elementRef.nativeElement.legacyStyle; + } + + @Input('legacy-style') + public set legacyStyle(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'legacyStyle', toBooleanProperty(value)); + } + + public get selected(): boolean { + return this.elementRef.nativeElement.selected; + } + + @Input() + public set selected(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'selected', toBooleanProperty(value)); + } + + public get textPosition(): FvSummaryPanelTileTextPosition { + return this.elementRef.nativeElement.textPosition; + } + + @Input('text-position') + public set textPosition(value: FvSummaryPanelTileTextPosition) { + this.renderer.setProperty(this.elementRef.nativeElement, 'textPosition', value); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ok-fv-summary-panel-tile.module.ts b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ok-fv-summary-panel-tile.module.ts new file mode 100644 index 0000000000..f58a527243 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/ok-fv-summary-panel-tile.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvSummaryPanelTileDirective } from './ok-fv-summary-panel-tile.directive'; + +import '@ni/ok-components/dist/esm/fv/summary-panel-tile'; + +@NgModule({ + declarations: [OkFvSummaryPanelTileDirective], + imports: [CommonModule], + exports: [OkFvSummaryPanelTileDirective] +}) +export class OkFvSummaryPanelTileModule { } diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel-tile/public-api.ts b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/public-api.ts new file mode 100644 index 0000000000..91727edc6d --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-summary-panel-tile.directive'; +export * from './ok-fv-summary-panel-tile.module'; diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel-tile/tests/ok-fv-summary-panel-tile.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/tests/ok-fv-summary-panel-tile.directive.spec.ts new file mode 100644 index 0000000000..267d22fdb2 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel-tile/tests/ok-fv-summary-panel-tile.directive.spec.ts @@ -0,0 +1,224 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvSummaryPanelTile, FvSummaryPanelTileTextPosition, OkFvSummaryPanelTileDirective } from '../ok-fv-summary-panel-tile.directive'; +import { OkFvSummaryPanelTileModule } from '../ok-fv-summary-panel-tile.module'; + +describe('Ok fv summary panel tile', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvSummaryPanelTileModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-summary-panel-tile')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('summaryPanelTile', { read: OkFvSummaryPanelTileDirective }) public directive: OkFvSummaryPanelTileDirective; + @ViewChild('summaryPanelTile', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSummaryPanelTileDirective; + let nativeElement: FvSummaryPanelTile; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSummaryPanelTileModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for count', () => { + expect(directive.count).toBe(''); + expect(nativeElement.count).toBe(''); + }); + + it('has expected defaults for label', () => { + expect(directive.label).toBe(''); + expect(nativeElement.label).toBe(''); + }); + + it('has expected defaults for legacyStyle', () => { + expect(directive.legacyStyle).toBe(false); + expect(nativeElement.legacyStyle).toBe(false); + }); + + it('has expected defaults for selected', () => { + expect(directive.selected).toBe(false); + expect(nativeElement.selected).toBe(false); + }); + + it('has expected defaults for textPosition', () => { + expect(directive.textPosition).toBe(FvSummaryPanelTileTextPosition.beside); + expect(nativeElement.textPosition).toBe(FvSummaryPanelTileTextPosition.beside); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('summaryPanelTile', { read: OkFvSummaryPanelTileDirective }) public directive: OkFvSummaryPanelTileDirective; + @ViewChild('summaryPanelTile', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSummaryPanelTileDirective; + let nativeElement: FvSummaryPanelTile; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSummaryPanelTileModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for count', () => { + expect(directive.count).toBe('7'); + expect(nativeElement.count).toBe('7'); + }); + + it('will use template string values for label', () => { + expect(directive.label).toBe('open items'); + expect(nativeElement.label).toBe('open items'); + }); + + it('will use template string values for legacyStyle', () => { + expect(directive.legacyStyle).toBeTrue(); + expect(nativeElement.legacyStyle).toBeTrue(); + }); + + it('will use template string values for selected', () => { + expect(directive.selected).toBeTrue(); + expect(nativeElement.selected).toBeTrue(); + }); + + it('will use template string values for textPosition', () => { + expect(directive.textPosition).toBe(FvSummaryPanelTileTextPosition.under); + expect(nativeElement.textPosition).toBe(FvSummaryPanelTileTextPosition.under); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('summaryPanelTile', { read: OkFvSummaryPanelTileDirective }) public directive: OkFvSummaryPanelTileDirective; + @ViewChild('summaryPanelTile', { read: ElementRef }) public elementRef: ElementRef; + public count = '7'; + public label = 'open items'; + public legacyStyle = false; + public selected = false; + public textPosition: FvSummaryPanelTileTextPosition = FvSummaryPanelTileTextPosition.beside; + } + + let fixture: ComponentFixture; + let directive: OkFvSummaryPanelTileDirective; + let nativeElement: FvSummaryPanelTile; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSummaryPanelTileModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for count', () => { + expect(directive.count).toBe('7'); + expect(nativeElement.count).toBe('7'); + + fixture.componentInstance.count = '12'; + fixture.detectChanges(); + + expect(directive.count).toBe('12'); + expect(nativeElement.count).toBe('12'); + }); + + it('can be configured with property binding for label', () => { + expect(directive.label).toBe('open items'); + expect(nativeElement.label).toBe('open items'); + + fixture.componentInstance.label = 'pending reviews'; + fixture.detectChanges(); + + expect(directive.label).toBe('pending reviews'); + expect(nativeElement.label).toBe('pending reviews'); + }); + + it('can be configured with property binding for legacyStyle', () => { + expect(directive.legacyStyle).toBeFalse(); + expect(nativeElement.legacyStyle).toBeFalse(); + + fixture.componentInstance.legacyStyle = true; + fixture.detectChanges(); + + expect(directive.legacyStyle).toBeTrue(); + expect(nativeElement.legacyStyle).toBeTrue(); + }); + + it('can be configured with property binding for selected', () => { + expect(directive.selected).toBeFalse(); + expect(nativeElement.selected).toBeFalse(); + + fixture.componentInstance.selected = true; + fixture.detectChanges(); + + expect(directive.selected).toBeTrue(); + expect(nativeElement.selected).toBeTrue(); + }); + + it('can be configured with property binding for textPosition', () => { + expect(directive.textPosition).toBe(FvSummaryPanelTileTextPosition.beside); + expect(nativeElement.textPosition).toBe(FvSummaryPanelTileTextPosition.beside); + + fixture.componentInstance.textPosition = FvSummaryPanelTileTextPosition.under; + fixture.detectChanges(); + + expect(directive.textPosition).toBe(FvSummaryPanelTileTextPosition.under); + expect(nativeElement.textPosition).toBe(FvSummaryPanelTileTextPosition.under); + }); + }); +}); diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel/ng-package.json b/packages/angular-workspace/ok-angular/fv/summary-panel/ng-package.json new file mode 100644 index 0000000000..90febd7fae --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel/ok-fv-summary-panel.directive.ts b/packages/angular-workspace/ok-angular/fv/summary-panel/ok-fv-summary-panel.directive.ts new file mode 100644 index 0000000000..3fb8b0f8dd --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel/ok-fv-summary-panel.directive.ts @@ -0,0 +1,48 @@ +import { Directive, ElementRef, Input, Renderer2 } from '@angular/core'; +import type { FvSummaryPanel } from '@ni/ok-components/dist/esm/fv/summary-panel'; +import { fvSummaryPanelTag } from '@ni/ok-components/dist/esm/fv/summary-panel'; +import { type BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities'; + +export type { FvSummaryPanel }; +export { fvSummaryPanelTag }; + +/** + * Directive to provide Angular integration for the summary panel. + */ +@Directive({ + selector: 'ok-fv-summary-panel', + standalone: false +}) +export class OkFvSummaryPanelDirective { + public get showEditItemsButton(): boolean { + return this.elementRef.nativeElement.showEditItemsButton; + } + + @Input('show-edit-items-button') + public set showEditItemsButton(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'showEditItemsButton', toBooleanProperty(value)); + } + + public get legacyStyle(): boolean { + return this.elementRef.nativeElement.legacyStyle; + } + + @Input('legacy-style') + public set legacyStyle(value: BooleanValueOrAttribute) { + this.renderer.setProperty(this.elementRef.nativeElement, 'legacyStyle', toBooleanProperty(value)); + } + + public get editItemsButtonLabel(): string { + return this.elementRef.nativeElement.editItemsButtonLabel; + } + + @Input('edit-items-button-label') + public set editItemsButtonLabel(value: string) { + this.renderer.setProperty(this.elementRef.nativeElement, 'editItemsButtonLabel', value); + } + + public constructor( + private readonly elementRef: ElementRef, + private readonly renderer: Renderer2 + ) {} +} diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel/ok-fv-summary-panel.module.ts b/packages/angular-workspace/ok-angular/fv/summary-panel/ok-fv-summary-panel.module.ts new file mode 100644 index 0000000000..ef47df520f --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel/ok-fv-summary-panel.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OkFvSummaryPanelDirective } from './ok-fv-summary-panel.directive'; + +import '@ni/ok-components/dist/esm/fv/summary-panel'; + +@NgModule({ + declarations: [OkFvSummaryPanelDirective], + imports: [CommonModule], + exports: [OkFvSummaryPanelDirective] +}) +export class OkFvSummaryPanelModule { } diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel/public-api.ts b/packages/angular-workspace/ok-angular/fv/summary-panel/public-api.ts new file mode 100644 index 0000000000..f4f0c32f48 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel/public-api.ts @@ -0,0 +1,2 @@ +export * from './ok-fv-summary-panel.directive'; +export * from './ok-fv-summary-panel.module'; diff --git a/packages/angular-workspace/ok-angular/fv/summary-panel/tests/ok-fv-summary-panel.directive.spec.ts b/packages/angular-workspace/ok-angular/fv/summary-panel/tests/ok-fv-summary-panel.directive.spec.ts new file mode 100644 index 0000000000..4bc8b4c762 --- /dev/null +++ b/packages/angular-workspace/ok-angular/fv/summary-panel/tests/ok-fv-summary-panel.directive.spec.ts @@ -0,0 +1,176 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { type FvSummaryPanel, OkFvSummaryPanelDirective } from '../ok-fv-summary-panel.directive'; +import { OkFvSummaryPanelModule } from '../ok-fv-summary-panel.module'; + +describe('Ok fv summary panel', () => { + describe('module', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [OkFvSummaryPanelModule] + }); + }); + + it('custom element is defined', () => { + expect(customElements.get('ok-fv-summary-panel')).not.toBeUndefined(); + }); + }); + + describe('with no values in template', () => { + @Component({ + template: ` + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('summaryPanel', { read: OkFvSummaryPanelDirective }) public directive: OkFvSummaryPanelDirective; + @ViewChild('summaryPanel', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSummaryPanelDirective; + let nativeElement: FvSummaryPanel; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSummaryPanelModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('has expected defaults for showEditItemsButton', () => { + expect(directive.showEditItemsButton).toBe(false); + expect(nativeElement.showEditItemsButton).toBe(false); + }); + + it('has expected defaults for legacyStyle', () => { + expect(directive.legacyStyle).toBe(false); + expect(nativeElement.legacyStyle).toBe(false); + }); + + it('has expected defaults for editItemsButtonLabel', () => { + expect(directive.editItemsButtonLabel).toBe('Configure'); + expect(nativeElement.editItemsButtonLabel).toBe('Configure'); + }); + }); + + describe('with template string values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('summaryPanel', { read: OkFvSummaryPanelDirective }) public directive: OkFvSummaryPanelDirective; + @ViewChild('summaryPanel', { read: ElementRef }) public elementRef: ElementRef; + } + + let fixture: ComponentFixture; + let directive: OkFvSummaryPanelDirective; + let nativeElement: FvSummaryPanel; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSummaryPanelModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('will use template string values for showEditItemsButton', () => { + expect(directive.showEditItemsButton).toBeTrue(); + expect(nativeElement.showEditItemsButton).toBeTrue(); + }); + + it('will use template string values for legacyStyle', () => { + expect(directive.legacyStyle).toBeTrue(); + expect(nativeElement.legacyStyle).toBeTrue(); + }); + + it('will use template string values for editItemsButtonLabel', () => { + expect(directive.editItemsButtonLabel).toBe('Customize summary'); + expect(nativeElement.editItemsButtonLabel).toBe('Customize summary'); + }); + }); + + describe('with property bound values', () => { + @Component({ + template: ` + + + `, + standalone: false + }) + class TestHostComponent { + @ViewChild('summaryPanel', { read: OkFvSummaryPanelDirective }) public directive: OkFvSummaryPanelDirective; + @ViewChild('summaryPanel', { read: ElementRef }) public elementRef: ElementRef; + public showEditItemsButton = false; + public legacyStyle = false; + public editItemsButtonLabel = 'Configure tiles'; + } + + let fixture: ComponentFixture; + let directive: OkFvSummaryPanelDirective; + let nativeElement: FvSummaryPanel; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestHostComponent], + imports: [OkFvSummaryPanelModule] + }); + fixture = TestBed.createComponent(TestHostComponent); + fixture.detectChanges(); + directive = fixture.componentInstance.directive; + nativeElement = fixture.componentInstance.elementRef.nativeElement; + }); + + it('can be configured with property binding for showEditItemsButton', () => { + expect(directive.showEditItemsButton).toBeFalse(); + expect(nativeElement.showEditItemsButton).toBeFalse(); + + fixture.componentInstance.showEditItemsButton = true; + fixture.detectChanges(); + + expect(directive.showEditItemsButton).toBeTrue(); + expect(nativeElement.showEditItemsButton).toBeTrue(); + }); + + it('can be configured with property binding for legacyStyle', () => { + expect(directive.legacyStyle).toBeFalse(); + expect(nativeElement.legacyStyle).toBeFalse(); + + fixture.componentInstance.legacyStyle = true; + fixture.detectChanges(); + + expect(directive.legacyStyle).toBeTrue(); + expect(nativeElement.legacyStyle).toBeTrue(); + }); + + it('can be configured with property binding for editItemsButtonLabel', () => { + expect(directive.editItemsButtonLabel).toBe('Configure tiles'); + expect(nativeElement.editItemsButtonLabel).toBe('Configure tiles'); + + fixture.componentInstance.editItemsButtonLabel = 'Customize summary'; + fixture.detectChanges(); + + expect(directive.editItemsButtonLabel).toBe('Customize summary'); + expect(nativeElement.editItemsButtonLabel).toBe('Customize summary'); + }); + }); +}); diff --git a/packages/eslint-config-nimble/ok/fv/ignore-attributes.js b/packages/eslint-config-nimble/ok/fv/ignore-attributes.js index 5858185bdb..54ae3473b8 100644 --- a/packages/eslint-config-nimble/ok/fv/ignore-attributes.js +++ b/packages/eslint-config-nimble/ok/fv/ignore-attributes.js @@ -1,7 +1,26 @@ // Fv angular attributes that SHOULD NOT ever be localized need to be added to ignoreAttributeSets // See: https://github.com/ni/javascript-styleguide/blob/main/packages/eslint-config-angular/lib/ok/fv/ignore-attributes.js -// Fv angular attributes that SHOULD be localized in production, but we don't want to localize -// in example apps should be added to the following list: +const exampleAppOnlyIgnoreAttributes = [ + // Should be localized in production, but are intentionally not localized in example apps. + 'card-title', + 'description', + 'edit-items-button-label', + 'header', + 'options', + 'selected-values', + 'subtitle', + 'trigger-label', +]; + +const temporaryAlwaysIgnoreAttributes = [ + // Should not be localized. Keep these here until they move into the shared ignoreAttributeSets. + 'count', + 'initials', + 'interaction-mode', +]; + export const fvIgnoreAttributes = [ + ...exampleAppOnlyIgnoreAttributes, + ...temporaryAlwaysIgnoreAttributes, ]; diff --git a/packages/ok-components/src/fv/.github/skills/component-review/SKILL.md b/packages/ok-components/src/fv/.github/skills/component-review/SKILL.md index 8a67d14439..ddd639f990 100644 --- a/packages/ok-components/src/fv/.github/skills/component-review/SKILL.md +++ b/packages/ok-components/src/fv/.github/skills/component-review/SKILL.md @@ -1,7 +1,7 @@ --- name: component-review description: This skill should be used when the user asks to "review this component PR", "review this Nimble component", "review this OK component change", "check component best practices", "review Storybook and wrapper changes", or "propose review feedback responses" for component work in this repository. Also use it when updating older OK components to match newer sibling implementations and current repo review standards. -version: 0.3.5 +version: 0.3.6 --- # Component Review @@ -41,6 +41,7 @@ If the review is for a pull request, inspect unresolved review comments and grou Review the PR structure and release metadata as part of the change quality bar: - Check beachball change files for the right change type, a client-impact-focused description, and the GitHub obfuscated email format +- Check that each published package changed in the PR has its own change file; do not treat a component-package change file as covering Angular, React, or Blazor wrapper package changes - If a bumped monorepo package is only a devDependency consumer, check whether `dependentChangeType` should be `none` - Treat oversized or multi-purpose PRs as review feedback when the scope is hard to reason about - Treat vague PR titles or descriptions as review feedback when they obscure client impact, test gaps, or rollout risk @@ -137,7 +138,9 @@ Review wrappers and example apps with repo-specific expectations: - Confirm Angular wrappers re-export public enum-like types when clients need them - Prefer `BooleanValueOrAttribute` plus `toBooleanProperty` for Angular directive boolean inputs so OK wrappers match the Nimble wrapper coercion pattern instead of relying on raw `booleanAttribute` transforms - Confirm wrapper property names and transforms match repo patterns -- Do not add `i18n` markers to example-client-app strings; the example app is not localized +- For Angular wrappers over web component attributes exposed in dash case, require `@Input('dash-case-name')` aliases on the directive setters so Angular clients can bind with the actual public attribute names instead of only camelCase wrapper property names +- In example-client-app templates, use the actual dash-case custom-element attribute names for static attributes; treat camelCase static attributes like `interactionMode` or `selectedValues` as review feedback because they do not match the real HTML attribute contract +- Do not add `i18n` markers to example-client-app strings; the example app is not localized. For FV example-app strings that intentionally skip localization, prefer the shared ignore list in `packages/eslint-config-nimble/ok/fv/ignore-attributes.js` over sprinkling temporary `i18n-*` markers through the examples - Confirm every example app that should demonstrate the component has been updated, not just the first touched app - Treat missing updates in Angular, Blazor, React, or other repo example apps as review feedback when the component is available there - Confirm build-order assumptions when wrappers consume generated `dist/esm` output from components @@ -167,6 +170,8 @@ Use the commands in as the default val When the review covers example app updates, run each relevant example app and confirm the updated component renders and behaves correctly instead of relying on static code inspection alone. +When the review adds or updates wrapper default-value tests, confirm those expectations against the underlying component implementation or inherited base-class behavior instead of assuming empty-string defaults for text-like properties. + Treat "example app was not run" as a testing gap unless environment constraints make runtime validation impossible. If unrelated pre-existing failures block broader validation, say so explicitly and distinguish them from issues introduced by the reviewed change.