From eb38e6f1a7ae3eae28bd4c8789b5a4f970a8eabf Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Thu, 19 Sep 2024 13:49:29 -0400 Subject: [PATCH 1/8] ci: fix e2e for pristine commits (#2760) --- .github/actions/e2e-affected/action.yml | 2 +- .../src/e2e/modal-viewkept-toolbars.cy.ts | 34 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/.github/actions/e2e-affected/action.yml b/.github/actions/e2e-affected/action.yml index df1dd5be3f..fc8c2ebe85 100644 --- a/.github/actions/e2e-affected/action.yml +++ b/.github/actions/e2e-affected/action.yml @@ -88,7 +88,7 @@ runs: '--withTarget=e2e', '--affected', '--json' - ]).then(({stdout}) => JSON.parse(stdout)); + ]).then(({stdout}) => JSON.parse(stdout)).catch(() => []); const projectAffected = affectedProjects.includes('${{ inputs.project }}'); if (projectAffected) { core.info(`E2E tests affected`); diff --git a/apps/integration-e2e/src/e2e/modal-viewkept-toolbars.cy.ts b/apps/integration-e2e/src/e2e/modal-viewkept-toolbars.cy.ts index d728056459..cf03530ae1 100644 --- a/apps/integration-e2e/src/e2e/modal-viewkept-toolbars.cy.ts +++ b/apps/integration-e2e/src/e2e/modal-viewkept-toolbars.cy.ts @@ -10,7 +10,7 @@ describe('modal-viewkept-toolbars', () => { }); it('verify viewkept toolbar in modal', () => { - cy.get('#ready') + cy.get('#ready', { timeout: 10000 }) .should('exist') .end() .get('#modal-viewkept-toolbars-modal-trigger') @@ -18,16 +18,40 @@ describe('modal-viewkept-toolbars', () => { .should('contain', 'Open modal') .click(); cy.get('sky-modal-header') + .should('exist') .should('be.visible') .should('contain', 'Viewkeeper inside a Modal'); - cy.get('.sky-lookup-show-more-modal-toolbar').should('be.visible'); - cy.get('.sky-lookup-show-more-modal-multiselect-toolbar').should( - 'be.visible', - ); + cy.get('.sky-lookup-show-more-modal-toolbar') + .should('exist') + .should('be.visible'); + cy.get('.sky-lookup-show-more-modal-toolbar sky-icon[icon="search"]') + .should('exist') + .should('be.visible'); + cy.get( + '.sky-lookup-show-more-modal-toolbar sky-icon[icon="search"] > i', + ) + .should('exist') + .should('be.visible') + .should('have.css', 'height'); + cy.get('.sky-lookup-show-more-modal-multiselect-toolbar') + .should('exist') + .should('be.visible'); cy.get('sky-modal-content > p:nth-child(2)').should('be.visible'); cy.get('.sky-modal-content').should('be.visible').scrollTo('bottom'); cy.get('sky-modal-content > p:nth-child(2)').should('not.be.visible'); cy.get('.sky-lookup-show-more-modal-toolbar').should('be.visible'); + cy.get('sky-modal-content > .sky-viewkeeper-fixed').should( + 'satisfy', + (el: JQuery) => + el.first() && parseFloat(el.first().css('opacity')) > 0.9, + ); + cy.get( + '.sky-lookup-show-more-modal-toolbar sky-icon[icon="search"] > i', + ).should( + 'satisfy', + (el: JQuery) => + el.first() && parseFloat(el.first().css('opacity')) > 0.9, + ); cy.get('.sky-lookup-show-more-modal-multiselect-toolbar').should( 'be.visible', ); From e63f73844be2b300df6e8e3ba4dd91a1bba7aaa9 Mon Sep 17 00:00:00 2001 From: John White <750350+johnhwhite@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:35:21 -0400 Subject: [PATCH 2/8] feat(components/ag-grid): support changing the tab focus behavior (#2748) --- .../ag-grid/data-entry-grid/focus/data.ts | 156 ++++++++++++++++++ .../data-entry-grid/focus/demo.component.html | 23 +++ .../data-entry-grid/focus/demo.component.ts | 115 +++++++++++++ .../src/app/features/ag-grid.module.ts | 7 + .../src/app/home/home.component.html | 5 + .../ag-grid/ag-grid-wrapper.component.spec.ts | 117 +++++++------ .../ag-grid/ag-grid-wrapper.component.ts | 63 ++----- .../lib/modules/ag-grid/ag-grid.service.ts | 18 ++ .../ag-grid/types/last-focused.cell.ts | 4 + 9 files changed, 402 insertions(+), 106 deletions(-) create mode 100644 apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/data.ts create mode 100644 apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.html create mode 100644 apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.ts create mode 100644 libs/components/ag-grid/src/lib/modules/ag-grid/types/last-focused.cell.ts diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/data.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/data.ts new file mode 100644 index 0000000000..c19a452159 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/data.ts @@ -0,0 +1,156 @@ +export interface AutocompleteOption { + id: string; + name: string; +} + +export const DEPARTMENTS = [ + { + id: '1', + name: 'Marketing', + }, + { + id: '2', + name: 'Sales', + }, + { + id: '3', + name: 'Engineering', + }, + { + id: '4', + name: 'Customer Support', + }, +]; + +export const JOB_TITLES: Record = { + Marketing: [ + { + id: '1', + name: 'Social Media Coordinator', + }, + { + id: '2', + name: 'Blog Manager', + }, + { + id: '3', + name: 'Events Manager', + }, + ], + Sales: [ + { + id: '4', + name: 'Business Development Representative', + }, + { + id: '5', + name: 'Account Executive', + }, + ], + Engineering: [ + { + id: '6', + name: 'Software Engineer', + }, + { + id: '7', + name: 'Senior Software Engineer', + }, + { + id: '8', + name: 'Principal Software Engineer', + }, + { + id: '9', + name: 'UX Designer', + }, + { + id: '10', + name: 'Product Manager', + }, + ], + 'Customer Support': [ + { + id: '11', + name: 'Customer Support Representative', + }, + { + id: '12', + name: 'Account Manager', + }, + { + id: '13', + name: 'Customer Support Specialist', + }, + ], +}; + +export interface AgGridDemoRow { + selected?: boolean; + name: string; + age: number; + startDate: Date; + endDate?: Date; + department: AutocompleteOption; + jobTitle?: AutocompleteOption; +} + +export const AG_GRID_DEMO_DATA: AgGridDemoRow[] = [ + { + selected: true, + name: 'Billy Bob', + age: 55, + startDate: new Date('12/1/1994'), + department: DEPARTMENTS[3], + jobTitle: JOB_TITLES['Customer Support'][1], + }, + { + selected: false, + name: 'Jane Deere', + age: 33, + startDate: new Date('7/15/2009'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][2], + }, + { + selected: false, + name: 'John Doe', + age: 38, + startDate: new Date('9/1/2017'), + endDate: new Date('9/30/2017'), + department: DEPARTMENTS[1], + }, + { + selected: false, + name: 'David Smith', + age: 51, + startDate: new Date('1/1/2012'), + endDate: new Date('6/15/2018'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][4], + }, + { + selected: true, + name: 'Emily Johnson', + age: 41, + startDate: new Date('1/15/2014'), + department: DEPARTMENTS[0], + jobTitle: JOB_TITLES['Marketing'][2], + }, + { + selected: false, + name: 'Nicole Davidson', + age: 22, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][0], + }, + { + selected: false, + name: 'Carl Roberts', + age: 23, + startDate: new Date('11/1/2019'), + department: DEPARTMENTS[2], + jobTitle: JOB_TITLES['Engineering'][3], + }, +]; diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.html b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.html new file mode 100644 index 0000000000..85437ba939 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.html @@ -0,0 +1,23 @@ + + + +
+ + + +
+ + + diff --git a/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.ts b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.ts new file mode 100644 index 0000000000..cf25e06144 --- /dev/null +++ b/apps/code-examples/src/app/code-examples/ag-grid/data-entry-grid/focus/demo.component.ts @@ -0,0 +1,115 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { SkyAgGridModule, SkyAgGridService, SkyCellType } from '@skyux/ag-grid'; +import { SkyInputBoxModule } from '@skyux/forms'; + +import { AgGridModule } from 'ag-grid-angular'; +import { ValueFormatterParams } from 'ag-grid-community'; + +import { AG_GRID_DEMO_DATA, AgGridDemoRow } from './data'; + +@Component({ + standalone: true, + selector: 'app-demo', + templateUrl: './demo.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [AgGridModule, SkyAgGridModule, SkyInputBoxModule], +}) +export class DemoComponent { + protected gridData = AG_GRID_DEMO_DATA; + protected gridOptions = inject(SkyAgGridService).getEditableGridOptions({ + gridOptions: { + columnDefs: [ + { + field: 'name', + headerName: 'Name', + type: SkyCellType.Text, + editable: true, + cellRendererParams: { + skyComponentProperties: { + validator: (value: string): boolean => String(value).length <= 10, + validatorMessage: `Value exceeds maximum length`, + }, + }, + }, + { + field: 'age', + headerName: 'Age', + type: SkyCellType.Number, + editable: true, + cellRendererParams: { + skyComponentProperties: { + validator: (value: number): boolean => value >= 18, + validatorMessage: `Age must be 18+`, + }, + }, + maxWidth: 60, + }, + { + field: 'startDate', + headerName: 'Start date', + type: SkyCellType.Date, + editable: true, + sort: 'asc', + }, + { + field: 'endDate', + headerName: 'End date', + type: SkyCellType.Date, + editable: true, + valueFormatter: (params: ValueFormatterParams) => + this.#endDateFormatter(params), + }, + { + field: 'department', + headerName: 'Department', + type: SkyCellType.Autocomplete, + editable: true, + }, + { + field: 'jobTitle', + headerName: 'Title', + type: SkyCellType.Autocomplete, + editable: true, + }, + { + colId: 'validationCurrency', + field: 'validationCurrency', + headerName: 'Validation currency', + type: [SkyCellType.CurrencyValidator], + editable: true, + }, + { + colId: 'validationDate', + field: 'validationDate', + headerName: 'Validation date', + type: [SkyCellType.Date, SkyCellType.Validator], + editable: true, + cellRendererParams: { + skyComponentProperties: { + validator: (value: Date): boolean => + !!value && value > new Date(1985, 9, 26), + validatorMessage: 'Enter a future date', + }, + }, + }, + ], + focusGridInnerElement: (params) => { + params.api.startEditingCell({ + rowIndex: 0, + colKey: 'name', + }); + return true; + }, + }, + }); + + #endDateFormatter(params: ValueFormatterParams): string { + return params.value + ? params.value.toLocaleDateString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : 'N/A'; + } +} diff --git a/apps/code-examples/src/app/features/ag-grid.module.ts b/apps/code-examples/src/app/features/ag-grid.module.ts index 2af1fd9e1b..f33c2dc201 100644 --- a/apps/code-examples/src/app/features/ag-grid.module.ts +++ b/apps/code-examples/src/app/features/ag-grid.module.ts @@ -16,6 +16,13 @@ const routes: Routes = [ '../code-examples/ag-grid/data-entry-grid/data-manager-added/demo.component' ).then((c) => c.DemoComponent), }, + { + path: 'data-entry-grid/focus', + loadComponent: () => + import( + '../code-examples/ag-grid/data-entry-grid/focus/demo.component' + ).then((c) => c.DemoComponent), + }, { path: 'data-entry-grid/inline-help', loadComponent: () => diff --git a/apps/code-examples/src/app/home/home.component.html b/apps/code-examples/src/app/home/home.component.html index 0e8db69809..a75db0e249 100644 --- a/apps/code-examples/src/app/home/home.component.html +++ b/apps/code-examples/src/app/home/home.component.html @@ -20,6 +20,11 @@ Data Entry Grid with Data Manager +
  • + + Data Entry Grid with Initial Focus + +
  • Data Entry Grid with inline help diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts index 8e0b47e329..c2b341182d 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.spec.ts @@ -17,6 +17,7 @@ import { CellFocusedEvent, DetailGridInfo, FirstDataRenderedEvent, + FocusGridInnerElementParams, GridApi, GridReadyEvent, HeaderFocusedEvent, @@ -95,7 +96,7 @@ describe('SkyAgGridWrapperComponent', () => { }; agGrid = { api, - + gridOptions: gridFixture.componentInstance.gridOptions, gridReady: new Subject(), rowDataUpdated: new Subject(), firstDataRendered: new Subject(), @@ -431,39 +432,19 @@ describe('SkyAgGridWrapperComponent', () => { gridWrapperFixture.detectChanges(); } - it('should shift focus to the first grid cell if it was not the previously focused element and there is a cell', () => { + it('should shift focus to the first grid cell if it was not the previously focused element', () => { const afterAnchorEl = gridWrapperNativeElement.querySelector( `#${gridWrapperComponent.afterAnchorId}`, ) as HTMLElement; const afterButtonEl = gridWrapperNativeElement.querySelector( '#button-after-grid', ) as HTMLElement; - const column = new AgColumn({}, {}, 'name', true); - - (agGrid.api.getAllDisplayedColumns as jasmine.Spy).and.returnValue([ - column, - ]); - (agGrid.api.ensureColumnVisible as jasmine.Spy).and.stub(); - + spyOn(gridWrapperNativeElement, 'contains').and.returnValue(true); + const querySelector = spyOn(gridWrapperNativeElement, 'querySelector'); focusOnAnchor(afterAnchorEl, afterButtonEl); - - expect(agGrid.api.ensureColumnVisible).toHaveBeenCalledWith('name'); - }); - - it('should not shift focus to the first grid cell if there is no cell', () => { - const afterAnchorEl = gridWrapperNativeElement.querySelector( - `#${gridWrapperComponent.afterAnchorId}`, - ) as HTMLElement; - const afterButtonEl = gridWrapperNativeElement.querySelector( - '#button-after-grid', - ) as HTMLElement; - - (agGrid.api.getAllDisplayedColumns as jasmine.Spy).and.returnValue([]); - - focusOnAnchor(afterAnchorEl, afterButtonEl); - - expect(agGrid.api.setFocusedCell).not.toHaveBeenCalled(); - expect(agGrid.api.setFocusedHeader).not.toHaveBeenCalled(); + expect(querySelector).toHaveBeenCalledWith( + '.ag-tab-guard.ag-tab-guard-top', + ); }); it('should not shift focus to the grid if it was the previously focused element', () => { @@ -479,66 +460,80 @@ describe('SkyAgGridWrapperComponent', () => { expect(agGrid.api.setFocusedHeader).not.toHaveBeenCalled(); }); - it('should focus on the last focused header', () => { - const afterAnchorEl = gridWrapperNativeElement.querySelector( - `#${gridWrapperComponent.afterAnchorId}`, - ) as HTMLElement; - const afterButtonEl = gridWrapperNativeElement.querySelector( - '#button-after-grid', - ) as HTMLElement; + it('should track focus on header', async () => { const column = new AgColumn({}, {}, 'name', true); - (agGrid.api.getAllDisplayedColumns as jasmine.Spy).and.returnValue([ - column, - ]); - (agGrid.api.ensureColumnVisible as jasmine.Spy).and.stub(); - agGrid.headerFocused.next({ column: null, + context: agGrid.gridOptions?.context, + } as unknown as HeaderFocusedEvent); + agGrid.headerFocused.next({ + column, + context: agGrid.gridOptions?.context, } as unknown as HeaderFocusedEvent); - agGrid.headerFocused.next({ column } as unknown as HeaderFocusedEvent); agGrid.headerFocused.next({ column: 'name', + context: agGrid.gridOptions?.context, } as unknown as HeaderFocusedEvent); - focusOnAnchor(afterAnchorEl, afterButtonEl); - expect(agGrid.api.setFocusedHeader).toHaveBeenCalledWith('name'); - expect(agGrid.api.setFocusedCell).not.toHaveBeenCalled(); + expect(agGrid.gridOptions?.context?.lastFocusedCell).toEqual({ + rowIndex: null, + column: 'name', + }); + const focusGridInnerElement = agGrid.gridOptions?.focusGridInnerElement; + if (focusGridInnerElement) { + expect( + focusGridInnerElement({ + context: agGrid.gridOptions?.context, + api: agGrid.api, + } as FocusGridInnerElementParams), + ).toBeTrue(); + } else { + expect(focusGridInnerElement).toBeTruthy(); + } }); - it('should focus on the last focused cell', () => { - const afterAnchorEl = gridWrapperNativeElement.querySelector( - `#${gridWrapperComponent.afterAnchorId}`, - ) as HTMLElement; - const afterButtonEl = gridWrapperNativeElement.querySelector( - '#button-after-grid', - ) as HTMLElement; + it('should track focus on cells', async () => { const column = new AgColumn({}, {}, 'name', true); - - (agGrid.api.getAllDisplayedColumns as jasmine.Spy).and.returnValue([ - column, - ]); - (agGrid.api.getDisplayedRowAtIndex as jasmine.Spy).and.returnValue({}); - (agGrid.api.ensureColumnVisible as jasmine.Spy).and.stub(); + const focusGridInnerElement = agGrid.gridOptions?.focusGridInnerElement; + if (focusGridInnerElement) { + expect( + focusGridInnerElement({ + context: agGrid.gridOptions?.context, + api: agGrid.api, + } as FocusGridInnerElementParams), + ).toBeFalse(); + } else { + expect(focusGridInnerElement).toBeTruthy(); + return; + } agGrid.cellFocused.next({ rowIndex: null, column: null, + context: agGrid.gridOptions?.context, } as unknown as CellFocusedEvent); agGrid.cellFocused.next({ rowIndex: 0, column, + context: agGrid.gridOptions?.context, } as unknown as CellFocusedEvent); agGrid.cellFocused.next({ rowIndex: 0, column: 'name', + context: agGrid.gridOptions?.context, } as unknown as CellFocusedEvent); - focusOnAnchor(afterAnchorEl, afterButtonEl); - expect(agGrid.api.setFocusedHeader).not.toHaveBeenCalled(); - expect(agGrid.api.getDisplayedRowAtIndex).toHaveBeenCalledWith(0); - expect(agGrid.api.ensureIndexVisible).toHaveBeenCalledWith(0, 'top'); - expect(agGrid.api.setFocusedCell).toHaveBeenCalledWith(0, 'name'); + expect(agGrid.gridOptions?.context?.lastFocusedCell).toEqual({ + rowIndex: 0, + column: 'name', + }); + expect( + focusGridInnerElement({ + context: agGrid.gridOptions?.context, + api: agGrid.api, + } as FocusGridInnerElementParams), + ).toBeTrue(); }); }); }); diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts index ce1e58a7c8..6f2fb9f70d 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid-wrapper.component.ts @@ -23,11 +23,8 @@ import { import { AgGridAngular } from 'ag-grid-angular'; import { - AgColumn, CellEditingStartedEvent, CellFocusedEvent, - Column, - ColumnGroup, DetailGridInfo, HeaderFocusedEvent, ModuleNames, @@ -135,9 +132,6 @@ export class SkyAgGridWrapperComponent readonly #mutationObserverService = inject(SkyMutationObserverService); readonly #isCompact = new BehaviorSubject(false); readonly #hasEditableClass = new ReplaySubject(1); - #lastFocusedCell: - | { rowIndex: number | 'header'; column: Column | ColumnGroup | string } - | undefined; constructor() { idIndex++; @@ -212,23 +206,25 @@ export class SkyAgGridWrapperComponent this.agGrid.cellFocused .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((event: CellFocusedEvent) => { - this.#lastFocusedCell = - Number.isInteger(event.rowIndex) && event.column - ? { - rowIndex: event.rowIndex as number, - column: event.column, - } - : undefined; + event.context ??= {}; + event.context['lastFocusedCell'] = { + rowIndex: event.rowIndex, + column: + typeof event.column === 'object' + ? event.column?.getColId() + : `${event.column}`, + }; }); this.agGrid.headerFocused .pipe(takeUntil(this.#ngUnsubscribe)) .subscribe((event: HeaderFocusedEvent) => { - this.#lastFocusedCell = event.column - ? { - rowIndex: 'header', - column: event.column, - } - : undefined; + event.context ??= {}; + event.context['lastFocusedCell'] = { + rowIndex: null, + column: event.column?.getUniqueId + ? event.column.getUniqueId() + : `${event.column}`, + }; }); } } @@ -306,32 +302,9 @@ export class SkyAgGridWrapperComponent relatedTarget && this.#elementRef.nativeElement.contains(relatedTarget); if (this.agGrid && !previousWasGrid) { - const displayedColumns = this.agGrid.api.getAllDisplayedColumns(); - const focusOn = this.#lastFocusedCell ?? { - column: displayedColumns?.[0]?.getColId(), - rowIndex: 'header', - }; - if ( - focusOn.column && - displayedColumns.some((col) => col.getId() === focusOn.column) - ) { - if ( - focusOn.column instanceof AgColumn || - typeof focusOn.column === 'string' - ) { - this.agGrid.api.ensureColumnVisible(focusOn.column); - } - if (focusOn.rowIndex === 'header') { - this.agGrid.api.setFocusedHeader(focusOn.column); - } else if ( - this.agGrid.api.getDisplayedRowAtIndex(focusOn.rowIndex) && - (focusOn.column instanceof AgColumn || - typeof focusOn.column === 'string') - ) { - this.agGrid.api.ensureIndexVisible(focusOn.rowIndex, 'top'); - this.agGrid.api.setFocusedCell(focusOn.rowIndex, focusOn.column); - } - } + this.#elementRef.nativeElement + .querySelector('.ag-tab-guard.ag-tab-guard-top') + ?.focus(); } } diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts index 5f0168305b..c07804c67d 100644 --- a/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/ag-grid.service.ts @@ -40,6 +40,7 @@ import { SkyAgGridLoadingComponent } from './loading/loading.component'; import { SkyCellClass } from './types/cell-class'; import { SkyCellType } from './types/cell-type'; import { SkyHeaderClass } from './types/header-class'; +import { LastFocusedCell } from './types/last-focused.cell'; import { SkyGetGridOptionsArgs } from './types/sky-grid-options'; function autocompleteComparator( @@ -470,6 +471,23 @@ export class SkyAgGridService implements OnDestroy { 'sky-ag-grid-cell-renderer-validator-tooltip': SkyAgGridCellRendererValidatorTooltipComponent, }, + focusGridInnerElement: (params) => { + const lastFocusedCell = params.context['lastFocusedCell'] as + | LastFocusedCell + | undefined; + if (lastFocusedCell) { + if (lastFocusedCell.rowIndex !== null) { + params.api.setFocusedCell( + lastFocusedCell.rowIndex, + lastFocusedCell.column, + ); + } else { + params.api.setFocusedHeader(lastFocusedCell.column); + } + return true; + } + return false; + }, getRowId: (params) => { const dataId = params.data.id; if (dataId !== undefined) { diff --git a/libs/components/ag-grid/src/lib/modules/ag-grid/types/last-focused.cell.ts b/libs/components/ag-grid/src/lib/modules/ag-grid/types/last-focused.cell.ts new file mode 100644 index 0000000000..753e1fed3d --- /dev/null +++ b/libs/components/ag-grid/src/lib/modules/ag-grid/types/last-focused.cell.ts @@ -0,0 +1,4 @@ +export interface LastFocusedCell { + rowIndex: number | null; + column: string; +} From 63d78d43e559c604ce23d86850e3977830c0fa84 Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 19 Sep 2024 14:51:39 -0400 Subject: [PATCH 3/8] fix(components/ag-grid): non-resizeable columns show a column divider but do not show a resize cursor or hover indicator (#2757) --- apps/code-examples/src/app/app.component.html | 30 ++-- apps/code-examples/src/app/app.component.scss | 4 + apps/code-examples/src/app/app.component.ts | 4 + apps/code-examples/src/app/app.module.ts | 8 +- .../theme-selector-spacing-value.ts | 3 + .../theme-selector/theme-selector-value.ts | 1 + .../theme-selector.component.html | 30 ++++ .../theme-selector.component.ts | 151 ++++++++++++++++++ .../ag-grid/src/lib/styles/_base.scss | 13 ++ .../ag-grid/src/lib/styles/_modern.scss | 37 ++--- 10 files changed, 251 insertions(+), 30 deletions(-) create mode 100644 apps/code-examples/src/app/shared/theme-selector/theme-selector-spacing-value.ts create mode 100644 apps/code-examples/src/app/shared/theme-selector/theme-selector-value.ts create mode 100644 apps/code-examples/src/app/shared/theme-selector/theme-selector.component.html create mode 100644 apps/code-examples/src/app/shared/theme-selector/theme-selector.component.ts diff --git a/apps/code-examples/src/app/app.component.html b/apps/code-examples/src/app/app.component.html index 12182120ce..517e0b0c39 100644 --- a/apps/code-examples/src/app/app.component.html +++ b/apps/code-examples/src/app/app.component.html @@ -1,11 +1,11 @@ -@if (router.url !== '/') { -
    -
    +
    +
    + @if (!isHome()) { -
    + }
    -} + +
    - +
    + +
    diff --git a/apps/code-examples/src/app/app.component.scss b/apps/code-examples/src/app/app.component.scss index b260cf8135..dc42a12132 100644 --- a/apps/code-examples/src/app/app.component.scss +++ b/apps/code-examples/src/app/app.component.scss @@ -3,6 +3,10 @@ margin: 5px; } +#content { + overflow-y: auto; +} + #controls { position: sticky; top: 0; diff --git a/apps/code-examples/src/app/app.component.ts b/apps/code-examples/src/app/app.component.ts index 767af5402a..80816d2a6a 100644 --- a/apps/code-examples/src/app/app.component.ts +++ b/apps/code-examples/src/app/app.component.ts @@ -35,4 +35,8 @@ export class AppComponent { themeSvc.init(document.body, renderer, themeSettings); } + + public isHome(): boolean { + return this.router.url === '/'; + } } diff --git a/apps/code-examples/src/app/app.module.ts b/apps/code-examples/src/app/app.module.ts index 00210979ee..fbe667021c 100644 --- a/apps/code-examples/src/app/app.module.ts +++ b/apps/code-examples/src/app/app.module.ts @@ -5,10 +5,16 @@ import { SkyThemeService } from '@skyux/theme'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +import { SkyThemeSelectorComponent } from './shared/theme-selector/theme-selector.component'; @NgModule({ declarations: [AppComponent], - imports: [AppRoutingModule, BrowserAnimationsModule, BrowserModule], + imports: [ + AppRoutingModule, + BrowserAnimationsModule, + BrowserModule, + SkyThemeSelectorComponent, + ], providers: [SkyThemeService], bootstrap: [AppComponent], }) diff --git a/apps/code-examples/src/app/shared/theme-selector/theme-selector-spacing-value.ts b/apps/code-examples/src/app/shared/theme-selector/theme-selector-spacing-value.ts new file mode 100644 index 0000000000..775732415b --- /dev/null +++ b/apps/code-examples/src/app/shared/theme-selector/theme-selector-spacing-value.ts @@ -0,0 +1,3 @@ +import { SkyThemeSpacing } from '@skyux/theme'; + +export type ThemeSelectorSpacingValue = keyof typeof SkyThemeSpacing.presets; diff --git a/apps/code-examples/src/app/shared/theme-selector/theme-selector-value.ts b/apps/code-examples/src/app/shared/theme-selector/theme-selector-value.ts new file mode 100644 index 0000000000..34db729dff --- /dev/null +++ b/apps/code-examples/src/app/shared/theme-selector/theme-selector-value.ts @@ -0,0 +1 @@ +export type ThemeSelectorValue = 'default' | 'modern-light' | 'modern-dark'; diff --git a/apps/code-examples/src/app/shared/theme-selector/theme-selector.component.html b/apps/code-examples/src/app/shared/theme-selector/theme-selector.component.html new file mode 100644 index 0000000000..5d712181a5 --- /dev/null +++ b/apps/code-examples/src/app/shared/theme-selector/theme-selector.component.html @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/apps/code-examples/src/app/shared/theme-selector/theme-selector.component.ts b/apps/code-examples/src/app/shared/theme-selector/theme-selector.component.ts new file mode 100644 index 0000000000..3b89d610b7 --- /dev/null +++ b/apps/code-examples/src/app/shared/theme-selector/theme-selector.component.ts @@ -0,0 +1,151 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { SkyIdModule } from '@skyux/core'; +import { SkyInputBoxModule } from '@skyux/forms'; +import { + SkyTheme, + SkyThemeMode, + SkyThemeService, + SkyThemeSettings, + SkyThemeSpacing, +} from '@skyux/theme'; + +import { ThemeSelectorSpacingValue } from './theme-selector-spacing-value'; +import { ThemeSelectorValue } from './theme-selector-value'; + +interface LocalStorageSettings { + themeName: ThemeSelectorValue; + themeSpacing: ThemeSelectorSpacingValue; +} + +const PREVIOUS_SETTINGS_KEY = 'skyux-playground-theme-selector-settings'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'sky-theme-selector', + standalone: true, + imports: [FormsModule, SkyIdModule, SkyInputBoxModule], + templateUrl: './theme-selector.component.html', +}) +export class SkyThemeSelectorComponent implements OnInit { + public set themeName(value: ThemeSelectorValue) { + const previousThemeName = this.#_themeName; + this.#_themeName = value; + + if (value !== previousThemeName) { + this.#updateThemeSettings(); + } + } + + public get themeName(): ThemeSelectorValue { + return this.#_themeName; + } + + public set themeSpacing(value: ThemeSelectorSpacingValue) { + const previous = this.#_themeSpacing; + this.#_themeSpacing = value; + + if (value !== previous) { + this.#updateThemeSettings(); + } + } + + public get themeSpacing(): ThemeSelectorSpacingValue { + return this.#_themeSpacing; + } + + protected spacingValues: ThemeSelectorSpacingValue[] = []; + + #_themeName: ThemeSelectorValue = 'default'; + #_themeSpacing: ThemeSelectorSpacingValue = 'standard'; + + #themeSvc = inject(SkyThemeService); + #currentThemeSettings: SkyThemeSettings | undefined; + + public ngOnInit(): void { + const previousSettings = this.#getLastSettings(); + + if (previousSettings) { + try { + this.themeName = previousSettings.themeName; + this.themeSpacing = previousSettings.themeSpacing; + } catch { + // Bad settings. + } + } + + this.#themeSvc.settingsChange.subscribe((settingsChange) => { + const settings = settingsChange.currentSettings; + + if (settings.theme === SkyTheme.presets.modern) { + this.themeName = + settings.mode === SkyThemeMode.presets.dark + ? 'modern-dark' + : 'modern-light'; + + this.themeSpacing = settings.spacing.name as ThemeSelectorSpacingValue; + } else { + this.themeName = 'default'; + this.themeSpacing = 'standard'; + } + + this.#currentThemeSettings = settings; + this.#updateSpacingOptions(); + }); + } + + #updateSpacingOptions(): void { + if (this.#currentThemeSettings) { + this.spacingValues = + this.#currentThemeSettings.theme.supportedSpacing.map( + (spacing) => spacing.name as ThemeSelectorSpacingValue, + ); + } + } + + #updateThemeSettings(): void { + const themeSpacing = SkyThemeSpacing.presets[this.themeSpacing]; + + let theme: SkyTheme; + let themeMode = SkyThemeMode.presets.light; + + switch (this.themeName) { + case 'modern-light': + theme = SkyTheme.presets.modern; + break; + case 'modern-dark': + theme = SkyTheme.presets.modern; + themeMode = SkyThemeMode.presets.dark; + break; + default: + theme = SkyTheme.presets.default; + break; + } + + this.#themeSvc.setTheme( + new SkyThemeSettings(theme, themeMode, themeSpacing), + ); + + this.#saveSettings({ + themeName: this.themeName, + themeSpacing: this.themeSpacing, + }); + } + + #getLastSettings(): LocalStorageSettings | undefined { + try { + return JSON.parse(localStorage.getItem(PREVIOUS_SETTINGS_KEY)!); + } catch { + // Local storage is disabled or settings are invalid. + return undefined; + } + } + + #saveSettings(settings: LocalStorageSettings): void { + try { + localStorage.setItem(PREVIOUS_SETTINGS_KEY, JSON.stringify(settings)); + } catch { + // Local storage is disabled. + } + } +} diff --git a/libs/components/ag-grid/src/lib/styles/_base.scss b/libs/components/ag-grid/src/lib/styles/_base.scss index d7976852a6..625ec6103f 100644 --- a/libs/components/ag-grid/src/lib/styles/_base.scss +++ b/libs/components/ag-grid/src/lib/styles/_base.scss @@ -153,6 +153,19 @@ $modernDarkThemeCompact: map.merge($modernDarkThemeBase, $modernThemeCompact); .ag-theme-sky-data-entry-grid-modern-dark-compact { @include ag-grid-extra.ag-grid-extra(); + .ag-header-cell { + cursor: initial; + + &:not(.sky-ag-grid-header-resizable) { + .ag-header-cell-resize { + cursor: initial; + // We want to always show our dividers that use the resize cell. The AG Grid style on non-resizable cells uses `!important` so we have to do so as well. + display: block !important; + } + } + } + + .ag-header-cell-sortable, .ag-header-cell-sortable .ag-header-cell-label { cursor: initial; } diff --git a/libs/components/ag-grid/src/lib/styles/_modern.scss b/libs/components/ag-grid/src/lib/styles/_modern.scss index 4ccaef5162..6283ae5235 100644 --- a/libs/components/ag-grid/src/lib/styles/_modern.scss +++ b/libs/components/ag-grid/src/lib/styles/_modern.scss @@ -12,24 +12,25 @@ color: var(--sky-text-color-default); } - .ag-header-cell.sky-ag-grid-header-resizable:not( - .ag-column-resizing, - :has(> .ag-header-cell-resize:hover) - ) { - --ag-header-column-resize-handle-color: var( - --sky-border-color-neutral-medium-dark - ); - --ag-header-column-resize-handle-height: 15px; - - .ag-header-cell-resize::after { - mask-size: 1px 2px; - -webkit-mask-size: 1px 2px; - mask-image: url('data:image/gif;base64,R0lGODlhAQACAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAIAAAICDAoAOw=='); - -webkit-mask-image: url('data:image/gif;base64,R0lGODlhAQACAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAIAAAICDAoAOw=='); - mask-repeat: repeat; - -webkit-mask-repeat: repeat; - mask-position: top; - -webkit-mask-position: top; + .ag-header-cell { + &:not( + .sky-ag-grid-header-resizable.ag-column-resizing, + .sky-ag-grid-header-resizable:has(> .ag-header-cell-resize:hover) + ) { + --ag-header-column-resize-handle-color: var( + --sky-border-color-neutral-medium-dark + ); + --ag-header-column-resize-handle-height: 15px; + .ag-header-cell-resize::after { + mask-size: 1px 2px; + -webkit-mask-size: 1px 2px; + mask-image: url('data:image/gif;base64,R0lGODlhAQACAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAIAAAICDAoAOw=='); + -webkit-mask-image: url('data:image/gif;base64,R0lGODlhAQACAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAIAAAICDAoAOw=='); + mask-repeat: repeat; + -webkit-mask-repeat: repeat; + mask-position: top; + -webkit-mask-position: top; + } } } From 708e754a7a67ab4c84e05809c50ff52160b8f849 Mon Sep 17 00:00:00 2001 From: Steve Brush Date: Thu, 19 Sep 2024 15:29:17 -0400 Subject: [PATCH 4/8] fix: update JSDocs to communicate that inline help features require a value for `labelText` (#2745) (#2751) --- .../modules/colorpicker/colorpicker.component.ts | 6 +++--- .../date-range-picker.component.ts | 6 +++--- .../modules/checkbox/checkbox-group.component.ts | 8 ++++---- .../lib/modules/checkbox/checkbox.component.ts | 6 +++--- .../modules/field-group/field-group.component.ts | 8 ++++---- .../file-attachment/file-attachment.component.ts | 6 +++--- .../file-drop/file-drop.component.ts | 6 +++--- .../fixtures/input-box.component.fixture.html | 2 +- .../fixtures/input-box.component.fixture.ts | 3 +++ .../modules/input-box/input-box.component.html | 3 +-- .../modules/input-box/input-box.component.spec.ts | 15 +++++++++++++++ .../lib/modules/input-box/input-box.component.ts | 8 ++++---- .../lib/modules/radio/radio-group.component.ts | 6 +++--- .../src/lib/modules/radio/radio.component.ts | 6 +++--- .../toggle-switch/toggle-switch.component.ts | 6 +++--- .../layout/src/lib/modules/box/box.component.ts | 8 ++++---- .../src/lib/modules/modal/modal.component.ts | 8 ++++---- .../progress-indicator-item.component.ts | 7 ++++--- .../modules/text-editor/text-editor.component.ts | 6 +++--- .../src/lib/modules/tiles/tile/tile.component.ts | 7 ++++--- 20 files changed, 75 insertions(+), 56 deletions(-) diff --git a/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.ts b/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.ts index 360e07d14c..2d838b1889 100644 --- a/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.ts +++ b/libs/components/colorpicker/src/lib/modules/colorpicker/colorpicker.component.ts @@ -130,9 +130,9 @@ export class SkyColorpickerComponent public labelHidden = false; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the colorpicker label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; @@ -140,7 +140,7 @@ export class SkyColorpickerComponent /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the colorpicker label. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; diff --git a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts index 4242b78abc..1a679643bf 100644 --- a/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts +++ b/libs/components/datetime/src/lib/modules/date-range-picker/date-range-picker.component.ts @@ -188,7 +188,7 @@ export class SkyDateRangePickerComponent /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to date range picker. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -242,9 +242,9 @@ export class SkyDateRangePickerComponent public stacked = false; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the date range picker label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts index 5382f0c5d2..7bc01bd168 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox-group.component.ts @@ -54,9 +54,9 @@ function numberAttribute4(value: unknown): number { }) export class SkyCheckboxGroupComponent implements Validator { /** - * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the checkbox group fieldset legend. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `headingText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -130,9 +130,9 @@ export class SkyCheckboxGroupComponent implements Validator { } /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the checkbox group heading. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `headingText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts index c2a6767b6d..be63aaa178 100644 --- a/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts +++ b/libs/components/forms/src/lib/modules/checkbox/checkbox.component.ts @@ -156,7 +156,7 @@ export class SkyCheckboxComponent implements ControlValueAccessor, Validator { /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the checkbox label. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -284,9 +284,9 @@ export class SkyCheckboxComponent implements ControlValueAccessor, Validator { public stacked = false; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the checkbox label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/field-group/field-group.component.ts b/libs/components/forms/src/lib/modules/field-group/field-group.component.ts index e61d7ee3cc..55a64323bd 100644 --- a/libs/components/forms/src/lib/modules/field-group/field-group.component.ts +++ b/libs/components/forms/src/lib/modules/field-group/field-group.component.ts @@ -72,9 +72,9 @@ export class SkyFieldGroupComponent { } /** - * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the field group heading. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `headingText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -87,9 +87,9 @@ export class SkyFieldGroupComponent { public helpPopoverTitle: string | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the field group heading. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `headingText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-attachment/file-attachment.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-attachment/file-attachment.component.ts index bb2cec29af..dc7e5123b0 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-attachment/file-attachment.component.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-attachment/file-attachment.component.ts @@ -118,7 +118,7 @@ export class SkyFileAttachmentComponent /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the single file attachment label. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -131,9 +131,9 @@ export class SkyFileAttachmentComponent public helpPopoverTitle: string | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the single file attachment label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts index ec88bc42ea..5114ba96e5 100644 --- a/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts +++ b/libs/components/forms/src/lib/modules/file-attachment/file-drop/file-drop.component.ts @@ -203,7 +203,7 @@ export class SkyFileDropComponent implements OnDestroy { /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the file attachment label. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -224,9 +224,9 @@ export class SkyFileDropComponent implements OnDestroy { public stacked = false; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the file attachment label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html b/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html index d72ffb5f42..a8b7933077 100644 --- a/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html +++ b/libs/components/forms/src/lib/modules/input-box/fixtures/input-box.component.fixture.html @@ -570,11 +570,11 @@
    - @if (helpPopoverContent || helpKey) { + @if ((helpPopoverContent || helpKey) && labelText) { } - diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts index fe10d63aef..2776413a70 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.spec.ts @@ -699,6 +699,21 @@ describe('Input box component', () => { await validateHelpInline(fixture, 'Help content from template'); }); + it('should not render help inline button if labelText undefined', async () => { + const fixture = TestBed.createComponent(InputBoxFixtureComponent); + fixture.detectChanges(); + + fixture.componentRef.setInput('labelText', undefined); + fixture.componentInstance.easyModeHelpPopoverContent = "What's this?"; + fixture.detectChanges(); + + const easyModeInput = getDefaultEls(fixture, 'input-easy-mode'); + + expect( + easyModeInput.inlineHelpEl?.querySelector('.sky-help-inline'), + ).toBeUndefined(); + }); + it('should render help inline with help key', async () => { const fixture = TestBed.createComponent(InputBoxFixtureComponent); fixture.detectChanges(); diff --git a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts index bc16503fe3..2ef32a7c24 100644 --- a/libs/components/forms/src/lib/modules/input-box/input-box.component.ts +++ b/libs/components/forms/src/lib/modules/input-box/input-box.component.ts @@ -131,17 +131,17 @@ export class SkyInputBoxComponent public helpPopoverTitle: string | undefined; /** - * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the input box label. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the input box label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.ts b/libs/components/forms/src/lib/modules/radio/radio-group.component.ts index 14ea8d4a11..bcfc004beb 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.ts +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.ts @@ -237,7 +237,7 @@ export class SkyRadioGroupComponent implements AfterContentInit, OnDestroy { /** * The content of the help popover. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to radio group. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `headingText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -250,9 +250,9 @@ export class SkyRadioGroupComponent implements AfterContentInit, OnDestroy { public helpPopoverTitle: string | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the radio group heading. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `headingText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/radio/radio.component.ts b/libs/components/forms/src/lib/modules/radio/radio.component.ts index 12813d545f..03684e6aae 100644 --- a/libs/components/forms/src/lib/modules/radio/radio.component.ts +++ b/libs/components/forms/src/lib/modules/radio/radio.component.ts @@ -231,7 +231,7 @@ export class SkyRadioComponent implements OnDestroy, ControlValueAccessor { /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to radio button. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -294,9 +294,9 @@ export class SkyRadioComponent implements OnDestroy, ControlValueAccessor { public hintText: string | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the radio button label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts b/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts index a9f7fc83a0..d3a3748ace 100644 --- a/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts +++ b/libs/components/forms/src/lib/modules/toggle-switch/toggle-switch.component.ts @@ -113,7 +113,7 @@ export class SkyToggleSwitchComponent /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the toggle switch. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -145,9 +145,9 @@ export class SkyToggleSwitchComponent public labelHidden = false; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the toggle switch label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/layout/src/lib/modules/box/box.component.ts b/libs/components/layout/src/lib/modules/box/box.component.ts index 535ccc3dc4..4d0462b6a5 100644 --- a/libs/components/layout/src/lib/modules/box/box.component.ts +++ b/libs/components/layout/src/lib/modules/box/box.component.ts @@ -79,9 +79,9 @@ export class SkyBoxComponent { } /** - * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the box heading. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `headingText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -94,9 +94,9 @@ export class SkyBoxComponent { public helpPopoverTitle: string | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the box heading. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `headingText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/modals/src/lib/modules/modal/modal.component.ts b/libs/components/modals/src/lib/modules/modal/modal.component.ts index 51af8d7004..0edf97a7d0 100644 --- a/libs/components/modals/src/lib/modules/modal/modal.component.ts +++ b/libs/components/modals/src/lib/modules/modal/modal.component.ts @@ -86,16 +86,16 @@ export class SkyModalComponent implements AfterViewInit, OnDestroy, OnInit { public headingText: string | undefined; /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is - * added to the modal header. Clicking the button invokes global help as configured by the application. + * A help key that identifies the global help content to display. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is + * added to the modal header. Clicking the button invokes global help as configured by the application. This property only applies when `headingText` is also specified. */ @Input() public helpKey: string | undefined; /** - * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `headingText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the modal header. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `headingText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; diff --git a/libs/components/progress-indicator/src/lib/modules/progress-indicator/progress-indicator-item/progress-indicator-item.component.ts b/libs/components/progress-indicator/src/lib/modules/progress-indicator/progress-indicator-item/progress-indicator-item.component.ts index 717946ac78..754eb33d77 100644 --- a/libs/components/progress-indicator/src/lib/modules/progress-indicator/progress-indicator-item/progress-indicator-item.component.ts +++ b/libs/components/progress-indicator/src/lib/modules/progress-indicator/progress-indicator-item/progress-indicator-item.component.ts @@ -38,16 +38,17 @@ export class SkyProgressIndicatorItemComponent implements OnInit { } /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is + * A help key that identifies the global help content to display. When specified along with `title`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is * placed beside the progress indicator item label. Clicking the button invokes global help as configured by the application. + * This property only applies when `title` is also specified. */ @Input() public helpKey: string | undefined; /** - * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `title`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the progress indicator item label. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `title` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; diff --git a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts index 0a128e3e15..eee5315d20 100644 --- a/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts +++ b/libs/components/text-editor/src/lib/modules/text-editor/text-editor.component.ts @@ -163,7 +163,7 @@ export class SkyTextEditorComponent /** * The content of the help popover. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the text editor. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `labelText` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; @@ -309,9 +309,9 @@ export class SkyTextEditorComponent } /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * A help key that identifies the global help content to display. When specified along with `labelText`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is placed beside the text editor label. Clicking the button invokes [global help](https://developer.blackbaud.com/skyux/learn/develop/global-help) - * as configured by the application. + * as configured by the application. This property only applies when `labelText` is also specified. */ @Input() public helpKey: string | undefined; diff --git a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts index 0d8b5513c4..fc149a599b 100644 --- a/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts +++ b/libs/components/tiles/src/lib/modules/tiles/tile/tile.component.ts @@ -61,16 +61,17 @@ import { SkyTileTitleComponent } from './tile-title.component'; }) export class SkyTileComponent implements OnChanges, OnDestroy { /** - * A help key that identifies the global help content to display. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is + * A help key that identifies the global help content to display. When specified along with `tileName`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) button is * added to the tile header. Clicking the button invokes global help as configured by the application. + * This property only applies when `tileName` is also specified. */ @Input() public helpKey: string | undefined; /** - * The content of the help popover. When specified, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) + * The content of the help popover. When specified along with `tileName`, a [help inline](https://developer.blackbaud.com/skyux/components/help-inline) * button is added to the tile header. The help inline button displays a [popover](https://developer.blackbaud.com/skyux/components/popover) - * when clicked using the specified content and optional title. + * when clicked using the specified content and optional title. This property only applies when `tileName` is also specified. */ @Input() public helpPopoverContent: string | TemplateRef | undefined; From b2116d35eaaa9ed092702b52896bfc9038f04534 Mon Sep 17 00:00:00 2001 From: Blackbaud Sky Build User Date: Thu, 19 Sep 2024 15:33:51 -0400 Subject: [PATCH 5/8] chore: release 11.3.0 (#2753) --- CHANGELOG.md | 16 ++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53778bb713..a128c4df26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## [11.3.0](https://github.com/blackbaud/skyux/compare/11.2.0...11.3.0) (2024-09-19) + + +### Features + +* **components/ag-grid:** support changing the tab focus behavior ([#2748](https://github.com/blackbaud/skyux/issues/2748)) ([e63f738](https://github.com/blackbaud/skyux/commit/e63f73844be2b300df6e8e3ba4dd91a1bba7aaa9)) + + +### Bug Fixes + +* **components/ag-grid:** non-resizeable columns show a column divider but do not show a resize cursor or hover indicator ([#2757](https://github.com/blackbaud/skyux/issues/2757)) ([63d78d4](https://github.com/blackbaud/skyux/commit/63d78d43e559c604ce23d86850e3977830c0fa84)) +* **components/ag-grid:** sort direction button is not visible when sorting is not active on a column ([#2743](https://github.com/blackbaud/skyux/issues/2743)) ([bb7c016](https://github.com/blackbaud/skyux/commit/bb7c01636c44fbe1ae4f3c108d9f84917d16b187)) +* **sdk/eslint-config:** alternatively look for `angular-eslint` dependency ([#2755](https://github.com/blackbaud/skyux/issues/2755)) ([e1fd53c](https://github.com/blackbaud/skyux/commit/e1fd53c531d0985813c78625a6e8ecf8c3038552)) +* update `@typescript-eslint/eslint-plugin` to use deprecated rule ([#2756](https://github.com/blackbaud/skyux/issues/2756)) ([a4ab15a](https://github.com/blackbaud/skyux/commit/a4ab15a40a3623c6c2dcf500ba77bad3635b4c18)) +* update JSDocs to communicate that inline help features require a value for `labelText` ([#2745](https://github.com/blackbaud/skyux/issues/2745)) ([#2751](https://github.com/blackbaud/skyux/issues/2751)) ([708e754](https://github.com/blackbaud/skyux/commit/708e754a7a67ab4c84e05809c50ff52160b8f849)) + ## [11.2.0](https://github.com/blackbaud/skyux/compare/11.1.0...11.2.0) (2024-09-17) diff --git a/package-lock.json b/package-lock.json index 86e9ce9e0c..14e6c32d29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skyux", - "version": "11.2.0", + "version": "11.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skyux", - "version": "11.2.0", + "version": "11.3.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d7e8d48b27..78f4f45eab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skyux", - "version": "11.2.0", + "version": "11.3.0", "license": "MIT", "scripts": { "ng": "nx", From ed3f5189c6e7ed1706b8d475daa344a13a6d67eb Mon Sep 17 00:00:00 2001 From: Trevor Burch Date: Thu, 19 Sep 2024 16:40:43 -0400 Subject: [PATCH 6/8] feat(components/pages): add `helpKey` input to the page component (#2739) --- .../demo.component.html | 2 +- .../demo.component.spec.ts | 16 ++++++++-- .../demo.component.html | 2 +- .../demo.component.spec.ts | 21 ++++++++++++-- .../demo.component.html | 2 +- .../demo.component.spec.ts | 21 ++++++++++++-- .../demo.component.html | 2 +- .../demo.component.spec.ts | 21 ++++++++++++-- .../demo.component.html | 2 +- .../demo.component.spec.ts | 21 ++++++++++++-- .../lib/modules/page/page.component.spec.ts | 29 +++++++++++++++++-- .../src/lib/modules/page/page.component.ts | 17 ++++++++++- 12 files changed, 138 insertions(+), 18 deletions(-) diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.html b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.html index d9a42a34b7..7346e609a0 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.html +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.html @@ -1,4 +1,4 @@ - + diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts index 09a41eb22b..5b47bac0e2 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-list-layout-demo/demo.component.spec.ts @@ -1,6 +1,10 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; @@ -9,18 +13,20 @@ describe('List page list layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; + helpController: SkyHelpTestingController; }> { const fixture = TestBed.createComponent(DemoComponent); const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); - return { pageHarness, fixture }; + return { pageHarness, fixture, helpController }; } beforeEach(() => { TestBed.configureTestingModule({ - imports: [DemoComponent, NoopAnimationsModule], + imports: [DemoComponent, SkyHelpTestingModule, NoopAnimationsModule], }); }); @@ -39,4 +45,10 @@ describe('List page list layout demo', () => { 'Dashboards', ); }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('demo-help'); + }); }); diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.html b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.html index 1a0762aac3..8fa3b40a28 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.html +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.html @@ -1,4 +1,4 @@ - + diff --git a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts index ceaa921f10..5ffae40dc7 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/list-page-tabs-layout-demo/demo.component.spec.ts @@ -2,6 +2,10 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; @@ -10,18 +14,25 @@ describe('List page tabs layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; + helpController: SkyHelpTestingController; }> { const fixture = TestBed.createComponent(DemoComponent); const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); - return { pageHarness, fixture }; + return { pageHarness, fixture, helpController }; } beforeEach(() => { TestBed.configureTestingModule({ - imports: [DemoComponent, NoopAnimationsModule, RouterTestingModule], + imports: [ + DemoComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + RouterTestingModule, + ], }); }); @@ -40,4 +51,10 @@ describe('List page tabs layout demo', () => { 'Contacts', ); }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('demo-help'); + }); }); diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.html b/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.html index aac593a941..cf30425555 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.html +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-blocks-layout-demo/demo.component.html @@ -1,4 +1,4 @@ - + + diff --git a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts index 709a51bf58..7b924c167f 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts +++ b/apps/code-examples/src/app/code-examples/pages/page/record-page-tabs-layout-demo/demo.component.spec.ts @@ -2,6 +2,10 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; +import { + SkyHelpTestingController, + SkyHelpTestingModule, +} from '@skyux/core/testing'; import { SkyPageHarness } from '@skyux/pages/testing'; import { DemoComponent } from './demo.component'; @@ -10,18 +14,25 @@ describe('Record page tabs layout demo', () => { async function setupTest(): Promise<{ pageHarness: SkyPageHarness; fixture: ComponentFixture; + helpController: SkyHelpTestingController; }> { const fixture = TestBed.createComponent(DemoComponent); const loader = TestbedHarnessEnvironment.documentRootLoader(fixture); const pageHarness = await loader.getHarness(SkyPageHarness); + const helpController = TestBed.inject(SkyHelpTestingController); - return { pageHarness, fixture }; + return { pageHarness, fixture, helpController }; } beforeEach(() => { TestBed.configureTestingModule({ - imports: [DemoComponent, NoopAnimationsModule, RouterTestingModule], + imports: [ + DemoComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + RouterTestingModule, + ], }); }); @@ -42,4 +53,10 @@ describe('Record page tabs layout demo', () => { 'Charlene Conners', ); }); + + it('should have the correct help key', async () => { + const { helpController } = await setupTest(); + + helpController.expectCurrentHelpKey('demo-help'); + }); }); diff --git a/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.html b/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.html index d05ceb422e..f13c289d88 100644 --- a/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.html +++ b/apps/code-examples/src/app/code-examples/pages/page/split-view-page-fit-layout-demo/demo.component.html @@ -1,4 +1,4 @@ - + } @if (errors?.['skyDate'] && errors?.['skyDate']['maxDate']) { } @@ -59,14 +65,22 @@ @if (errors?.['skyFuzzyDate'] && errors?.['skyFuzzyDate']['maxDate']) { } @if (errors?.['skyFuzzyDate'] && errors?.['skyFuzzyDate']['minDate']) { } diff --git a/libs/components/forms/src/lib/modules/form-error/form-errors.component.spec.ts b/libs/components/forms/src/lib/modules/form-error/form-errors.component.spec.ts index 6803ff27bd..77efb7921a 100644 --- a/libs/components/forms/src/lib/modules/form-error/form-errors.component.spec.ts +++ b/libs/components/forms/src/lib/modules/form-error/form-errors.component.spec.ts @@ -44,12 +44,12 @@ describe('Form errors component', () => { required: true, maxlength: true, minlength: true, - skyDate: { invalid: true, minDate: true, maxDate: true }, + skyDate: { invalid: true, minDate: '01/01/2024', maxDate: '01/01/2022' }, skyFuzzyDate: { futureDisabled: true, invalid: true, - maxDate: true, - minDate: true, + maxDate: '01/2023', + minDate: '01/2024', yearRequired: true, }, skyEmail: true, @@ -100,6 +100,57 @@ describe('Form errors component', () => { }); }); + it('should include the minimum or maximum date in the date error messages', () => { + componentInstance.errors = { + skyDate: { + invalid: true, + maxDate: new Date('01/01/2025'), + maxDateFormatted: '01/01/2025', + minDate: new Date('01/01/2024'), + minDateFormatted: '01/01/2024', + }, + skyFuzzyDate: { + futureDisabled: true, + invalid: true, + maxDate: new Date('01/01/2021'), + maxDateFormatted: '01/01/2021', + minDate: new Date('01/01/2020'), + minDateFormatted: '01/01/2020', + yearRequired: true, + }, + }; + + componentInstance.dirty = true; + componentInstance.touched = true; + fixture.detectChanges(); + + const minDateErrorMessage = fixture.nativeElement.querySelector( + `sky-form-error[errorName='minDate'] .sky-status-indicator-message`, + ); + const maxDateErrorMessage = fixture.nativeElement.querySelector( + `sky-form-error[errorName='maxDate'] .sky-status-indicator-message`, + ); + const fuzzyMinDateErrorMessage = fixture.nativeElement.querySelector( + `sky-form-error[errorName='fuzzyMinDate'] .sky-status-indicator-message`, + ); + const fuzzyMaxDateErrorMessage = fixture.nativeElement.querySelector( + `sky-form-error[errorName='fuzzyMaxDate'] .sky-status-indicator-message`, + ); + + expect(minDateErrorMessage.textContent).toEqual( + 'Select or enter a date on or after 01/01/2024.', + ); + expect(maxDateErrorMessage.textContent).toEqual( + 'Select or enter a date on or before 01/01/2025.', + ); + expect(fuzzyMinDateErrorMessage.textContent).toEqual( + 'Select or enter a date on or after 01/01/2020.', + ); + expect(fuzzyMaxDateErrorMessage.textContent).toEqual( + 'Select or enter a date on or before 01/01/2021.', + ); + }); + it('should render custom errors when there are no known errors and labelText is present regardless of touched or dirty', () => { componentInstance.touched = true; fixture.detectChanges(); diff --git a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts index 786fc6aa56..954114af13 100644 --- a/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts +++ b/libs/components/forms/src/lib/modules/shared/sky-forms-resources.module.ts @@ -29,10 +29,10 @@ const RESOURCES: Record = { }, skyux_form_error_date: { message: 'Select or enter a valid date.' }, skyux_form_error_date_max: { - message: 'Select or enter a date before the max date.', + message: 'Select or enter a date on or before {0}.', }, skyux_form_error_date_min: { - message: 'Select or enter a date after the min date.', + message: 'Select or enter a date on or after {0}.', }, skyux_form_error_fuzzy_date_future_disabled: { message: 'Future dates are disabled, select or enter a date in the past.', @@ -41,10 +41,10 @@ const RESOURCES: Record = { message: 'Select or enter a valid date.', }, skyux_form_error_fuzzy_date_max_date: { - message: 'Select or enter a date before the max date.', + message: 'Select or enter a date on or before {0}.', }, skyux_form_error_fuzzy_date_min_date: { - message: 'Select or enter a date after the min date.', + message: 'Select or enter a date on or after {0}.', }, skyux_form_error_fuzzy_date_year_required: { message: 'Year is required.' }, skyux_form_error_email: { From ca31508a00a258fd5b2b5523d01615c137664b4c Mon Sep 17 00:00:00 2001 From: Corey Archer Date: Fri, 20 Sep 2024 11:34:37 -0400 Subject: [PATCH 8/8] feat(components/forms): add harnesses for radio and radio group components (#2754) --- .../forms/radio/standard/demo.component.html | 1 + .../radio/standard/demo.component.spec.ts | 91 ++++++ .../modules/radio/radio-group.component.html | 2 +- .../modules/radio/radio-group.component.scss | 16 +- .../lib/modules/radio/radio.component.html | 8 +- .../forms/testing/src/public-api.ts | 5 + .../radio-harness-test.component.html | 49 +++ .../fixtures/radio-harness-test.component.ts | 62 ++++ .../src/radio/radio-group-harness-filters.ts | 8 + .../src/radio/radio-group-harness.spec.ts | 297 ++++++++++++++++++ .../testing/src/radio/radio-group-harness.ts | 196 ++++++++++++ .../src/radio/radio-harness-filters.ts | 8 + .../testing/src/radio/radio-harness.spec.ts | 245 +++++++++++++++ .../forms/testing/src/radio/radio-harness.ts | 183 +++++++++++ .../testing/src/radio/radio-label-harness.ts | 21 ++ 15 files changed, 1183 insertions(+), 9 deletions(-) create mode 100644 apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.spec.ts create mode 100644 libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.html create mode 100644 libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.ts create mode 100644 libs/components/forms/testing/src/radio/radio-group-harness-filters.ts create mode 100644 libs/components/forms/testing/src/radio/radio-group-harness.spec.ts create mode 100644 libs/components/forms/testing/src/radio/radio-group-harness.ts create mode 100644 libs/components/forms/testing/src/radio/radio-harness-filters.ts create mode 100644 libs/components/forms/testing/src/radio/radio-harness.spec.ts create mode 100644 libs/components/forms/testing/src/radio/radio-harness.ts create mode 100644 libs/components/forms/testing/src/radio/radio-label-harness.ts diff --git a/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.html b/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.html index bfafc01c76..8e78bd76eb 100644 --- a/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.html +++ b/apps/code-examples/src/app/code-examples/forms/radio/standard/demo.component.html @@ -1,6 +1,7 @@
    { + async function setupTest(options: { + dataSkyId: string; + }): Promise { + const fixture = TestBed.createComponent(DemoComponent); + + const loader = TestbedHarnessEnvironment.loader(fixture); + + const harness = await loader.getHarness( + SkyRadioGroupHarness.with({ dataSkyId: options.dataSkyId }), + ); + + fixture.detectChanges(); + await fixture.whenStable(); + + return harness; + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, DemoComponent], + }); + }); + + it('should have the appropriate heading text/level/style, label text, and hint text', async () => { + const harness = await setupTest({ dataSkyId: 'radio-group' }); + + const radioButtons = await harness.getRadioButtons(); + + await expectAsync(harness.getHeadingText()).toBeResolvedTo( + 'Payment method', + ); + await expectAsync(harness.getHeadingLevel()).toBeResolvedTo(4); + await expectAsync(harness.getHeadingStyle()).toBeResolvedTo(4); + await expectAsync(harness.getHintText()).toBeResolvedTo( + 'Card methods require proof of identification.', + ); + + await expectAsync(radioButtons[0].getLabelText()).toBeResolvedTo('Cash'); + await expectAsync(radioButtons[0].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[1].getLabelText()).toBeResolvedTo('Check'); + await expectAsync(radioButtons[1].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[2].getLabelText()).toBeResolvedTo( + 'Apple pay', + ); + await expectAsync(radioButtons[2].getHintText()).toBeResolvedTo(''); + + await expectAsync(radioButtons[3].getLabelText()).toBeResolvedTo('Credit'); + await expectAsync(radioButtons[3].getHintText()).toBeResolvedTo( + 'A 2% late fee is applied to payments made after the due date.', + ); + + await expectAsync(radioButtons[4].getLabelText()).toBeResolvedTo('Debit'); + await expectAsync(radioButtons[4].getHintText()).toBeResolvedTo(''); + }); + + it('should display an error message when there is a custom validation error', async () => { + const harness = await setupTest({ dataSkyId: 'radio-group' }); + + const radioHarness = (await harness.getRadioButtons())[1]; + + await radioHarness.check(); + + await expectAsync(harness.hasError('processingIssue')).toBeResolvedTo(true); + }); + + it('should show a help popover with the expected text', async () => { + const harness = await setupTest({ + dataSkyId: 'radio-group', + }); + + await harness.clickHelpInline(); + + const helpPopoverTitle = await harness.getHelpPopoverTitle(); + expect(helpPopoverTitle).toBe('Are there fees?'); + + const helpPopoverContent = await harness.getHelpPopoverContent(); + expect(helpPopoverContent).toBe( + `We don't charge fees for any payment method. The only exception is when credit card payments are late, which incurs a 2% fee.`, + ); + }); +}); diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.html b/libs/components/forms/src/lib/modules/radio/radio-group.component.html index 246e7eec62..8f6811b82c 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.html +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.html @@ -36,7 +36,7 @@
    {{ headingText }}
    } @else { - + {{ headingText }} } diff --git a/libs/components/forms/src/lib/modules/radio/radio-group.component.scss b/libs/components/forms/src/lib/modules/radio/radio-group.component.scss index 66100a5b6e..132be2b485 100644 --- a/libs/components/forms/src/lib/modules/radio/radio-group.component.scss +++ b/libs/components/forms/src/lib/modules/radio/radio-group.component.scss @@ -13,9 +13,15 @@ display: flex; } -h3, -h4, -h5 { - margin: 0; - display: inline-block; +legend { + h3, + h4, + h5 { + margin: 0; + display: inline-block; + } + + span { + line-height: 1.1; + } } diff --git a/libs/components/forms/src/lib/modules/radio/radio.component.html b/libs/components/forms/src/lib/modules/radio/radio.component.html index 0a7024dd80..45b0f43c21 100644 --- a/libs/components/forms/src/lib/modules/radio/radio.component.html +++ b/libs/components/forms/src/lib/modules/radio/radio.component.html @@ -35,9 +35,11 @@ } @if (labelText) { - @if (!labelHidden) { - {{ labelText }} - } + {{ labelText }} } @else { } diff --git a/libs/components/forms/testing/src/public-api.ts b/libs/components/forms/testing/src/public-api.ts index 41e7d7224e..d14cbaef51 100644 --- a/libs/components/forms/testing/src/public-api.ts +++ b/libs/components/forms/testing/src/public-api.ts @@ -16,3 +16,8 @@ export { SkyFormErrorHarness } from './form-error/form-error-harness'; export { SkyFormErrorHarnessFilters } from './form-error/form-error-harness.filters'; export { SkyRadioFixture } from './radio-fixture'; +export { SkyRadioGroupHarness } from './radio/radio-group-harness'; +export { SkyRadioGroupHarnessFilters } from './radio/radio-group-harness-filters'; +export { SkyRadioHarness } from './radio/radio-harness'; +export { SkyRadioHarnessFilters } from './radio/radio-harness-filters'; +export { SkyRadioLabelHarness } from './radio/radio-label-harness'; diff --git a/libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.html b/libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.html new file mode 100644 index 0000000000..08e58858ab --- /dev/null +++ b/libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.html @@ -0,0 +1,49 @@ + + + + + @if (!hideCheckLabel) { + Check + } + + + @if (paymentMethod.errors?.['processingIssue']) { + + } + + diff --git a/libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.ts b/libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.ts new file mode 100644 index 0000000000..7a73464e64 --- /dev/null +++ b/libs/components/forms/testing/src/radio/fixtures/radio-harness-test.component.ts @@ -0,0 +1,62 @@ +import { Component, inject } from '@angular/core'; +import { + AbstractControl, + FormsModule, + ReactiveFormsModule, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + ValidationErrors, +} from '@angular/forms'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { + SkyRadioGroupHeadingLevel, + SkyRadioGroupHeadingStyle, + SkyRadioModule, +} from '@skyux/forms'; + +function validatePaymentMethod( + control: AbstractControl, +): ValidationErrors | null { + return control.value === 'check' ? { processingIssue: true } : null; +} + +@Component({ + standalone: true, + selector: 'test-radio-harness', + templateUrl: './radio-harness-test.component.html', + imports: [FormsModule, ReactiveFormsModule, SkyRadioModule], +}) +export class RadioHarnessTestComponent { + public class = ''; + public cashHintText: string | undefined; + public headingLevel: SkyRadioGroupHeadingLevel | undefined = 3; + public headingStyle: SkyRadioGroupHeadingStyle = 3; + public helpKey: string | undefined; + public helpPopoverContent: string | undefined; + public helpPopoverTitle: string | undefined; + public hideCashLabel = false; + public hideCheckLabel = false; + public hideGroupHeading = false; + public hintText: string | undefined; + public myForm: UntypedFormGroup; + public paymentMethod: UntypedFormControl; + public required = false; + public stacked = false; + + #formBuilder = inject(UntypedFormBuilder); + + constructor() { + this.paymentMethod = this.#formBuilder.control('cash', { + validators: [validatePaymentMethod], + }); + + this.myForm = this.#formBuilder.group({ + paymentMethod: this.paymentMethod, + }); + } + + public disableForm(): void { + this.myForm.disable(); + } +} diff --git a/libs/components/forms/testing/src/radio/radio-group-harness-filters.ts b/libs/components/forms/testing/src/radio/radio-group-harness-filters.ts new file mode 100644 index 0000000000..335076940f --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-group-harness-filters.ts @@ -0,0 +1,8 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyRadioGroupHarness` instances. + * @internal + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SkyRadioGroupHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/forms/testing/src/radio/radio-group-harness.spec.ts b/libs/components/forms/testing/src/radio/radio-group-harness.spec.ts new file mode 100644 index 0000000000..77d1de9898 --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-group-harness.spec.ts @@ -0,0 +1,297 @@ +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyHelpService } from '@skyux/core'; +import { SkyHelpTestingModule } from '@skyux/core/testing'; + +import { RadioHarnessTestComponent } from './fixtures/radio-harness-test.component'; +import { SkyRadioGroupHarness } from './radio-group-harness'; + +async function setupTest(options: { dataSkyId?: string } = {}): Promise<{ + radioGroupHarness: SkyRadioGroupHarness; + fixture: ComponentFixture; +}> { + await TestBed.configureTestingModule({ + imports: [ + RadioHarnessTestComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(RadioHarnessTestComponent); + const loader = TestbedHarnessEnvironment.loader(fixture); + + const radioGroupHarness: SkyRadioGroupHarness = options.dataSkyId + ? await loader.getHarness( + SkyRadioGroupHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyRadioGroupHarness); + + return { radioGroupHarness, fixture }; +} + +describe('Radio group harness', () => { + it('should get the heading text', async () => { + const { radioGroupHarness } = await setupTest(); + + await expectAsync(radioGroupHarness.getHeadingText()).toBeResolvedTo( + 'Payment method', + ); + }); + + it('should get the heading text when heading text is hidden', async () => { + const { radioGroupHarness, fixture } = await setupTest({ + dataSkyId: 'radio-group', + }); + + fixture.componentInstance.hideGroupHeading = true; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingText()).toBeResolvedTo( + 'Payment method', + ); + }); + + it('should indicate the heading is not hidden', async () => { + const { radioGroupHarness } = await setupTest(); + + await expectAsync(radioGroupHarness.getHeadingHidden()).toBeResolvedTo( + false, + ); + }); + + it('should indicate the heading is hidden', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.hideGroupHeading = true; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingHidden()).toBeResolvedTo( + true, + ); + }); + + it('should return the heading level', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingLevel = undefined; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo( + undefined, + ); + + fixture.componentInstance.headingLevel = 3; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(3); + + fixture.componentInstance.headingLevel = 4; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(4); + + fixture.componentInstance.headingLevel = 5; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(5); + }); + + it('should return the heading style', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.headingLevel = undefined; + fixture.componentInstance.headingStyle = 3; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo( + undefined, + ); + await expectAsync(radioGroupHarness.getHeadingStyle()).toBeResolvedTo(3); + + fixture.componentInstance.headingLevel = 3; + fixture.componentInstance.headingStyle = 4; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(3); + await expectAsync(radioGroupHarness.getHeadingStyle()).toBeResolvedTo(4); + + fixture.componentInstance.headingLevel = 4; + fixture.componentInstance.headingStyle = 5; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(4); + await expectAsync(radioGroupHarness.getHeadingStyle()).toBeResolvedTo(5); + + fixture.componentInstance.headingLevel = 5; + fixture.componentInstance.headingStyle = 3; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHeadingLevel()).toBeResolvedTo(5); + await expectAsync(radioGroupHarness.getHeadingStyle()).toBeResolvedTo(3); + }); + + it('should get the hint text', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + const hintText = 'Hint text for the section.'; + + await expectAsync(radioGroupHarness.getHintText()).toBeResolvedTo(''); + + fixture.componentInstance.hintText = hintText; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getHintText()).toBeResolvedTo(hintText); + }); + + it('should indicate the component is stacked when margin is lg and headingLevel is not set', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.stacked = true; + fixture.componentInstance.headingLevel = undefined; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getStacked()).toBeResolvedTo(true); + }); + + it('should indicate the component is not stacked when margin is lg and headingLevel is set', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.class = 'sky-margin-stacked-lg'; + fixture.componentInstance.headingLevel = 4; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getStacked()).toBeResolvedTo(false); + }); + + it('should indicate the component is stacked when margin is xl and headingLevel is set', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.stacked = true; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getStacked()).toBeResolvedTo(true); + }); + + it('should indicate the component is not stacked when margin is xl and headingLevel is not set', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.class = 'sky-margin-stacked-xl'; + fixture.componentInstance.headingLevel = undefined; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getStacked()).toBeResolvedTo(false); + }); + + it('should indicate the component is not stacked', async () => { + const { radioGroupHarness } = await setupTest(); + + await expectAsync(radioGroupHarness.getStacked()).toBeResolvedTo(false); + }); + + it('should indicate the component is required', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.required = true; + fixture.detectChanges(); + + await expectAsync(radioGroupHarness.getRequired()).toBeResolvedTo(true); + }); + + it('should indicate the component is not required', async () => { + const { radioGroupHarness } = await setupTest(); + + await expectAsync(radioGroupHarness.getRequired()).toBeResolvedTo(false); + }); + + it('should display an error message when there is a custom validation error', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + fixture.componentInstance.required = true; + fixture.detectChanges(); + + const radioHarness = (await radioGroupHarness.getRadioButtons())[1]; + + await radioHarness.check(); + + await expectAsync( + radioGroupHarness.hasError('processingIssue'), + ).toBeResolvedTo(true); + }); + + it('should throw an error if no form error is found', async () => { + const { radioGroupHarness } = await setupTest(); + const radioHarness = (await radioGroupHarness.getRadioButtons())[2]; + + await radioHarness.check(); + + await expectAsync(radioGroupHarness.hasError('test')).toBeResolvedTo(false); + }); + + it('should throw an error if no help inline is found', async () => { + const { radioGroupHarness } = await setupTest(); + + await expectAsync( + radioGroupHarness.clickHelpInline(), + ).toBeRejectedWithError('No help inline found.'); + }); + + it('should open help inline popover when clicked', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + + fixture.componentInstance.helpPopoverContent = 'popover content'; + fixture.detectChanges(); + + await radioGroupHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioGroupHarness.getHelpPopoverContent()).toBeResolved(); + }); + + it('should open global help widget when clicked', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + const helpSvc = TestBed.inject(SkyHelpService); + const helpSpy = spyOn(helpSvc, 'openHelp'); + + fixture.componentInstance.helpPopoverContent = undefined; + fixture.componentInstance.helpKey = 'helpKey.html'; + fixture.detectChanges(); + + await radioGroupHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(helpSpy).toHaveBeenCalledWith({ helpKey: 'helpKey.html' }); + }); + + it('should get help popover content', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + fixture.componentInstance.helpPopoverContent = 'popover content'; + fixture.detectChanges(); + + await radioGroupHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioGroupHarness.getHelpPopoverContent()).toBeResolvedTo( + 'popover content', + ); + }); + + it('should get help popover title', async () => { + const { radioGroupHarness, fixture } = await setupTest(); + fixture.componentInstance.helpPopoverContent = 'popover content'; + fixture.componentInstance.helpPopoverTitle = 'popover title'; + fixture.detectChanges(); + + await radioGroupHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioGroupHarness.getHelpPopoverTitle()).toBeResolvedTo( + 'popover title', + ); + }); +}); diff --git a/libs/components/forms/testing/src/radio/radio-group-harness.ts b/libs/components/forms/testing/src/radio/radio-group-harness.ts new file mode 100644 index 0000000000..34bd547fbe --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-group-harness.ts @@ -0,0 +1,196 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { + SkyRadioGroupHeadingLevel, + SkyRadioGroupHeadingStyle, +} from '@skyux/forms'; +import { SkyHelpInlineHarness } from '@skyux/help-inline/testing'; + +import { SkyFormErrorsHarness } from '../form-error/form-errors-harness'; + +import { SkyRadioGroupHarnessFilters } from './radio-group-harness-filters'; +import { SkyRadioHarness } from './radio-harness'; + +/** + * Harness for interacting with a radio group component in tests. + */ +export class SkyRadioGroupHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-radio-group'; + + #getH3 = this.locatorForOptional('legend h3'); + #getH4 = this.locatorForOptional('legend h4'); + #getH5 = this.locatorForOptional('legend h5'); + #getHeading = this.locatorFor('.sky-control-label'); + #getHeadingText = this.locatorForOptional( + 'legend .sky-radio-group-heading-text', + ); + #getHintText = this.locatorForOptional('.sky-radio-group-hint-text'); + #getRadioButtons = this.locatorForAll(SkyRadioHarness); + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyRadioGroupHarness` that meets certain criteria. + */ + public static with( + filters: SkyRadioGroupHarnessFilters, + ): HarnessPredicate { + return SkyRadioGroupHarness.getDataSkyIdPredicate(filters); + } + + /** + * Clicks the help inline button. + */ + public async clickHelpInline(): Promise { + return (await this.#getHelpInline()).click(); + } + + /** + * Whether the heading is hidden. + */ + public async getHeadingHidden(): Promise { + return (await this.#getHeading()).hasClass('sky-screen-reader-only'); + } + + /** + * The semantic heading level used for the radio group. Returns undefined if heading level is not set. + */ + public async getHeadingLevel(): Promise< + SkyRadioGroupHeadingLevel | undefined + > { + const h3 = await this.#getH3(); + const h4 = await this.#getH4(); + const h5 = await this.#getH5(); + + if (h3) { + return 3; + } else if (h4) { + return 4; + } else if (h5) { + return 5; + } else { + return undefined; + } + } + + /** + * The heading style used for the radio group. + */ + public async getHeadingStyle(): Promise { + const headingOrLabel = + (await this.#getH3()) || + (await this.#getH4()) || + (await this.#getH5()) || + (await this.#getHeadingText()); + + const isHeadingStyle3 = + await headingOrLabel?.hasClass('sky-font-heading-3'); + const isHeadingStyle4 = + await headingOrLabel?.hasClass('sky-font-heading-4'); + + if (isHeadingStyle3) { + return 3; + } else if (isHeadingStyle4) { + return 4; + } else { + return 5; + } + } + + /** + * Gets the radio group's heading text. If `headingHidden` is true, + * the text will still be returned. + */ + public async getHeadingText(): Promise { + return (await this.#getHeading()).text(); + } + + /** + * Gets the help popover content. + */ + public async getHelpPopoverContent(): Promise { + const content = await (await this.#getHelpInline()).getPopoverContent(); + + /* istanbul ignore if */ + if (typeof content === 'object') { + throw Error('Unexpected template ref'); + } + + return content; + } + + /** + * Gets the help popover title. + */ + public async getHelpPopoverTitle(): Promise { + return await (await this.#getHelpInline()).getPopoverTitle(); + } + + /** + * Gets the radio group's hint text. + */ + public async getHintText(): Promise { + const hintText = await this.#getHintText(); + + return (await hintText?.text())?.trim() ?? ''; + } + + /** + * Gets an array of harnesses for the radio buttons in the radio group. + */ + public async getRadioButtons(): Promise { + return await this.#getRadioButtons(); + } + + /** + * Whether the radio group is required. + */ + public async getRequired(): Promise { + const heading = await this.#getHeading(); + + return await heading.hasClass('sky-control-label-required'); + } + + /** + * Whether the radio group is stacked. + */ + public async getStacked(): Promise { + const host = await this.host(); + const heading = + (await this.#getH3()) || (await this.#getH4()) || (await this.#getH5()); + const label = await this.#getHeadingText(); + + return ( + ((await host.hasClass('sky-margin-stacked-lg')) && !!label) || + ((await host.hasClass('sky-margin-stacked-xl')) && !!heading) + ); + } + + /** + * Whether the radio group has errors. + */ + public async hasError(errorName: string): Promise { + return (await this.#getFormErrors()).hasError(errorName); + } + + async #getFormErrors(): Promise { + return await this.locatorFor(SkyFormErrorsHarness)(); + } + + async #getHelpInline(): Promise { + const harness = await this.locatorForOptional( + SkyHelpInlineHarness.with({ + ancestor: '.sky-radio-group > .sky-radio-group-label-wrapper', + }), + )(); + + if (harness) { + return harness; + } + + throw Error('No help inline found.'); + } +} diff --git a/libs/components/forms/testing/src/radio/radio-harness-filters.ts b/libs/components/forms/testing/src/radio/radio-harness-filters.ts new file mode 100644 index 0000000000..12164b6ec8 --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-harness-filters.ts @@ -0,0 +1,8 @@ +import { SkyHarnessFilters } from '@skyux/core/testing'; + +/** + * A set of criteria that can be used to filter a list of `SkyRadioHarness` instances. + * @internal + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SkyRadioHarnessFilters extends SkyHarnessFilters {} diff --git a/libs/components/forms/testing/src/radio/radio-harness.spec.ts b/libs/components/forms/testing/src/radio/radio-harness.spec.ts new file mode 100644 index 0000000000..5b69abf69a --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-harness.spec.ts @@ -0,0 +1,245 @@ +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { SkyHelpService } from '@skyux/core'; +import { SkyHelpTestingModule } from '@skyux/core/testing'; + +import { RadioHarnessTestComponent } from './fixtures/radio-harness-test.component'; +import { SkyRadioHarness } from './radio-harness'; + +async function setupTest( + options: { dataSkyId?: string; hideCheckLabel?: boolean } = {}, +): Promise<{ + radioHarness: SkyRadioHarness; + fixture: ComponentFixture; + loader: HarnessLoader; +}> { + await TestBed.configureTestingModule({ + imports: [ + RadioHarnessTestComponent, + SkyHelpTestingModule, + NoopAnimationsModule, + ], + }).compileComponents(); + + const fixture = TestBed.createComponent(RadioHarnessTestComponent); + if (options.hideCheckLabel) { + fixture.componentInstance.hideCheckLabel = true; + fixture.detectChanges(); + } + const loader = TestbedHarnessEnvironment.loader(fixture); + + const radioHarness: SkyRadioHarness = options.dataSkyId + ? await loader.getHarness( + SkyRadioHarness.with({ + dataSkyId: options.dataSkyId, + }), + ) + : await loader.getHarness(SkyRadioHarness); + + return { radioHarness, fixture, loader }; +} + +describe('Radio harness', () => { + it('should check if radio is disabled', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-check-radio', + }); + + await expectAsync(radioHarness.isDisabled()).toBeResolvedTo(false); + + fixture.componentInstance.disableForm(); + await expectAsync(radioHarness.isDisabled()).toBeResolvedTo(true); + }); + + it('should focus the radio', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-check-radio', + }); + + await expectAsync(radioHarness.isFocused()).toBeResolvedTo(false); + + await radioHarness.focus(); + await expectAsync(radioHarness.isFocused()).toBeResolvedTo(true); + + await radioHarness.blur(); + await expectAsync(radioHarness.isFocused()).toBeResolvedTo(false); + }); + + it('should get ARIA attributes', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-check-radio', + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioHarness.getAriaLabel()).toBeResolvedTo( + 'Pay by check', + ); + await expectAsync(radioHarness.getAriaLabelledby()).toBeResolvedTo( + 'foo-check-id', + ); + await expectAsync(radioHarness.getLabelText()).toBeResolvedTo('Check'); + }); + + it('should handle a missing label when getting the label text', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-check-radio', + hideCheckLabel: true, + }); + await expectAsync(radioHarness.getLabelText()).toBeResolvedTo(undefined); + }); + + it('should get the label text when specified via `labelText` input', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + + await expectAsync(radioHarness.getLabelText()).toBeResolvedTo('Cash'); + }); + + it('should get the label text when specified via `labelText` input and label is hidden', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + + fixture.componentInstance.hideCashLabel = true; + fixture.detectChanges(); + + await expectAsync(radioHarness.getLabelText()).toBeResolvedTo('Cash'); + }); + + it('should indicate the label is not hidden when the label is specified via `labelText`', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + + await expectAsync(radioHarness.getLabelHidden()).toBeResolvedTo(false); + }); + + it('should indicate the label is hidden when the label is specified via `labelText`', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + + fixture.componentInstance.hideCashLabel = true; + fixture.detectChanges(); + + await expectAsync(radioHarness.getLabelHidden()).toBeResolvedTo(true); + }); + + it('should throw an error when getting `labelIsHidden` for a radio using `sky-radio-label`', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-check-radio', + }); + + await expectAsync(radioHarness.getLabelHidden()).toBeRejectedWithError( + '`labelIsHidden` is only supported when setting the radio label via the `labelText` input.', + ); + }); + + it('should get the hint text', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + const hintText = 'Hint text for the radio.'; + + await expectAsync(radioHarness.getHintText()).toBeResolvedTo(''); + + fixture.componentInstance.cashHintText = hintText; + fixture.detectChanges(); + + await expectAsync(radioHarness.getHintText()).toBeResolvedTo(hintText); + }); + + it('should get the radio name', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-check-radio', + }); + + await expectAsync(radioHarness.getName()).toBeResolvedTo( + jasmine.stringMatching(/sky-radio-group-[0-9]+/), + ); + }); + + it('should throw error if toggling a disabled radio', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-check-radio', + }); + + fixture.componentInstance.disableForm(); + + await expectAsync(radioHarness.isChecked()).toBeResolvedTo(false); + + await expectAsync(radioHarness.check()).toBeRejectedWithError( + 'Could not check the radio button because it is disabled.', + ); + }); + + it('should throw an error if no help inline is found', async () => { + const { radioHarness } = await setupTest({ + dataSkyId: 'my-credit-radio', + }); + + await expectAsync(radioHarness.clickHelpInline()).toBeRejectedWithError( + 'No help inline found.', + ); + }); + + it('should open help inline popover when clicked', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + + await radioHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioHarness.getHelpPopoverContent()).toBeResolved(); + }); + + it('should open help widget when clicked', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + const helpSvc = TestBed.inject(SkyHelpService); + const helpSpy = spyOn(helpSvc, 'openHelp'); + fixture.componentInstance.helpKey = 'helpKey.html'; + fixture.componentInstance.helpPopoverContent = undefined; + fixture.detectChanges(); + + await radioHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(helpSpy).toHaveBeenCalledWith({ helpKey: 'helpKey.html' }); + }); + + it('should get help popover content', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + await radioHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioHarness.getHelpPopoverContent()).toBeResolvedTo( + '(xxx)xxx-xxxx', + ); + }); + + it('should get help popover title', async () => { + const { radioHarness, fixture } = await setupTest({ + dataSkyId: 'my-cash-radio', + }); + await radioHarness.clickHelpInline(); + fixture.detectChanges(); + await fixture.whenStable(); + + await expectAsync(radioHarness.getHelpPopoverTitle()).toBeResolvedTo( + 'Format', + ); + }); +}); diff --git a/libs/components/forms/testing/src/radio/radio-harness.ts b/libs/components/forms/testing/src/radio/radio-harness.ts new file mode 100644 index 0000000000..ecfaee800b --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-harness.ts @@ -0,0 +1,183 @@ +import { HarnessPredicate } from '@angular/cdk/testing'; +import { SkyComponentHarness } from '@skyux/core/testing'; +import { SkyHelpInlineHarness } from '@skyux/help-inline/testing'; + +import { SkyRadioHarnessFilters } from './radio-harness-filters'; +import { SkyRadioLabelHarness } from './radio-label-harness'; + +/** + * Harness for interacting with a radio button component in tests. + */ +export class SkyRadioHarness extends SkyComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-radio'; + + #getHintText = this.locatorForOptional('.sky-radio-hint-text'); + + #getInput = this.locatorFor('input.sky-radio-input'); + + #getLabel = this.locatorForOptional(SkyRadioLabelHarness); + + #getLabelText = this.locatorForOptional( + 'span.sky-switch-label.sky-radio-label-text', + ); + + /** + * Gets a `HarnessPredicate` that can be used to search for a + * `SkyRadioHarness` that meets certain criteria. + */ + public static with( + filters: SkyRadioHarnessFilters, + ): HarnessPredicate { + return SkyRadioHarness.getDataSkyIdPredicate(filters); + } + + /** + * Blurs the radio button. + */ + public async blur(): Promise { + return (await this.#getInput()).blur(); + } + + /** + * Puts the radio button in a checked state if it is currently unchecked. + */ + public async check(): Promise { + if (await this.isDisabled()) { + throw new Error( + 'Could not check the radio button because it is disabled.', + ); + } else if (!(await this.isChecked())) { + await (await this.#getInput()).click(); + } + } + + /** + * Clicks the help inline button. + */ + public async clickHelpInline(): Promise { + return (await this.#getHelpInline()).click(); + } + + /** + * Focuses the radio button. + */ + public async focus(): Promise { + return (await this.#getInput()).focus(); + } + + /** + * Gets the radio button's aria-label. + */ + public async getAriaLabel(): Promise { + return (await this.#getInput()).getAttribute('aria-label'); + } + + /** + * Gets the radio button's aria-labelledby. + */ + public async getAriaLabelledby(): Promise { + return (await this.#getInput()).getAttribute('aria-labelledby'); + } + + /** + * Gets the help popover content. + */ + public async getHelpPopoverContent(): Promise { + const content = await (await this.#getHelpInline()).getPopoverContent(); + + /* istanbul ignore if */ + if (typeof content === 'object') { + throw Error('Unexpected template ref'); + } + + return content; + } + + /** + * Gets the help popover title. + */ + public async getHelpPopoverTitle(): Promise { + return await (await this.#getHelpInline()).getPopoverTitle(); + } + + /** + * Gets the radio button's hint text. + */ + public async getHintText(): Promise { + const hintText = await this.#getHintText(); + + return (await hintText?.text())?.trim() ?? ''; + } + + /** + * Whether the label is hidden. Only supported when using the `labelText` input to set the label. + */ + public async getLabelHidden(): Promise { + const labelText = await this.#getLabelText(); + const label = await this.#getLabel(); + + if (label) { + throw new Error( + '`labelIsHidden` is only supported when setting the radio label via the `labelText` input.', + ); + } else { + return !!(await labelText?.hasClass('sky-screen-reader-only')); + } + } + + /** + * Gets the radio button's label text. If the label is set via `labelText` and `labelHidden` is true, + * the text will still be returned. + */ + public async getLabelText(): Promise { + const labelText = await this.#getLabelText(); + + if (labelText) { + return labelText.text(); + } else { + return (await this.#getLabel())?.getText(); + } + } + + /** + * Gets the radio button's name. + */ + public async getName(): Promise { + return (await this.#getInput()).getAttribute('name'); + } + + /** + * Whether the radio button is checked. + */ + public async isChecked(): Promise { + return (await this.#getInput()).getProperty('checked'); + } + + /** + * Whether the radio button is disabled. + */ + public async isDisabled(): Promise { + const disabled = await (await this.#getInput()).getAttribute('disabled'); + return disabled !== null; + } + + /** + * Whether the radio button is focused. + */ + public async isFocused(): Promise { + return (await this.#getInput()).isFocused(); + } + + async #getHelpInline(): Promise { + const harness = await this.locatorForOptional(SkyHelpInlineHarness)(); + + if (harness) { + return harness; + } + + throw Error('No help inline found.'); + } +} diff --git a/libs/components/forms/testing/src/radio/radio-label-harness.ts b/libs/components/forms/testing/src/radio/radio-label-harness.ts new file mode 100644 index 0000000000..aef9c063dc --- /dev/null +++ b/libs/components/forms/testing/src/radio/radio-label-harness.ts @@ -0,0 +1,21 @@ +import { ComponentHarness } from '@angular/cdk/testing'; + +/** + * Harness for interacting with a radio label component in tests. + * @internal + */ +export class SkyRadioLabelHarness extends ComponentHarness { + /** + * @internal + */ + public static hostSelector = 'sky-radio-label'; + + #getLabelContent = this.locatorFor('.sky-switch-label'); + + /** + * Gets the text content of the radio label. + */ + public async getText(): Promise { + return (await this.#getLabelContent()).text(); + } +}