From 193a6c48ce2b6d4aef6443fbe464a3214f3a8ab3 Mon Sep 17 00:00:00 2001 From: syedszeeshan <47701214+syedszeeshan@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:31:54 -0400 Subject: [PATCH 01/80] fix(#3072): angular reset and value binding issue --- .../src/lib/components/base.component.ts | 60 ++++++++++++- .../components/checkbox-list/checkbox-list.ts | 8 +- .../lib/components/checkbox/checkbox.spec.ts | 53 ++++++++++-- .../src/lib/components/checkbox/checkbox.ts | 78 ++++++++++------- .../lib/components/date-picker/date-picker.ts | 25 +++--- .../lib/components/dropdown/dropdown.spec.ts | 84 ++++++++++++++++++- .../src/lib/components/dropdown/dropdown.ts | 13 ++- .../src/lib/components/input/input.spec.ts | 46 ++++++++++ .../src/lib/components/input/input.ts | 9 +- .../radio-group/radio-group.spec.ts | 61 ++++++++++++-- .../lib/components/radio-group/radio-group.ts | 9 +- .../lib/components/textarea/textarea.spec.ts | 47 +++++++++++ .../src/lib/components/textarea/textarea.ts | 9 +- .../src/components/dropdown/Dropdown.svelte | 12 ++- 14 files changed, 442 insertions(+), 72 deletions(-) diff --git a/libs/angular-components/src/lib/components/base.component.ts b/libs/angular-components/src/lib/components/base.component.ts index 74b957aac3..74768456f3 100644 --- a/libs/angular-components/src/lib/components/base.component.ts +++ b/libs/angular-components/src/lib/components/base.component.ts @@ -1,6 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Spacing } from "@abgov/ui-components-common"; -import { booleanAttribute, Component, Input } from "@angular/core"; +import { + booleanAttribute, + Component, + Input, + ElementRef, + ViewChild, + Renderer2, +} from "@angular/core"; import { ControlValueAccessor } from "@angular/forms"; @Component({ @@ -28,9 +35,25 @@ export abstract class GoabBaseComponent { * - Supports `disabled="true"` and `error="true` attribute bindings for convenience. * - Handles form control value changes and touch events via `ControlValueAccessor` methods. * - Allows for flexible value types (`unknown`), making it suitable for various data types like integers, dates, or booleans. + * - Uses ViewChild to capture a reference to the native GOA web component element via `#goaComponentRef`. + * - Uses Renderer2 for safe DOM manipulation (compatible with SSR and security best practices). * * ## Usage - * Extend this class to create custom form controls. Implement additional functionality as needed for your specific use case. + * Extend this class to create custom form controls. Child components must: + * 1. Add `#goaComponentRef` template reference to their `goa-*` element in the template + * 2. Inject `Renderer2` in their constructor and pass it to `super(renderer)` + * + * ### Example: + * ```typescript + * @Component({ + * template: `` + * }) + * export class GoabInput extends GoabControlValueAccessor { + * constructor(private cdr: ChangeDetectorRef, renderer: Renderer2) { + * super(renderer); // Required: pass Renderer2 to base class + * } + * } + * ``` * * ## Properties * - `id?`: An optional identifier for the component. @@ -40,10 +63,11 @@ export abstract class GoabBaseComponent { * * ## Methods * - `markAsTouched()`: Marks the component as touched and triggers the `fcTouched` callback if defined. - * - `writeValue(value: unknown)`: Writes a new value to the form control. + * - `writeValue(value: unknown)`: Writes a new value to the form control (can be overridden for special behavior like checkbox). * - `registerOnChange(fn: any)`: Registers a function to handle changes in the form control value. * - `registerOnTouched(fn: any)`: Registers a function to handle touch events on the form control. * - `setDisabledState?(isDisabled: boolean)`: Sets the disabled state of the component. + * - `convertValueToString(value: unknown)`: Converts a value to a string for DOM attribute assignment (can be overridden). * * ## Callbacks * - `fcChange?`: A function to handle changes in the form control value. @@ -87,12 +111,42 @@ export abstract class GoabControlValueAccessor } } + /** + * Reference to the native GOA web component element. + * Child templates should declare `#goaComponentRef` on the `goa-*` element. + * The base class captures it here so children don't need their own ViewChild. + */ + @ViewChild("goaComponentRef", { static: false, read: ElementRef }) + protected goaComponentRef?: ElementRef; + + constructor(protected renderer: Renderer2) { + super(); + } + + /** + * Convert an arbitrary value into a string for DOM attribute assignment. + * Child classes can override when they need special formatting. + * @param value The value to convert + * @returns string representation or empty string for nullish/empty + */ + protected convertValueToString(value: unknown): string { + if (value === null || value === undefined || value === "") { + return ""; + } + return String(value); + } + /** * Writes a new value to the form control. * @param {unknown} value - The value to write. */ public writeValue(value: unknown): void { this.value = value; + const el = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (el) { + const stringValue = this.convertValueToString(value); + this.renderer.setAttribute(el, "value", stringValue); + } } /** diff --git a/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts b/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts index db052a4dbe..c716ae827c 100644 --- a/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts +++ b/libs/angular-components/src/lib/components/checkbox-list/checkbox-list.ts @@ -8,6 +8,7 @@ import { forwardRef, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -51,8 +52,11 @@ export class GoabCheckboxList extends GoabControlValueAccessor implements OnInit // Override value to handle string arrays consistently @Input() override value?: string[]; - constructor(private cdr: ChangeDetectorRef) { - super(); + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); } ngOnInit(): void { diff --git a/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts b/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts index fc7d6a82e0..35e21f225c 100644 --- a/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts +++ b/libs/angular-components/src/lib/components/checkbox/checkbox.spec.ts @@ -93,7 +93,7 @@ describe("GoabCheckbox", () => { expect(checkboxElement.getAttribute("maxwidth")).toBe("480px"); }); - it("should handle onChange event", fakeAsync(() => { + it("should handle onChange event", async () => { const onChange = jest.spyOn(component, "onChange"); const checkboxElement = fixture.debugElement.query( @@ -108,7 +108,50 @@ describe("GoabCheckbox", () => { ); expect(onChange).toHaveBeenCalled(); - })); + }); + + describe("writeValue", () => { + it("should set checked attribute to true when value is truthy", () => { + const checkboxComponent = fixture.debugElement.query(By.css("goab-checkbox")).componentInstance; + const checkboxElement = fixture.debugElement.query(By.css("goa-checkbox")).nativeElement; + + checkboxComponent.writeValue(true); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + + checkboxComponent.writeValue("some value"); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + + checkboxComponent.writeValue(1); + expect(checkboxElement.getAttribute("checked")).toBe("true"); + }); + + it("should set checked attribute to false when value is falsy", () => { + const checkboxComponent = fixture.debugElement.query(By.css("goab-checkbox")).componentInstance; + const checkboxElement = fixture.debugElement.query(By.css("goa-checkbox")).nativeElement; + + checkboxComponent.writeValue(false); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(null); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(undefined); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + + checkboxComponent.writeValue(""); + expect(checkboxElement.getAttribute("checked")).toBe("false"); + }); + + it("should update component value property", () => { + const checkboxComponent = fixture.debugElement.query(By.css("goab-checkbox")).componentInstance; + + checkboxComponent.writeValue(true); + expect(checkboxComponent.value).toBe(true); + + checkboxComponent.writeValue(null); + expect(checkboxComponent.value).toBe(null); + }); + }); }); @Component({ @@ -136,7 +179,7 @@ describe("Checkbox with description slot", () => { it("should render with slot description", fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabCheckbox, ReactiveFormsModule, TestCheckboxWithDescriptionSlotComponent], + imports: [TestCheckboxWithDescriptionSlotComponent, GoabCheckbox, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -155,7 +198,7 @@ describe("Checkbox with description slot", () => { @Component({ standalone: true, - imports: [GoabCheckbox, ReactiveFormsModule], + imports: [GoabCheckbox], template: ` { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabCheckbox, ReactiveFormsModule, TestCheckboxWithRevealSlotComponent], + imports: [TestCheckboxWithRevealSlotComponent, GoabCheckbox, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); diff --git a/libs/angular-components/src/lib/components/checkbox/checkbox.ts b/libs/angular-components/src/lib/components/checkbox/checkbox.ts index 47dcd083fd..4dc3ba957c 100644 --- a/libs/angular-components/src/lib/components/checkbox/checkbox.ts +++ b/libs/angular-components/src/lib/components/checkbox/checkbox.ts @@ -10,6 +10,7 @@ import { booleanAttribute, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { NgTemplateOutlet, CommonModule } from "@angular/common"; @@ -19,34 +20,35 @@ import { GoabControlValueAccessor } from "../base.component"; standalone: true, selector: "goab-checkbox", template: ` - -
- -
-
- -
-
`, + #goaComponentRef + *ngIf="isReady" + [attr.name]="name" + [checked]="checked" + [disabled]="disabled" + [attr.indeterminate]="indeterminate ? 'true' : undefined" + [attr.error]="error" + [attr.text]="text" + [value]="value" + [attr.testid]="testId" + [attr.arialabel]="ariaLabel" + [attr.description]="getDescriptionAsString()" + [attr.revealarialabel]="revealArialLabel" + [id]="id" + [attr.maxwidth]="maxWidth" + [attr.mt]="mt" + [attr.mb]="mb" + [attr.ml]="ml" + [attr.mr]="mr" + (_change)="_onChange($event)" + > + +
+ +
+
+ +
+ `, schemas: [CUSTOM_ELEMENTS_SCHEMA], providers: [ { @@ -60,8 +62,11 @@ import { GoabControlValueAccessor } from "../base.component"; export class GoabCheckbox extends GoabControlValueAccessor implements OnInit { isReady = false; - constructor(private cdr: ChangeDetectorRef) { - super(); + constructor( + private cdr: ChangeDetectorRef, + renderer: Renderer2, + ) { + super(renderer); } ngOnInit(): void { @@ -78,7 +83,7 @@ export class GoabCheckbox extends GoabControlValueAccessor implements OnInit { @Input({ transform: booleanAttribute }) indeterminate?: boolean; @Input() text?: string; // ** NOTE: can we just use the base component for this? - @Input() override value?: string | number | boolean; + @Input() override value?: string | number | boolean | null; @Input() ariaLabel?: string; @Input() description!: string | TemplateRef; @Input() reveal?: TemplateRef; @@ -104,4 +109,15 @@ export class GoabCheckbox extends GoabControlValueAccessor implements OnInit { this.markAsTouched(); this.fcChange?.(detail.binding === "check" ? detail.checked : detail.value || ""); } + + // Checkbox is a special case: it uses `checked` instead of `value`. + override writeValue(value: string | number | boolean | null): void { + this.value = value; + this.checked = !!value; + + const el = this.goaComponentRef?.nativeElement as HTMLElement | undefined; + if (el) { + this.renderer.setAttribute(el, "checked", this.checked ? "true" : "false"); + } + } } diff --git a/libs/angular-components/src/lib/components/date-picker/date-picker.ts b/libs/angular-components/src/lib/components/date-picker/date-picker.ts index dc9872fafa..1e315a65ee 100644 --- a/libs/angular-components/src/lib/components/date-picker/date-picker.ts +++ b/libs/angular-components/src/lib/components/date-picker/date-picker.ts @@ -1,4 +1,7 @@ -import { GoabDatePickerInputType, GoabDatePickerOnChangeDetail } from "@abgov/ui-components-common"; +import { + GoabDatePickerInputType, + GoabDatePickerOnChangeDetail, +} from "@abgov/ui-components-common"; import { CUSTOM_ELEMENTS_SCHEMA, Component, @@ -10,6 +13,7 @@ import { HostListener, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -20,6 +24,7 @@ import { GoabControlValueAccessor } from "../base.component"; selector: "goab-date-picker", imports: [CommonModule], template: ` { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabDropdown, GoabDropdownItem, ReactiveFormsModule, TestDropdownComponent], + imports: [TestDropdownComponent, GoabDropdown, GoabDropdownItem, ReactiveFormsModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -140,6 +141,8 @@ describe("GoABDropdown", () => { const onChangeMock = jest.spyOn(component, "onChange"); component.native = true; fixture.detectChanges(); + tick(); + fixture.detectChanges(); const el = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; expect(el).toBeTruthy(); @@ -152,4 +155,79 @@ describe("GoABDropdown", () => { ); expect(onChangeMock).toHaveBeenCalled(); })); -}); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called with a value", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + dropdownComponent.writeValue("red"); + expect(dropdownElement.getAttribute("value")).toBe("red"); + + dropdownComponent.writeValue("blue"); + expect(dropdownElement.getAttribute("value")).toBe("blue"); + }); + + it("should set value attribute to empty string when writeValue is called with null", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + // First set a value + dropdownComponent.writeValue("red"); + expect(dropdownElement.getAttribute("value")).toBe("red"); + + // Then clear it + dropdownComponent.writeValue(null); + expect(dropdownElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + + dropdownComponent.writeValue("yellow"); + expect(dropdownComponent.value).toBe("yellow"); + + dropdownComponent.writeValue(null); + expect(dropdownComponent.value).toBe(null); + }); + }); + + describe("_onChange", () => { + it("should update component value when user selects an option", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "yellow" }, + }), + ); + + expect(dropdownComponent.value).toBe("yellow"); + }); + + it("should update value to null when cleared", () => { + const dropdownComponent = fixture.debugElement.query(By.css("goab-dropdown")).componentInstance; + const dropdownElement = fixture.debugElement.query(By.css("goa-dropdown")).nativeElement; + + // Set initial value + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "red" }, + }), + ); + expect(dropdownComponent.value).toBe("red"); + + // Clear value + fireEvent( + dropdownElement, + new CustomEvent("_change", { + detail: { name: component.name, value: "" }, + }), + ); + expect(dropdownComponent.value).toBe(null); + }); + }); +}); \ No newline at end of file diff --git a/libs/angular-components/src/lib/components/dropdown/dropdown.ts b/libs/angular-components/src/lib/components/dropdown/dropdown.ts index eb5ab04cca..5fa3dbc93e 100644 --- a/libs/angular-components/src/lib/components/dropdown/dropdown.ts +++ b/libs/angular-components/src/lib/components/dropdown/dropdown.ts @@ -9,6 +9,7 @@ import { forwardRef, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -21,6 +22,7 @@ import { GoabControlValueAccessor } from "../base.component"; imports: [CommonModule], template: ` ).detail; + // Keep local value in sync with emitted detail + this.value = detail.value || null; this.onChange.emit(detail); this.markAsTouched(); this.fcChange?.(detail.value || ""); } -} \ No newline at end of file +} diff --git a/libs/angular-components/src/lib/components/input/input.spec.ts b/libs/angular-components/src/lib/components/input/input.spec.ts index 27fd76e318..8069b13919 100644 --- a/libs/angular-components/src/lib/components/input/input.spec.ts +++ b/libs/angular-components/src/lib/components/input/input.spec.ts @@ -266,6 +266,52 @@ describe("GoABInput", () => { expect(trailingContent).toBeTruthy(); expect(trailingContent.textContent).toContain("Trailing Content"); }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const inputComponent = fixture.debugElement.query(By.css("goab-input")).componentInstance; + const inputElement = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + inputComponent.writeValue("new value"); + expect(inputElement.getAttribute("value")).toBe("new value"); + + inputComponent.writeValue("another value"); + expect(inputElement.getAttribute("value")).toBe("another value"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const inputComponent = fixture.debugElement.query(By.css("goab-input")).componentInstance; + const inputElement = fixture.debugElement.query(By.css("goa-input")).nativeElement; + + // First set a value + inputComponent.writeValue("some value"); + expect(inputElement.getAttribute("value")).toBe("some value"); + + // Then clear it with null + inputComponent.writeValue(null); + expect(inputElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + inputComponent.writeValue("test"); + inputComponent.writeValue(undefined); + expect(inputElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + inputComponent.writeValue("test2"); + inputComponent.writeValue(""); + expect(inputElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const inputComponent = fixture.debugElement.query(By.css("goab-input")).componentInstance; + + inputComponent.writeValue("updated"); + expect(inputComponent.value).toBe("updated"); + + inputComponent.writeValue(null); + expect(inputComponent.value).toBe(null); + }); + }); }); @Component({ diff --git a/libs/angular-components/src/lib/components/input/input.ts b/libs/angular-components/src/lib/components/input/input.ts index f1d16292ec..41fa0b8b37 100644 --- a/libs/angular-components/src/lib/components/input/input.ts +++ b/libs/angular-components/src/lib/components/input/input.ts @@ -19,6 +19,7 @@ import { numberAttribute, TemplateRef, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { GoabControlValueAccessor } from "../base.component"; @@ -34,6 +35,7 @@ export interface IgnoreMe { imports: [NgIf, NgTemplateOutlet, CommonModule], template: ` { beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabRadioGroup, GoabRadioItem, TestRadioGroupComponent], + imports: [TestRadioGroupComponent, GoabRadioGroup, GoabRadioItem], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); @@ -136,14 +137,12 @@ describe("GoABRadioGroup", () => { }); }); - it("should render description", fakeAsync(() => { + it("should render description", () => { component.options.forEach((option, index) => { component.options[index].description = `Description for ${component.options[index].text}`; }); component.options[0].isDescriptionSlot = true; fixture.detectChanges(); - tick(); - fixture.detectChanges(); const radioGroup = fixture.nativeElement.querySelector("goa-radio-group"); expect(radioGroup).toBeTruthy(); @@ -157,16 +156,62 @@ describe("GoABRadioGroup", () => { // attribute description expect(radioItems[1].getAttribute("description")).toBe(`Description for ${component.options[1].text}`); expect(radioItems[2].getAttribute("description")).toBe(`Description for ${component.options[2].text}`); - })); + }); it("should dispatch onChange", () => { const onChange = jest.spyOn(component, "onChange"); const radioGroup = fixture.nativeElement.querySelector("goa-radio-group"); fireEvent(radioGroup, new CustomEvent("_change", { - detail: { "name": component.name, value: component.options[0].value } + detail: {"name": component.name, value: component.options[0].value} })); - expect(onChange).toBeCalledWith({ name: component.name, value: component.options[0].value }); - }) + expect(onChange).toBeCalledWith({name: component.name, value: component.options[0].value}); + }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const radioGroupComponent = fixture.debugElement.query(By.css("goab-radio-group")).componentInstance; + const radioGroupElement = fixture.nativeElement.querySelector("goa-radio-group"); + + radioGroupComponent.writeValue("apples"); + expect(radioGroupElement.getAttribute("value")).toBe("apples"); + + radioGroupComponent.writeValue("oranges"); + expect(radioGroupElement.getAttribute("value")).toBe("oranges"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const radioGroupComponent = fixture.debugElement.query(By.css("goab-radio-group")).componentInstance; + const radioGroupElement = fixture.nativeElement.querySelector("goa-radio-group"); + + // First set a value + radioGroupComponent.writeValue("bananas"); + expect(radioGroupElement.getAttribute("value")).toBe("bananas"); + + // Then clear it with null + radioGroupComponent.writeValue(null); + expect(radioGroupElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + radioGroupComponent.writeValue("apples"); + radioGroupComponent.writeValue(undefined); + expect(radioGroupElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + radioGroupComponent.writeValue("oranges"); + radioGroupComponent.writeValue(""); + expect(radioGroupElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const radioGroupComponent = fixture.debugElement.query(By.css("goab-radio-group")).componentInstance; + + radioGroupComponent.writeValue("apples"); + expect(radioGroupComponent.value).toBe("apples"); + + radioGroupComponent.writeValue(null); + expect(radioGroupComponent.value).toBe(null); + }); + }); }); diff --git a/libs/angular-components/src/lib/components/radio-group/radio-group.ts b/libs/angular-components/src/lib/components/radio-group/radio-group.ts index a2284c6128..56e614363d 100644 --- a/libs/angular-components/src/lib/components/radio-group/radio-group.ts +++ b/libs/angular-components/src/lib/components/radio-group/radio-group.ts @@ -11,6 +11,7 @@ import { forwardRef, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -21,6 +22,7 @@ import { GoabControlValueAccessor } from "../base.component"; selector: "goab-radio-group", template: ` { expect(onBlur).toBeCalledTimes(1); }); + + describe("writeValue", () => { + it("should set value attribute when writeValue is called", () => { + const textareaComponent = fixture.debugElement.query(By.css("goab-textarea")).componentInstance; + const textareaElement = fixture.nativeElement.querySelector("goa-textarea"); + + textareaComponent.writeValue("new content"); + expect(textareaElement.getAttribute("value")).toBe("new content"); + + textareaComponent.writeValue("updated content"); + expect(textareaElement.getAttribute("value")).toBe("updated content"); + }); + + it("should set value attribute to empty string when writeValue is called with null or empty", () => { + const textareaComponent = fixture.debugElement.query(By.css("goab-textarea")).componentInstance; + const textareaElement = fixture.nativeElement.querySelector("goa-textarea"); + + // First set a value + textareaComponent.writeValue("some content"); + expect(textareaElement.getAttribute("value")).toBe("some content"); + + // Then clear it with null + textareaComponent.writeValue(null); + expect(textareaElement.getAttribute("value")).toBe(""); + + // Set again and clear with undefined + textareaComponent.writeValue("test content"); + textareaComponent.writeValue(undefined); + expect(textareaElement.getAttribute("value")).toBe(""); + + // Set again and clear with empty string + textareaComponent.writeValue("more content"); + textareaComponent.writeValue(""); + expect(textareaElement.getAttribute("value")).toBe(""); + }); + + it("should update component value property", () => { + const textareaComponent = fixture.debugElement.query(By.css("goab-textarea")).componentInstance; + + textareaComponent.writeValue("updated value"); + expect(textareaComponent.value).toBe("updated value"); + + textareaComponent.writeValue(null); + expect(textareaComponent.value).toBe(null); + }); + }); }); diff --git a/libs/angular-components/src/lib/components/textarea/textarea.ts b/libs/angular-components/src/lib/components/textarea/textarea.ts index 453371b511..8887d28550 100644 --- a/libs/angular-components/src/lib/components/textarea/textarea.ts +++ b/libs/angular-components/src/lib/components/textarea/textarea.ts @@ -15,6 +15,7 @@ import { numberAttribute, OnInit, ChangeDetectorRef, + Renderer2, } from "@angular/core"; import { NG_VALUE_ACCESSOR } from "@angular/forms"; import { CommonModule } from "@angular/common"; @@ -26,6 +27,7 @@ import { GoabControlValueAccessor } from "../base.component"; imports: [CommonModule], template: ` element function setDisplayedValue() { - _inputEl.value = _selectedOption?.label || _selectedOption?.value || ""; + const newValue = _selectedOption?.label || _selectedOption?.value || ""; + _inputEl.value = newValue; } function dispatchValue(newValue?: string) { @@ -636,6 +641,11 @@ } onKeyUp(_: KeyboardEvent) { + // Clear selection and highlight if input becomes empty + if (this.input.value === "" && _selectedOption) { + _selectedOption = undefined; + _highlightedIndex = -1; + } showMenu(); } From 81b9ae7de7ae5f4ccc2c0e4b700110034d5a7a75 Mon Sep 17 00:00:00 2001 From: chrisolsen Date: Fri, 31 Oct 2025 16:34:48 -0600 Subject: [PATCH 02/80] chore: add flake file for nix environment --- flake.lock | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 32 ++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..6915301551 --- /dev/null +++ b/flake.lock @@ -0,0 +1,126 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1761672384, + "narHash": "sha256-o9KF3DJL7g7iYMZq9SWgfS1BFlNbsm6xplRjVlOCkXI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "08dacfca559e1d7da38f3cf05f1f45ee9bfd213c", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 0, + "narHash": "sha256-u+rxA79a0lyhG+u+oPBRtTDtzz8kvkc9a6SWSt9ekVc=", + "path": "/nix/store/0283cbhm47kd3lr9zmc5fvdrx9qkav8s-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "playwright": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1760833992, + "narHash": "sha256-CiVBf+Com8mwDexMVw6s4BIT1J1In/UNHvaqiZwSfIs=", + "owner": "pietdevries94", + "repo": "playwright-web-flake", + "rev": "d3996ee82c6bcdc4c9535b94068abaa2744a7411", + "type": "github" + }, + "original": { + "owner": "pietdevries94", + "repo": "playwright-web-flake", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "playwright": "playwright" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..b9aaf9fd8b --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "GoA ui-component dev environment"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs.playwright.url = "github:pietdevries94/playwright-web-flake"; + + outputs = { self, flake-utils, nixpkgs, playwright }: + flake-utils.lib.eachDefaultSystem (system: + let + overlay = final: prev: { + inherit (playwright.packages.${system}) playwright-test playwright-driver; + }; + pkgs = import nixpkgs { + inherit system; + overlays = [ overlay ]; + }; + in + { + devShells = { + default = pkgs.mkShell { + packages = [ + pkgs.nodejs_24 + pkgs.playwright-test + ]; + shellHook = '' + export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + export PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}" + ''; + }; + }; + }); +} From 3ebb431de1080560ac5f580324aadd9d404703d4 Mon Sep 17 00:00:00 2001 From: Chris Olsen Date: Wed, 29 Oct 2025 16:23:33 -0600 Subject: [PATCH 03/80] chore: add an ignored dependency when linting --- libs/web-components/.eslintrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/web-components/.eslintrc.json b/libs/web-components/.eslintrc.json index ca41bae324..1fec91a988 100644 --- a/libs/web-components/.eslintrc.json +++ b/libs/web-components/.eslintrc.json @@ -26,7 +26,7 @@ "error", { "ignoredFiles": ["{projectRoot}/vite.config.{js,ts,mjs,mts}"], - "ignoredDependencies": ["glob", "svelte", "@sveltejs/vite-plugin-svelte"] + "ignoredDependencies": ["glob", "date-fns", "svelte", "@sveltejs/vite-plugin-svelte"] } ] } From 709fd71293a95d181391960f6fd68435cea2bff7 Mon Sep 17 00:00:00 2001 From: syedszeeshan <47701214+syedszeeshan@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:36:59 -0400 Subject: [PATCH 04/80] fix(#1813): date picker width prop --- .../lib/components/date-picker/date-picker.ts | 2 + .../specs/datepicker.browser.spec.tsx | 131 ++++++++++++++++++ .../src/lib/date-picker/date-picker.tsx | 4 + .../components/date-picker/DatePicker.svelte | 112 ++++++++++----- .../date-picker/date-picker.spec.ts | 40 ++++++ .../src/components/popover/Popover.svelte | 13 +- 6 files changed, 265 insertions(+), 37 deletions(-) diff --git a/libs/angular-components/src/lib/components/date-picker/date-picker.ts b/libs/angular-components/src/lib/components/date-picker/date-picker.ts index 1e315a65ee..0006a9b4b2 100644 --- a/libs/angular-components/src/lib/components/date-picker/date-picker.ts +++ b/libs/angular-components/src/lib/components/date-picker/date-picker.ts @@ -35,6 +35,7 @@ import { GoabControlValueAccessor } from "../base.component"; [attr.relative]="relative" [attr.type]="type" [attr.testid]="testId" + [attr.width]="width" [attr.mt]="mt" [attr.mb]="mb" [attr.ml]="ml" @@ -63,6 +64,7 @@ export class GoabDatePicker extends GoabControlValueAccessor implements OnInit { * @deprecated This property has no effect and will be removed in a future version */ @Input() relative?: boolean; + @Input() width?: string; @Output() onChange = new EventEmitter(); diff --git a/libs/react-components/specs/datepicker.browser.spec.tsx b/libs/react-components/specs/datepicker.browser.spec.tsx index ce8a7a531c..eaa4c33af2 100644 --- a/libs/react-components/specs/datepicker.browser.spec.tsx +++ b/libs/react-components/specs/datepicker.browser.spec.tsx @@ -144,6 +144,137 @@ describe("DatePicker", () => { expect(inputEl.disabled).toBe(true); }) }); + + describe("Width property", () => { + it("applies custom width with px units", async () => { + const Component = () => { + return ; + }; + + const result = render(); + const input = result.getByTestId("calendar-input"); + + await vi.waitFor(() => { + // Check the input element's computed style + const computedStyle = window.getComputedStyle(input.element()); + const inputWidth = parseFloat(computedStyle.width); + + // The width should be close to 400px (the underlying goa-input component handles the width) + expect(inputWidth).toBeGreaterThan(300); + expect(inputWidth).toBeLessThan(450); + }); + }); + + it("applies custom width with ch units", async () => { + const Component = () => { + return ; + }; + + const result = render(); + const input = result.getByTestId("calendar-input"); + + await vi.waitFor(() => { + // Check computed width is applied (browser converts ch to px) + const computedStyle = window.getComputedStyle(input.element()); + expect(computedStyle.width).toMatch(/^\d+(\.\d+)?px$/); + + // Should have a reasonable width for 25ch + const inputWidth = parseFloat(computedStyle.width); + expect(inputWidth).toBeGreaterThan(200); + expect(inputWidth).toBeLessThan(600); + }); + }); + + it("uses default width when not specified", async () => { + const Component = () => { + return ; + }; + + const result = render(); + const input = result.getByTestId("calendar-input"); + + await vi.waitFor(() => { + // Default width should be 16ch - check computed width + const computedStyle = window.getComputedStyle(input.element()); + const inputWidth = parseFloat(computedStyle.width); + + // 16ch should be around 150-300px depending on font + expect(inputWidth).toBeGreaterThan(100); + expect(inputWidth).toBeLessThan(400); + }); + }); + + it("applies width to input type datepicker container", async () => { + const Component = () => { + return ; + }; + + const result = render(); + + await vi.waitFor(() => { + // Select the host element and inspect shadow DOM container width + const host = result.container.querySelector("goa-date-picker") as HTMLElement; + + const shadow = host.shadowRoot as ShadowRoot | null; + const formItem = shadow.querySelector("goa-form-item") as HTMLElement | null; + + const computedStyle = window.getComputedStyle(formItem); + const containerWidth = parseFloat(computedStyle.width); + + // The width should be close to 500px + expect(containerWidth).toBeGreaterThan(490); + expect(containerWidth).toBeLessThan(510); + }); + }); + + it("supports percentage width units", async () => { + const Component = () => { + return ( +
+ +
+ ); + }; + + const result = render(); + const input = result.getByTestId("calendar-input"); + + await vi.waitFor(() => { + // Check computed width + const computedStyle = window.getComputedStyle(input.element()); + expect(computedStyle.width).toMatch(/^\d+(\.\d+)?px$/); + + // Should be a reasonable percentage of container + const inputWidth = parseFloat(computedStyle.width); + expect(inputWidth).toBeGreaterThan(50); + expect(inputWidth).toBeLessThan(800); + }); + }); + + it("maintains minimum width to ensure date display", async () => { + const Component = () => { + return ; + }; + + const result = render(); + const input = result.getByTestId("calendar-input"); + + await vi.waitFor(() => { + const inputEl = input.element() as HTMLInputElement; + + // Check that date value is displayed + expect(inputEl.value).toBeTruthy(); + expect(inputEl.value.length).toBeGreaterThan(0); + + // Check width is applied + const computedStyle = window.getComputedStyle(inputEl); + const inputWidth = parseFloat(computedStyle.width); + + // Should be wide enough to display date (20ch should be enough) + expect(inputWidth).toBeGreaterThan(150); + }); + }); + }); }); describe("Date Picker input type", () => { diff --git a/libs/react-components/src/lib/date-picker/date-picker.tsx b/libs/react-components/src/lib/date-picker/date-picker.tsx index e75b46e546..fb69a09fe0 100644 --- a/libs/react-components/src/lib/date-picker/date-picker.tsx +++ b/libs/react-components/src/lib/date-picker/date-picker.tsx @@ -16,6 +16,7 @@ interface WCProps extends Margins { relative?: string; disabled?: string; testid?: string; + width?: string; } declare module "react" { @@ -40,6 +41,7 @@ export interface GoabDatePickerProps extends Margins { */ relative?: boolean; disabled?: boolean; + width?: string; onChange?: (detail: GoabDatePickerOnChangeDetail) => void; } @@ -57,6 +59,7 @@ export function GoabDatePicker({ mb, ml, relative, + width, onChange, }: GoabDatePickerProps): JSX.Element { const ref = useRef(null); @@ -109,6 +112,7 @@ export function GoabDatePicker({ mb={mb} ml={ml} relative={relative ? "true" : undefined} + width={width} /> ); } diff --git a/libs/web-components/src/components/date-picker/DatePicker.svelte b/libs/web-components/src/components/date-picker/DatePicker.svelte index 2c0ce4014a..3020786f2e 100644 --- a/libs/web-components/src/components/date-picker/DatePicker.svelte +++ b/libs/web-components/src/components/date-picker/DatePicker.svelte @@ -8,7 +8,7 @@ /> + + + + diff --git a/.templates/web/src/app/Playground.svelte b/.templates/web/src/app/Playground.svelte new file mode 100644 index 0000000000..46962b165b --- /dev/null +++ b/.templates/web/src/app/Playground.svelte @@ -0,0 +1,6 @@ + + + +The web playground diff --git a/.templates/web/tsconfig.app.json b/.templates/web/tsconfig.app.json index 031e9c720f..5a6d3e61df 100644 --- a/.templates/web/tsconfig.app.json +++ b/.templates/web/tsconfig.app.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "../../dist/out-tsc", + "outDir": "../../../dist/out-tsc", "declaration": true, "types": ["node"] }, diff --git a/.templates/web/tsconfig.json b/.templates/web/tsconfig.json index af79c85a6c..715e5cd65f 100644 --- a/.templates/web/tsconfig.json +++ b/.templates/web/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "files": [], "compilerOptions": { "target": "ESNext", diff --git a/.templates/web/vite.config.mts b/.templates/web/vite.config.mts index 4ada67c6b1..8d35eda1ad 100644 --- a/.templates/web/vite.config.mts +++ b/.templates/web/vite.config.mts @@ -1,26 +1,27 @@ /// import { defineConfig } from "vite"; import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; -import { svelte } from '@sveltejs/vite-plugin-svelte'; +import { svelte } from "@sveltejs/vite-plugin-svelte"; export default defineConfig({ root: __dirname, - cacheDir: "../../node_modules/.vite/playground/web", + cacheDir: "../../node_modules/.vite/apps/prs-web", server: { - port: 4200, + port: 4202, host: "0.0.0.0", + // Enable SPA fallback for client-side routing + historyApiFallback: true, }, preview: { port: 4300, host: "localhost", + // Enable SPA fallback in preview mode too + historyApiFallback: true, }, - plugins: [ - nxViteTsPaths(), - svelte(), - ], + plugins: [nxViteTsPaths(), svelte()], // Uncomment this if you are using workers. // worker: { @@ -28,7 +29,7 @@ export default defineConfig({ // }, build: { - outDir: "../../dist/playground/web", + outDir: "../../dist/apps/prs-web", reportCompressedSize: true, commonjsOptions: { transformMixedEsModules: true, diff --git a/README.md b/README.md index 8590658a50..e5d744a892 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,12 @@ is designed to be used to help bring consistency to all Government of Alberta websites and web applications. It's also being designed to help ease the burden on designers and developers alike throughout the development process. -## Development environment - -Create .env file from .env.example - -```bash -if [ ! -f .env ]; then cp ./.env.example ./.env; fi -``` - ### Playground setup -Run the `pg-setup` file. +Run the `dev-setup` file. ```bash -npm run pg:setup +npm run dev:setup ``` You can then test the playground apps at `localhost:4200` by running: @@ -29,28 +21,9 @@ You can then test the playground apps at `localhost:4200` by running: npm run dev:watch # add one of the following -npm run serve:angular -npm run serve:react -npm run serve:web -``` - -### Multiple playgrounds - -Since the playground is not included in the CVS it is common to have playground -comment/uncomment code the `npm run pg:switch` script can automate this by switching -code that is out of sync with library code. To prevent having to continually -between playgrounds that are specific to the branch. - -To switch to a branch that doesn't yet exist, run the following - -```bash -npm run pg:switch new [branch-name] -``` - -To switch to an existing branch run the following - -```bash -npm run pg:switch [branch-name] +npm run serve:dev:angular +npm run serve:dev:react +npm run serve:dev:web ``` --- diff --git a/apps/prs/angular/.eslintrc.json b/apps/prs/angular/.eslintrc.json new file mode 100644 index 0000000000..96b5a50035 --- /dev/null +++ b/apps/prs/angular/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "abgov", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "abgov", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/apps/prs/angular/.gitignore b/apps/prs/angular/.gitignore new file mode 100644 index 0000000000..4d058db7df --- /dev/null +++ b/apps/prs/angular/.gitignore @@ -0,0 +1,9 @@ +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/apps/prs/angular/angular.json b/apps/prs/angular/angular.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/apps/prs/angular/angular.json @@ -0,0 +1 @@ +{} diff --git a/apps/prs/angular/project.json b/apps/prs/angular/project.json new file mode 100644 index 0000000000..4ace99a7fc --- /dev/null +++ b/apps/prs/angular/project.json @@ -0,0 +1,72 @@ +{ + "name": "angular-prs", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "abgov", + "sourceRoot": "apps/prs/angular/src", + "tags": [], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:application", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/prs/angular", + "index": "apps/prs/angular/src/index.html", + "browser": "apps/prs/angular/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/prs/angular/tsconfig.app.json", + "assets": [ + "apps/prs/angular/src/favicon.ico", + "apps/prs/angular/src/assets" + ], + "styles": ["apps/prs/angular/src/styles.css"], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "3mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "angular-prs:build:production" + }, + "development": { + "buildTarget": "angular-prs:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "angular-prs:build" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/apps/prs/angular/src/app/abgov-app.ts b/apps/prs/angular/src/app/abgov-app.ts new file mode 100644 index 0000000000..3ab16c2e39 --- /dev/null +++ b/apps/prs/angular/src/app/abgov-app.ts @@ -0,0 +1,17 @@ +import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import {} from "@abgov/angular-components"; + +@Component({ + standalone: true, + selector: "abgov-app", + template: ` +
+ +
+ `, + + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [CommonModule], +}) +export class AbgovAppComponent {} diff --git a/apps/prs/angular/src/app/app-routing.module.ts b/apps/prs/angular/src/app/app-routing.module.ts new file mode 100644 index 0000000000..80f2be5250 --- /dev/null +++ b/apps/prs/angular/src/app/app-routing.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { AbgovAppComponent } from "./abgov-app"; + +const routes: Routes = [ + { path: "", component: AbgovAppComponent }, + // add custom paths here +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], +}) +export class AppRoutingModule {} diff --git a/apps/prs/angular/src/app/app.component.ts b/apps/prs/angular/src/app/app.component.ts new file mode 100644 index 0000000000..4195567bae --- /dev/null +++ b/apps/prs/angular/src/app/app.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from "@angular/core"; +import { RouterOutlet } from "@angular/router"; + +@Component({ + selector: "abgov-root", + template: "", + standalone: true, + imports: [RouterOutlet], +}) +export class AppComponent implements OnInit { + ngOnInit() { + console.log("Hello from Angular"); + } +} diff --git a/apps/prs/angular/src/app/app.module.ts b/apps/prs/angular/src/app/app.module.ts new file mode 100644 index 0000000000..15a4f9fa89 --- /dev/null +++ b/apps/prs/angular/src/app/app.module.ts @@ -0,0 +1,31 @@ +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; + +import { BrowserModule } from "@angular/platform-browser"; +import { AppComponent } from "./app.component"; +import { AngularComponentsModule } from "@abgov/angular-components"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { NgForOf, NgIf } from "@angular/common"; +import { AbgovAppComponent } from "./abgov-app"; +import { AppRoutingModule } from "./app-routing.module"; +import "@abgov/web-components"; + +@NgModule({ + declarations: [], + imports: [ + AppComponent, + AngularComponentsModule, + AbgovAppComponent, + BrowserModule, + AppRoutingModule, + FormsModule, + NgForOf, + NgIf, + NoopAnimationsModule, + ReactiveFormsModule, + ], + providers: [], + bootstrap: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], +}) +export class AppModule {} diff --git a/apps/prs/angular/src/assets/.gitkeep b/apps/prs/angular/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/prs/angular/src/favicon.ico b/apps/prs/angular/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA + + + + + angular + + + + + + + + + + + + diff --git a/apps/prs/angular/src/main.ts b/apps/prs/angular/src/main.ts new file mode 100644 index 0000000000..c8de31031e --- /dev/null +++ b/apps/prs/angular/src/main.ts @@ -0,0 +1,6 @@ +import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; +import { AppModule } from "./app/app.module"; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/apps/prs/angular/src/styles.css b/apps/prs/angular/src/styles.css new file mode 100644 index 0000000000..963ef40b19 --- /dev/null +++ b/apps/prs/angular/src/styles.css @@ -0,0 +1,6 @@ +/* You can add global styles to this file, and also import other style files */ +@import "../../../../dist/libs/web-components/index.css"; + +:root { + --goa-space-fill: 32ch; +} diff --git a/apps/prs/angular/tsconfig.app.json b/apps/prs/angular/tsconfig.app.json new file mode 100644 index 0000000000..fff4a41d44 --- /dev/null +++ b/apps/prs/angular/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/prs/angular/tsconfig.editor.json b/apps/prs/angular/tsconfig.editor.json new file mode 100644 index 0000000000..4ee6393404 --- /dev/null +++ b/apps/prs/angular/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": [] + } +} diff --git a/apps/prs/angular/tsconfig.json b/apps/prs/angular/tsconfig.json new file mode 100644 index 0000000000..b94f8837df --- /dev/null +++ b/apps/prs/angular/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.editor.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/prs/react/.eslintrc.json b/apps/prs/react/.eslintrc.json new file mode 100644 index 0000000000..75b85077de --- /dev/null +++ b/apps/prs/react/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/prs/react/index.html b/apps/prs/react/index.html new file mode 100644 index 0000000000..2d9b4c6bd8 --- /dev/null +++ b/apps/prs/react/index.html @@ -0,0 +1,27 @@ + + + + + + PlaygroundReact + + + + + + + + + + +
+ + + + diff --git a/apps/prs/react/project.json b/apps/prs/react/project.json new file mode 100644 index 0000000000..5d029099c5 --- /dev/null +++ b/apps/prs/react/project.json @@ -0,0 +1,61 @@ +{ + "name": "react-prs", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "apps/prs/react/src", + "projectType": "application", + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/apps/prs/react" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "serve": { + "executor": "@nx/vite:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "react-prs:build" + }, + "configurations": { + "development": { + "buildTarget": "react-prs:build:development", + "hmr": true + }, + "production": { + "buildTarget": "react-prs:build:production", + "hmr": false + } + } + }, + "preview": { + "executor": "@nx/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "react-prs:build" + }, + "configurations": { + "development": { + "buildTarget": "react-prs:build:development" + }, + "production": { + "buildTarget": "react-prs:build:production" + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + }, + "tags": [] +} diff --git a/apps/prs/react/public/favicon.ico b/apps/prs/react/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA(); + const [basicModalOpen, setBasicModalOpen] = useState(); + const [basicModal2Open, setBasicModal2Open] = useState(); + const [basicModal3Open, setBasicModal3Open] = useState(); + const [contentModalOpen, setContentModalOpen] = useState(); + const [contentModalScrollOpen, setContentModalScrollOpen] = useState(); + const [contentModal2Open, setContentModal2Open] = useState(); + const [NoHeaderModalOpen, setNoHeaderModalOpen] = useState(); + const [step, setStep] = useState(-1); + const [step2, setStep2] = useState(-1); + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => {}; + + function radio1(name: string, value: string) { + console.log("onChange", name, value); + } + + function radio2(name: string, value: string) { + console.log("onChange", name, value); + } + + function radio3(name: string, value: string) { + console.log("onChange", name, value); + } + + const popovertarget = ( + + Click me + + ); + const [value, setValue] = useState(""); + + function onChangeDropdown(detail: GoabDropdownOnChangeDetail) { + setValue(detail.value as string); + } + + function onChangeTextArea(detail: GoabTextAreaOnChangeDetail) { + console.log(detail.value); + } + + interface User { + firstName: string; + lastName: string; + age: number; + } + + const [users, setUsers] = useState([]); + const [open, setOpen] = useState(false); + const [openNoActions, setOpenNoActions] = useState(false); + const [position, setPosition] = useState("left"); + const [dateTaken, setDateTaken] = useState("today"); + const [hasActionsSlot, setActionsSlot] = useState("y"); + + function openDrawer() { + if (hasActionsSlot === "n") { + setOpenNoActions(true); + } else { + setOpen(true); + } + } + + const _users: User[] = [ + { + firstName: "Christian", + lastName: "Batz", + age: 18, + }, + { + firstName: "Brain", + lastName: "Wisozk", + age: 19, + }, + { + firstName: "Neha", + lastName: "Jones", + age: 23, + }, + { + firstName: "Tristin", + lastName: "Buckridge", + age: 31, + }, + ]; + React.useEffect(() => { + setUsers(_users); + }, []); + + function sortData(sortBy: string, sortDir: number) { + const _users = [...users]; + _users.sort((a: any, b: any) => { + return (a[sortBy] > b[sortBy] ? 1 : -1) * sortDir; + }); + setUsers(_users); + } + + const containeractions = ( + + + + Edit + + + ); + + const containeractionsinverse = ( + + + + Edit + + + ); + + return ( + + {/* Main page content here */} +
+ + + + onChange(detail.tab)}> + + + --- + + + {/* ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Components + + } + > + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Badge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Button + + + + Primary + Secondary + Tertiary + + + + Primary + + Secondary + + + Tertiary + + + + + Primary + + Secondary + + + Tertiary + + + + + Compact primary + + Compact secondary + + + Compact tertiary + + + + + + Compact primary + + + Compact secondary + + + Compact tertiary + + + + + + Compact primary + + + Compact secondary + + + Compact tertiary + + + + + Destructive primary + + Destructive secondary + + + Destructive tertiary + + + + + + Destructive primary + + + Destructive secondary + + + Destructive tertiary + + + + + + Destructive primary + + + Destructive secondary + + + Destructive tertiary + + + + + + Compact destructive primary + + + Compact destructive secondary + + + Compact destructive tertiary + + + + + + Compact destructive primary + + + Compact destructive secondary + + + Compact destructive tertiary + + + + + + Compact destructive primary + + + Compact destructive secondary + + + Compact destructive tertiary + + + + Start + + + + Inverse (experimental) + + + + Regular primary + Regular secondary + Regular tertiary + + +
+ + Inverse primary + + Inverse secondary + + + Inverse tertiary + + +
+ + + Disabled + + + + Primary + + Secondary + + + Tertiary + + + + + + Primary + + + Secondary + + + Tertiary + + + + + + Primary + + + Secondary + + + Tertiary + + + + + + Cmpact primary + + + Compact secondary + + + Compact tertiary + + + + + + Compact primary + + + Compact secondary + + + Compact tertiary + + + + + + Compact primary + + + Compact secondary + + + Compact tertiary + + + + + + Destructive primary + + + Destructive secondary + + + Destructive tertiary + + + + + + Destructive primary + + + Destructive secondary + + + Destructive tertiary + + + + + + Destructive primary + + + Destructive secondary + + + Destructive tertiary + + + + + + Compact destructive primary + + + Compact destructive secondary + + + Compact destructive tertiary + + + + + + Compact destructive primary + + + Compact destructive secondary + + + Compact destructive tertiary + + + + + + Compact destructive primary + + + Compact destructive secondary + + + Compact destructive tertiary + + + + + + Disabled start + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Button group + + + + Default + Button + Group + + + + Compact + + Button + + + Group + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Checkbox + + + + + + + + Help text with a link. + + } + > + + + Help text with a link. + + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Container + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Non-interactive with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Non-interactive with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Non-interactive filled + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Interactive with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Interactive with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Interactive filled + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Info with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Info with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Info filled + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Error with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Error with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Error filled + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Success with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Success with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Success filled + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Important with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Important with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Important filled + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Non-interactive, compact with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Non-interactive, compact with accent + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + + Non-interactive, compact + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam at + risus et magna interdum vestibulum in at ligula. + + + + + + + Button + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Date picker + + + + + + + + + + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Detail + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vel + lacinia metus, sed sodales lectus. Aliquam sed volutpat velit. + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vel + lacinia metus, sed sodales lectus. Aliquam sed volutpat velit. + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Divider + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Dropdown + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + File upload + + + + + { + /* do nothing */ + }} + /> + + + + { + /** do nothing **/ + }} + /> + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Form item + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + /** do nothing **/ + }} + /> + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Form stepper + + + + setStep(e.step)}> + + + + + + +
Page 1 content
+
Page 2 content
+
Page 3 content
+
Page 4 content
+
+ + setStep2(e.step)}> + + + + + + + +
Page 1 content
+
Page 2 content
+
Page 3 content
+
Page 4 content
+
Page 5 content
+
+
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Hero Banner + + + + + Resources are available to help Alberta entrepreneurs and small + businesses start, grow and succeed. + + Call to action + + + + + Resources are available to help Alberta entrepreneurs and small + businesses start, grow and succeed. + + Call to action + + + + + Resources are available to help Alberta entrepreneurs and small + businesses start, grow and succeed. + + Call to action + + + + + Resources are available to help Alberta entrepreneurs and small + businesses start, grow and succeed. + + Call to action + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Icon + + + + Tshirt sizing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Number sizing + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inverted + +
+ + + + + + +
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Icon button + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + + + +
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Input + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Search + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Link + + + + Link with a leading icon + + Link with a trailing icon + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Microsite header + + + + + + + + + + + Feedback link + + + + + + + + + + Version number + + + + + + + + + + Feedback link and Version number + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Modal + + + + setDestructiveModalOpen(true)} + > + Delete my application + + + + setDestructiveModalOpen(false)} + > + Cancel + + { + setDestructiveModalOpen(false); + }} + > + Delete application + + + } + > +

This action cannot be undone.

+
+ + setBasicModalOpen(true)}> + Open basic modal with close + + + setBasicModalOpen(false)} + > +

+ This is meant to be dismissed, the user can click outside of the modal + or click the close button in the top right corner. +

+
+ + setBasicModal2Open(true)}> + Open basic modal with actions + + + + setBasicModal2Open(false)} + > + Cancel + + { + setBasicModal2Open(false); + }} + > + Continue + + + } + > +

+ This is meant to make the user choose an option in order to continue. +

+
+ + setContentModalOpen(true)}> + Open modal with lots of content and actions + + + + setContentModalOpen(false)} + > + Cancel + + { + setContentModalOpen(false); + }} + > + Continue + + + } + > +

+ This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal + scroll.This is a lot of content that make the modal scroll. This is a + lot of content that make the modal scroll. This is a lot of content + that make the modal scroll. This is a lot of content that make the + modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. +

+ + ): void { + throw new Error("Function not implemented."); + }} + > + {" "} +
+ + setContentModal2Open(true)}> + Open modal with lots of content and close button + + + setContentModal2Open(false)} + > +

+ This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. +

+
+ + setBasicModal3Open(true)}> + Open basic modal with actions and close button + + + setBasicModal3Open(false)} + actions={ + + setBasicModal3Open(false)} + > + Cancel + + { + setBasicModal3Open(false); + }} + > + Continue + + + } + > +

+ The use can dismiss the modal by clicking outside of the modal, + clicking the close button, or choose an option to continue.{" "} +

+
+ + setNoHeaderModalOpen(true)}> + Open modal with no header + + + setNoHeaderModalOpen(false)} */ + actions={ + + setNoHeaderModalOpen(false)} + > + Cancel + + { + setNoHeaderModalOpen(false); + }} + > + Continue + + + } + > +

+ This is a modal with no header. Choose an option to continue. Lorem + ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse + facilisis quam ac massa commodo fringilla. Sed gravida elit urna, vel + rhoncus velit ullamcorper vitae. Phasellus ullamcorper enim et leo + dignissim, sed dignissim mi varius. +

+
+
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Notification banner + + + + + Notification banner message + + + + Notification banner message that is really long and eventually it wraps + around the screen because it's so long that it needs to wrap around the + screen + + + + Notification banner message + + + + Notification banner message + + + + Notification banner message + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Pagination + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Popover + + + + +

This is a popover

+ It can be used for a number of different contexts. +
+
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Progress indicator + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Radio + + + + + radio1(e.name, e.value)} + > + + + + + + + + + radio2(e.name, e.value)} + > + + + + + + + + + radio2(e.name, e.value)} + > + +
+ + {" "} + {" "} +
+
+ + {" "} +
+ + {" "} + {" "} +
+
+
+
+ + + radio2(e.name, e.value)} + > + + + + + + + + + radio3(e.name, e.value)} + > + + Help text with a link. + + } + /> + + + + + + + radio3(e.name, e.value)} + > + + Help text with a link. + + } + /> + + + + + + + radio2(e.name, e.value)} + > + + + + + + + + + radio2(e.name, e.value)} + > + + + + + + + radio2(e.name, e.value)} + > + + + + + + + + + radio2(e.name, e.value)} + > + + + + + + +
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Scroll bar + + + + setContentModalScrollOpen(true)} + > + Open modal with lots of content to see scroll bar + + + + setContentModalScrollOpen(false)} + > + Cancel + + { + setContentModalScrollOpen(false); + }} + > + Continue + + + } + > +

+ This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. This is a lot of content that make + the modal scroll. This is a lot of content that make the modal scroll. + This is a lot of content that make the modal scroll. This is a lot of + content that make the modal scroll. +

+
+
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Side menu + + + + + + +
+ + + This is a side menu heading + + This is a side menu item + This is another side menu item + + + This is another side menu heading + + Side menu item + Side menu item + + } + > + Side menu heading + + + Foo + Bar + + + Foo + Bar + + Foo + Bar + + Foo + Bar + + + + +
+ + +
+
+ + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Table + + + + + + + Status + Text + Number + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + + + Status + Text + Number + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + sortData(e.sortBy, e.sortDir)}> + + + + + First name + + + + + Last name + + + + + Age + + + + + + {users.map((user) => ( + + {user.firstName} + {user.lastName} + {user.age} + + ))} + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Tabs + + + + + + Tab 1 content: Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. + + + Tab 2 content: Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. + + + Tab 3 content: Lorem ipsum dolor sit amet, consectetur adipiscing + elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. + + + + + + + + + + No content + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Text area + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Component ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ */} + + + Tooltip + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* TAB ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */} + + + + + + + + + + + + + {/* TODO: add file cards on upload file. eslint-disable-next-line @typescript-eslint/no-empty-function */} + console.log(file)} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* TAB ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */} + + + + + + Status + Text + Number + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + + Text that runs really really really really really really really + really really really really really really really really really + really really really really really really really really really + really really really really really really really really really + really really long{" "} + + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + + + Status + Text + Number + Action + + + + + + + + + Text that runs really really really really really really really + really really really really really really really really really + really really really really really really really really really + really really really really really really really really really + really really long + + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + + Lorem ipsum + 1234567890 + + + Action + + + + + + + sortData(e.sortBy, e.sortDir)}> + + + First name + + + Last name + + + + + Age + + + + + + {users.map((user) => ( + + {user.firstName} + {user.lastName} + {user.age} + + ))} + + + + + + {/* TAB ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */} + + + + {" "} + Back link{" "} + + + {/* Apply max width to input, not form item for fixed width inputs. */} + + + Heading extra large as page h1 + + + + Heading large + + + + Heading medium + + + + Heading small + + + + Heading extra small + + + + Body large Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec + rutrum dignissim erat quis iaculis. + + + + Body medium text, lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec rutrum dignissim erat quis iaculis. + + + + Body small text, lorem ipsum dolor sit amet, consectetur adipiscing elit. + Donec rutrum dignissim erat quis iaculis. + + + + Body extra small text, lorem ipsum dolor sit amet, consectetur adipiscing + elit. Donec rutrum dignissim erat quis iaculis. + + + + Text component with margin top and bottom + + + + Text component with margin top + + + + Text component with margin bottom + + + + {/* TAB ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */} + + + + Image + + + + + + + + Text + + + + + + + + Title + + + + + + + + Text-small + + + + + + + + Avatar + + + + + + + + Header + + + + + + + + Paragraph + + + + + + + + Thumbnail + + + + + + + + Card + + + + + + + + Profile + + + + + + + +
+
+
+ +
+ + + Link 123we + Link 2 + Link 3 + Other thing + + + Link 123we + Link 2 + Link 3 + Other thing + + + Meta link + Meta link + Meta link + Meta link + + + + + + {" "} + + + + + Meta link + Meta link + Meta link + Meta link + + + + + + Link 1 + Link 2 + Link 3 + Other thing + + +
+
+ ); +} + +export default AllComponents; diff --git a/apps/prs/react/src/app/app.module.css b/apps/prs/react/src/app/app.module.css new file mode 100644 index 0000000000..7b88fbabf8 --- /dev/null +++ b/apps/prs/react/src/app/app.module.css @@ -0,0 +1 @@ +/* Your styles goes here. */ diff --git a/apps/prs/react/src/app/app.tsx b/apps/prs/react/src/app/app.tsx new file mode 100644 index 0000000000..26d7e15f19 --- /dev/null +++ b/apps/prs/react/src/app/app.tsx @@ -0,0 +1,32 @@ +import { Outlet } from "react-router-dom"; + +import { + GoabAppFooter, + GoabAppHeader, + GoabMicrositeHeader, + GoabOneColumnLayout, +} from "@abgov/react-components"; +import "@abgov/style"; + + +export function App() { + return ( + +
+ + + View All + Issues + +
+
+ +
+
+ +
+
+ ); +} + +export default App; diff --git a/apps/prs/react/src/assets/.gitkeep b/apps/prs/react/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/prs/react/src/main.tsx b/apps/prs/react/src/main.tsx new file mode 100644 index 0000000000..1d380c7664 --- /dev/null +++ b/apps/prs/react/src/main.tsx @@ -0,0 +1,26 @@ +import { StrictMode } from "react"; + +import * as ReactDOM from "react-dom/client"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import "@abgov/web-components"; +import App from "./app/app"; +import AllComponents from "./app/all"; + +const root = ReactDOM.createRoot( + document.getElementById("root") as HTMLElement, +); + +root.render( + + + + }> + + } /> + { /* add new routes here */ } + + + + + , +); diff --git a/apps/prs/react/src/routes/issues/.gitkeep b/apps/prs/react/src/routes/issues/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/prs/react/src/styles.css b/apps/prs/react/src/styles.css new file mode 100644 index 0000000000..90d4ee0072 --- /dev/null +++ b/apps/prs/react/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/prs/react/tsconfig.app.json b/apps/prs/react/tsconfig.app.json new file mode 100644 index 0000000000..2a5ff5ebd5 --- /dev/null +++ b/apps/prs/react/tsconfig.app.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "vite/client" + ] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] +} diff --git a/apps/prs/react/tsconfig.json b/apps/prs/react/tsconfig.json new file mode 100644 index 0000000000..4467b39320 --- /dev/null +++ b/apps/prs/react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "types": ["vite/client"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/apps/prs/react/vite.config.ts b/apps/prs/react/vite.config.ts new file mode 100644 index 0000000000..f77894c6f3 --- /dev/null +++ b/apps/prs/react/vite.config.ts @@ -0,0 +1,43 @@ +/// +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react-swc"; +import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; + +export default defineConfig({ + root: __dirname, + cacheDir: "../../../node_modules/.vite/playground/react", + + server: { + port: 4201, + host: "0.0.0.0", + // Enable SPA fallback for client-side routing + historyApiFallback: true, + }, + + preview: { + port: 4300, + host: "localhost", + // Enable SPA fallback in preview mode too + historyApiFallback: true, + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: "../../../dist/apps/prs/react", + reportCompressedSize: true, + minify: false, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + define: { + "import.meta.vitest": undefined, + }, +}); diff --git a/apps/prs/web/.babelrc b/apps/prs/web/.babelrc new file mode 100644 index 0000000000..f2f3806745 --- /dev/null +++ b/apps/prs/web/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@nx/js/babel"] +} diff --git a/apps/prs/web/.eslintrc.json b/apps/prs/web/.eslintrc.json new file mode 100644 index 0000000000..3456be9b90 --- /dev/null +++ b/apps/prs/web/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/apps/prs/web/.swcrc b/apps/prs/web/.swcrc new file mode 100644 index 0000000000..a2d5b04f47 --- /dev/null +++ b/apps/prs/web/.swcrc @@ -0,0 +1,8 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript" + }, + "target": "es2016" + } +} diff --git a/apps/prs/web/index.html b/apps/prs/web/index.html new file mode 100644 index 0000000000..34d5db9f03 --- /dev/null +++ b/apps/prs/web/index.html @@ -0,0 +1,21 @@ + + + + + + PlaygroundWeb + + + + + + + + + + +
+ + + + diff --git a/apps/prs/web/project.json b/apps/prs/web/project.json new file mode 100644 index 0000000000..3e590fd061 --- /dev/null +++ b/apps/prs/web/project.json @@ -0,0 +1,68 @@ +{ + "name": "web-prs", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/prs/web/src", + "tags": [], + "implicitDependencies": [ + "web-components" + ], + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": [ + "{options.outputPath}" + ], + "defaultConfiguration": "production", + "options": { + "outputPath": "dist/apps/prs/web" + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production" + } + } + }, + "serve": { + "executor": "@nx/vite:dev-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "web-prs:build" + }, + "configurations": { + "development": { + "buildTarget": "web-prs:build:development", + "hmr": true + }, + "production": { + "buildTarget": "web-prs:build:production", + "hmr": false + } + } + }, + "preview": { + "executor": "@nx/vite:preview-server", + "defaultConfiguration": "development", + "options": { + "buildTarget": "web-prs:build" + }, + "configurations": { + "development": { + "buildTarget": "web-prs:build:development" + }, + "production": { + "buildTarget": "web-prs:build:production" + } + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + } + } +} diff --git a/apps/prs/web/public/favicon.ico b/apps/prs/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA + Goab Component Playground + + + + + + + diff --git a/apps/prs/web/src/assets/.gitkeep b/apps/prs/web/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/prs/web/src/main.js b/apps/prs/web/src/main.js new file mode 100644 index 0000000000..38af7076f3 --- /dev/null +++ b/apps/prs/web/src/main.js @@ -0,0 +1,11 @@ +import "@abgov/web-components"; +import App from "./app/App.svelte"; + +let app; +const target = document.getElementById("app"); +if (target) { + app = new App({ target }); +} else { + console.error("Target element not found"); +} +export default app; diff --git a/apps/prs/web/src/routes/2333.svelte b/apps/prs/web/src/routes/2333.svelte new file mode 100644 index 0000000000..10806a83e0 --- /dev/null +++ b/apps/prs/web/src/routes/2333.svelte @@ -0,0 +1,19 @@ + + +
+

Route 2333

+

This is the content for the /2333 route.

+
+ + diff --git a/apps/prs/web/src/styles.css b/apps/prs/web/src/styles.css new file mode 100644 index 0000000000..90d4ee0072 --- /dev/null +++ b/apps/prs/web/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/prs/web/src/svelte-shim.d.ts b/apps/prs/web/src/svelte-shim.d.ts new file mode 100644 index 0000000000..393ff5ef70 --- /dev/null +++ b/apps/prs/web/src/svelte-shim.d.ts @@ -0,0 +1,5 @@ +declare module '*.svelte' { + import type { ComponentType } from 'svelte'; + const component: ComponentType; + export default component; +} diff --git a/apps/prs/web/svelte.config.mjs b/apps/prs/web/svelte.config.mjs new file mode 100644 index 0000000000..3bce8eaa6c --- /dev/null +++ b/apps/prs/web/svelte.config.mjs @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +}; diff --git a/apps/prs/web/tsconfig.app.json b/apps/prs/web/tsconfig.app.json new file mode 100644 index 0000000000..5a6d3e61df --- /dev/null +++ b/apps/prs/web/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "src/**/*.svelte"], + "exclude": ["vite.config.mts", "svelte.config.mjs", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/apps/prs/web/tsconfig.json b/apps/prs/web/tsconfig.json new file mode 100644 index 0000000000..715e5cd65f --- /dev/null +++ b/apps/prs/web/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "types": ["vite/client"] + }, + "include": ["src"], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/apps/prs/web/vite.config.mts b/apps/prs/web/vite.config.mts new file mode 100644 index 0000000000..8d35eda1ad --- /dev/null +++ b/apps/prs/web/vite.config.mts @@ -0,0 +1,38 @@ +/// +import { defineConfig } from "vite"; +import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +export default defineConfig({ + root: __dirname, + cacheDir: "../../node_modules/.vite/apps/prs-web", + + server: { + port: 4202, + host: "0.0.0.0", + // Enable SPA fallback for client-side routing + historyApiFallback: true, + }, + + preview: { + port: 4300, + host: "localhost", + // Enable SPA fallback in preview mode too + historyApiFallback: true, + }, + + plugins: [nxViteTsPaths(), svelte()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: "../../dist/apps/prs-web", + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index 20ff1ccc96..acf5673f19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "react-dom": "19.0.0", "react-router-dom": "6.11.2", "rxjs": "~7.8.0", + "svelte-routing": "^2.13.0", "tslib": "^2.3.0", "zone.js": "0.15.1" }, @@ -47773,6 +47774,12 @@ "@types/estree": "^1.0.6" } }, + "node_modules/svelte-routing": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/svelte-routing/-/svelte-routing-2.13.0.tgz", + "integrity": "sha512-/NTxqTwLc7Dq306hARJrH2HLXOBtKd7hu8nxgoFDlK0AC4SOKnzisiX/9m8Uksei1QAWtlAEdF91YphNM8iDMg==", + "license": "MIT" + }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", diff --git a/package.json b/package.json index 9c5a59cc89..b705d76b20 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,15 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { - "serve:web": "nx run web:serve:development", - "serve:react": "nx run react:serve:development", - "serve:angular": "nx run angular:serve:development", + "serve:dev:web": "nx run web-dev:serve:development", + "serve:dev:react": "nx run react-dev:serve:development", + "serve:dev:angular": "nx run angular-dev:serve:development", + "serve:prs:web": "nx run web-prs:serve:development", + "serve:prs:react": "nx run react-prs:serve:development", + "serve:prs:angular": "nx run angular-prs:serve:development", "dev:watch": "vite build --watch libs/web-components", - "build": "nx affected --base=dev -t build --exclude=apps/* --exclude=playground/*", - "build:prod": "nx run-many -t build --all --prod --exclude=apps/* --exclude=playground/*", + "build": "nx affected --base=dev -t build --exclude=apps/*", + "build:prod": "nx run-many -t build --all --prod --exclude=apps/*", "build:vscode-doc": "node libs/web-components/custom-element-manifest-analyze.js", "pretest:pr": "npx nx run common:build && npx nx run web-components:build && npx nx run react-components:build", "test:pr": "vitest --run --project=*-unit && vitest --run --project=*-headless && nx test angular-components", @@ -18,8 +21,7 @@ "test:angular": "nx test angular-components", "lint": "nx run-many --target=lint --exclude=angular --exclude=react --exclude=web", "validate": "npm run build && npm run lint && vitest --run", - "pg:setup": "./scripts/pg-setup", - "pg:switch": "./scripts/pg-switch" + "dev:setup": "bash ./scripts/dev-setup" }, "private": true, "devDependencies": { @@ -121,6 +123,7 @@ "react-dom": "19.0.0", "react-router-dom": "6.11.2", "rxjs": "~7.8.0", + "svelte-routing": "^2.13.0", "tslib": "^2.3.0", "zone.js": "0.15.1" }, diff --git a/scripts/dev-setup b/scripts/dev-setup new file mode 100644 index 0000000000..d26292617f --- /dev/null +++ b/scripts/dev-setup @@ -0,0 +1,13 @@ +#!/usr/bin/bash + +if [ ! -f .env ]; then cp ./.env.example ./.env; fi + +if [ -d "./apps/dev" ]; then + echo "./apps/dev already exists. To re-create manually delete the current folder" + exit 0 +fi + +mkdir ./apps/dev +cp -r ./.templates/* ./apps/dev + +echo "Dev env created!" diff --git a/scripts/pg-setup b/scripts/pg-setup deleted file mode 100755 index c601f022d2..0000000000 --- a/scripts/pg-setup +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/bash - -if [ -d playground ]; then - echo "Playground folder already exists!" - exit 1 -fi - -mkdir ./playground -cp -r ./_templates/* ./playground diff --git a/scripts/pg-switch b/scripts/pg-switch deleted file mode 100755 index 7fde9b75d9..0000000000 --- a/scripts/pg-switch +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -if [ "$1" = "new" ]; then - target=$2 - option=$1 -else - target=$1 - option="" -fi - -# Ensure local repo is in sync with remote () -git fetch upstream - -if [ ! -n "$target" ]; then - echo "pg-switch [target] => Target repository is required" - exit 1 -fi - -if [ -n "$(git status -s -uno)" ]; then - echo "Uncommitted changes exist, commit or stash changes before proceeding" - exit 1 -fi - -# Create new branch if specified -if [ "$option" = "new" ]; then - git branch "$target" -elif [ -z "$(git branch | grep "$target")" ]; then - echo "[$target] is an invalid target branch name" - exit 1 -fi - -# Get the current git branch name -branch=$(git branch --show-current) - -# Check if branch equals target -if [ "$branch" = "$target" ]; then - echo "Already on target branch: $target" - exit 0 -fi - -# Check if .playgrounds directory exists, create it if it doesn't -if [ ! -d ".playgrounds/$branch" ]; then - mkdir -p .playgrounds/"$branch" -fi - -# Move the existing playground to the .playgrounds dir -if [ -d "playground" ]; then - mv playground/* .playgrounds/"$branch" -fi - -# Move the target playground to the `playground` dir to allow it to tested -targetdir=".playgrounds/$target" -if [ -n "$target" ] && [ -d "$targetdir" ]; then - cp -r "$targetdir"/* playground - rm -rf "$targetdir" -else - cp -r .templates/* playground -fi - -# Checkout the defined target repo -git checkout "$target" diff --git a/vitest.config.mjs b/vitest.config.mjs index 57aa73cd6e..8e5e8fb61c 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -7,7 +7,7 @@ export default defineConfig({ retries: 3, exclude: [ "**/node_modules", - "playground", + "apps", "libs/angular-components", // run angular via nx ], }, From 516c6f74577b14d767e46dff1a74b05ea05a94d5 Mon Sep 17 00:00:00 2001 From: syedszeeshan <47701214+syedszeeshan@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:36:13 -0500 Subject: [PATCH 06/80] fix(#3123): set badge icon prop default to false --- .../src/lib/components/badge/badge.spec.ts | 46 +++++++++++++++++-- .../src/lib/components/badge/badge.ts | 3 +- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/libs/angular-components/src/lib/components/badge/badge.spec.ts b/libs/angular-components/src/lib/components/badge/badge.spec.ts index cfaa3e1fc2..9862bbaa19 100644 --- a/libs/angular-components/src/lib/components/badge/badge.spec.ts +++ b/libs/angular-components/src/lib/components/badge/badge.spec.ts @@ -33,16 +33,28 @@ class TestBadgeComponent { mr?: Spacing; } +@Component({ + standalone: true, + imports: [GoabBadge], + template: ` `, +}) +class TestBadgeNoIconComponent { + type?: GoabBadgeType; + content?: string; +} + describe("GoABBadge", () => { let fixture: ComponentFixture; let component: TestBadgeComponent; beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ - imports: [GoabBadge, TestBadgeComponent], + imports: [GoabBadge, TestBadgeComponent, TestBadgeNoIconComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); + })); + it("should render and set the props correctly", fakeAsync(() => { fixture = TestBed.createComponent(TestBadgeComponent); component = fixture.componentInstance; component.type = "information"; @@ -58,9 +70,6 @@ describe("GoABBadge", () => { fixture.detectChanges(); tick(); fixture.detectChanges(); - })); - - it("should render and set the props correctly", () => { const badgeElement = fixture.debugElement.query(By.css("goa-badge")).nativeElement; expect(badgeElement.getAttribute("type")).toBe("information"); expect(badgeElement.getAttribute("content")).toBe("Information"); @@ -71,5 +80,32 @@ describe("GoABBadge", () => { expect(badgeElement.getAttribute("mb")).toBe(component.mb); expect(badgeElement.getAttribute("ml")).toBe(component.ml); expect(badgeElement.getAttribute("mr")).toBe(component.mr); - }); + })); + + it("should not set icon attribute by default (icon undefined)", fakeAsync(() => { + const noIconFixture = TestBed.createComponent(TestBadgeNoIconComponent); + const noIconComponent = noIconFixture.componentInstance; + noIconComponent.type = "information"; + noIconComponent.content = "Information"; + noIconFixture.detectChanges(); + tick(); + noIconFixture.detectChanges(); + const badgeElement = noIconFixture.debugElement.query( + By.css("goa-badge"), + ).nativeElement; + expect(badgeElement.getAttribute("icon")).toBe("false"); + })); + + it("should not render icon when icon is false", fakeAsync(() => { + fixture = TestBed.createComponent(TestBadgeComponent); + component = fixture.componentInstance; + component.type = "information"; + component.content = "Information"; + component.icon = false; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + const badgeElement = fixture.debugElement.query(By.css("goa-badge")).nativeElement; + expect(badgeElement.getAttribute("icon")).toBe("false"); + })); }); diff --git a/libs/angular-components/src/lib/components/badge/badge.ts b/libs/angular-components/src/lib/components/badge/badge.ts index b3aaf8e1cb..b52bbb3e4a 100644 --- a/libs/angular-components/src/lib/components/badge/badge.ts +++ b/libs/angular-components/src/lib/components/badge/badge.ts @@ -17,7 +17,7 @@ import { GoabBaseComponent } from "../base.component"; Date: Tue, 28 Oct 2025 16:57:28 -0600 Subject: [PATCH 07/80] fix(#2664): move away from Date value within the calendar/datepicker Since time is never wanted when selecting dates it only adds to complexity and adds issues regarding timezones. These changes replace the Date with a custom type and minimizes the use of the Date type to prevent breaking the current component binding. --- .../lib/components/date-picker/date-picker.ts | 13 +- libs/common/src/lib/common.ts | 9 +- .../specs/calendar.browser.spec.tsx | 700 +++++++++++++++++- .../specs/datepicker.browser.spec.tsx | 70 +- .../src/lib/calendar/calendar.spec.tsx | 12 +- .../src/lib/calendar/calendar.tsx | 24 +- .../src/lib/date-picker/date-picker.tsx | 12 +- .../src/common/calendar-date.spec.ts | 460 ++++++++++++ .../src/common/calendar-date.ts | 195 +++++ .../src/components/calendar/Calendar.svelte | 233 +++--- .../src/components/calendar/calendar.spec.ts | 319 ++++---- .../components/date-picker/DatePicker.svelte | 200 ++--- .../date-picker/date-picker.spec.ts | 155 +--- 13 files changed, 1736 insertions(+), 666 deletions(-) create mode 100644 libs/web-components/src/common/calendar-date.spec.ts create mode 100644 libs/web-components/src/common/calendar-date.ts diff --git a/libs/angular-components/src/lib/components/date-picker/date-picker.ts b/libs/angular-components/src/lib/components/date-picker/date-picker.ts index 0006a9b4b2..2f5dc24f86 100644 --- a/libs/angular-components/src/lib/components/date-picker/date-picker.ts +++ b/libs/angular-components/src/lib/components/date-picker/date-picker.ts @@ -55,7 +55,6 @@ import { GoabControlValueAccessor } from "../base.component"; export class GoabDatePicker extends GoabControlValueAccessor implements OnInit { isReady = false; @Input() name?: string; - // ** NOTE: can we just use the base component for this? @Input() override value?: Date | string | null | undefined; @Input() min?: Date | string; @Input() max?: Date | string; @@ -100,6 +99,12 @@ export class GoabDatePicker extends GoabControlValueAccessor implements OnInit { this.isReady = true; this.cdr.detectChanges(); }, 0); + + if (this.value && typeof this.value !== "string") { + console.warn( + "Using a `Date` type for value is deprecated. Instead use a string of the format `yyyy-mm-dd`", + ); + } } override setDisabledState(isDisabled: boolean) { @@ -120,7 +125,11 @@ export class GoabDatePicker extends GoabControlValueAccessor implements OnInit { if (!value) { this.renderer.setAttribute(datePickerEl, "value", ""); } else { - this.renderer.setAttribute(datePickerEl, "value", value instanceof Date ? value.toISOString() : value); + this.renderer.setAttribute( + datePickerEl, + "value", + value instanceof Date ? value.toISOString() : value, + ); } } } diff --git a/libs/common/src/lib/common.ts b/libs/common/src/lib/common.ts index d82133d96b..6a78b3e2a5 100644 --- a/libs/common/src/lib/common.ts +++ b/libs/common/src/lib/common.ts @@ -28,7 +28,7 @@ export type GoabInputOnFocusDetail = GoabInputOnChangeDetail; export type GoabMenuButtonOnActionDetail = { action: string; -} +}; export type GoabInputAutoCapitalize = | "on" @@ -70,7 +70,11 @@ export type GoabDropdownOnChangeDetail = { export type GoabDatePickerOnChangeDetail = { name?: string; - value: string | Date | undefined; + valueStr: string; + /** + * @deprecated Use `valueStr` instead + */ + value: Date; }; export type GoabDatePickerInputType = "calendar" | "input"; @@ -1158,7 +1162,6 @@ export type GoabPublicFormStatus = "initializing" | "complete"; export type GoabPublicFormPageStep = "step" | "summary" | "multistep"; export type GoabPublicFormPageButtonVisibility = "visible" | "hidden"; - // Public form Task export type GoabPublicFormTaskStatus = "completed" | "not-started" | "cannot-start"; diff --git a/libs/react-components/specs/calendar.browser.spec.tsx b/libs/react-components/specs/calendar.browser.spec.tsx index 5a54fe9124..a85d7b075c 100644 --- a/libs/react-components/specs/calendar.browser.spec.tsx +++ b/libs/react-components/specs/calendar.browser.spec.tsx @@ -2,30 +2,712 @@ import { render } from "vitest-browser-react"; import { GoabCalendar } from "../src"; import { expect, describe, it, vi } from "vitest"; +import { userEvent } from "@vitest/browser/context"; +import { format, addDays, addMonths, addYears } from "date-fns"; describe("Calendar", () => { const noop = () => { // noop }; + it("renders", async () => { + const handleChange = vi.fn(); + + const Component = () => { + return ; + }; + + const result = render(); + + await vi.waitFor(() => { + const calendar = result.getByTestId("cal"); + expect(calendar.element()).toBeTruthy(); + }); + }); + + it("renders with a value", async () => { + const value = "2024-03-15"; + const handleChange = vi.fn(); + + const Component = () => { + return ; + }; + + const result = render(); + const selectedDate = result.getByTestId("2024-03-15"); + + await vi.waitFor(() => { + expect(selectedDate.element()).toBeTruthy(); + expect(selectedDate.element().classList.contains("selected")).toBe(true); + }); + }); + + it("dispatches change event when clicked", async () => { + const handleChange = vi.fn(); + const today = new Date(); + const todayStr = format(today, "yyyy-MM-dd"); + + const Component = () => { + return ; + }; + + const result = render(); + const dateButton = result.getByTestId(todayStr); + + await dateButton.click(); + + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "birthdate", + value: expect.any(String), + }); + }); + }); + + it("respects min date constraint", async () => { + const handleChange = vi.fn(); + const today = new Date(); + const minDate = format(today, "yyyy-MM-dd"); + const pastDate = format(addDays(today, -5), "yyyy-MM-dd"); + + const Component = () => { + return ; + }; + + const result = render(); + const pastDateButton = result.getByTestId(pastDate); + + await vi.waitFor(() => { + expect(pastDateButton.element().classList.contains("disabled")).toBe(true); + }); + + await pastDateButton.click(); + + // Should not trigger change for disabled date + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(handleChange).not.toHaveBeenCalled(); + }); + + it("respects max date constraint", async () => { + const handleChange = vi.fn(); + const testDate = new Date(2024, 2, 15); // March 15, 2024 + const maxDate = format(testDate, "yyyy-MM-dd"); + const futureDate = format(addDays(testDate, 5), "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + + // Wait for component to render + await vi.waitFor(() => { + expect(result.getByTestId("cal-test").element()).toBeTruthy(); + }); + + const futureDateButton = result.getByTestId(futureDate); + + await vi.waitFor(() => { + expect(futureDateButton.element().classList.contains("disabled")).toBe(true); + }); + + await futureDateButton.click(); + + // Should not trigger change for disabled date + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(handleChange).not.toHaveBeenCalled(); + }); + + describe("Month and Year selection", () => { + it("changes the month when month dropdown is changed", async () => { + const handleChange = vi.fn(); + + const Component = () => { + return ( +
+ +
+ ); + }; + + const result = render(); + + const monthsDropdown = result.getByTestId("months"); + const june = result.getByTestId("dropdown-item-6"); + const firstDayOfJune = result.getByTestId("2024-06-01"); + + // select June + await monthsDropdown.click(); + await june.click(); + + // Wait for dropdown to be interactive + await vi.waitFor(() => { + expect(firstDayOfJune).toBeVisible(); + }); + }); + + it("changes the year when year dropdown is changed", async () => { + const handleChange = vi.fn(); + + const Component = () => { + return ( +
+ +
+ ); + }; + + const result = render(); + const yearsDropdown = result.getByTestId("years"); + const nextYear = result.getByTestId("dropdown-item-2025"); + const firstDay = result.getByTestId("2025-03-01"); + + // select June + await yearsDropdown.click(); + await nextYear.click(); + + // Wait for dropdown to be interactive + await vi.waitFor(() => { + expect(firstDay).toBeVisible(); + }); + }); + }); + + describe("Keyboard Navigation", () => { + it("navigates to previous day with ArrowLeft and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initialDate = result.getByTestId(startDateStr); + + await initialDate.click(); + await userEvent.keyboard("{ArrowLeft}"); + await userEvent.keyboard("{Enter}"); + + const prevDate = format(addDays(startDate, -1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(prevDate), + }); + }); + }); + + it("navigates to next day with ArrowRight and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{ArrowRight}"); + await userEvent.keyboard("{Enter}"); + + const nextDate = format(addDays(startDate, 1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(nextDate), + }); + }); + }); + + it("navigates to previous week with ArrowUp and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{ArrowUp}"); + await userEvent.keyboard("{Enter}"); + + const prevWeekDate = format(addDays(startDate, -7), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(prevWeekDate), + }); + }); + }); + + it("navigates to next week with ArrowDown and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{ArrowDown}"); + await userEvent.keyboard("{Enter}"); + + const nextWeekDate = format(addDays(startDate, 7), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(nextWeekDate), + }); + }); + }); + + it("navigates to first day of month with Home and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{Home}"); + await userEvent.keyboard("{Enter}"); + + const firstDay = format(new Date(2024, 2, 1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(firstDay), + }); + }); + }); + + it("navigates to last day of month with End and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{End}"); + await userEvent.keyboard("{Enter}"); + + const lastDay = format(new Date(2024, 2, 31), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(lastDay), + }); + }); + }); + + it("navigates to previous month with PageUp and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{PageUp}"); + await userEvent.keyboard("{Enter}"); + + const prevMonthDate = format(addMonths(startDate, -1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(prevMonthDate), + }); + }); + }); + + it("navigates to next month with PageDown and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{PageDown}"); + await userEvent.keyboard("{Enter}"); + + const nextMonthDate = format(addMonths(startDate, 1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(nextMonthDate), + }); + }); + }); + + it("navigates to previous year with Shift+PageUp and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{Shift>}{PageUp}"); + await userEvent.keyboard("{Enter}"); + + const prevYearDate = format(addYears(startDate, -1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(prevYearDate), + }); + }); + }); + + it("navigates to next year with Shift+PageDown and selects with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{Shift>}{PageDown}"); + await userEvent.keyboard("{Enter}"); + + const nextYearDate = format(addYears(startDate, 1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(nextYearDate), + }); + }); + }); + + it("selects the focused date with Enter", async () => { + const handleChange = vi.fn(); + const startDate = new Date(2024, 2, 15); // March 15, 2024 + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{ArrowRight}"); // Move to next day + await userEvent.keyboard("{Enter}"); // Select it + + const expectedDate = format(addDays(startDate, 1), "yyyy-MM-dd"); + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(expectedDate), + }); + }); + }); + + it("does not navigate beyond min date constraint", async () => { + const handleChange = vi.fn(); + const minDate = new Date(2024, 2, 10); // March 10, 2024 + const startDate = new Date(2024, 2, 11); // March 11, 2024 + const minDateStr = format(minDate, "yyyy-MM-dd"); + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{ArrowLeft}"); // Try to move before min + await userEvent.keyboard("{Enter}"); // Try to select + + // Should still select March 11 (start date), not March 10 + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(startDateStr), + }); + }); + }); + + it("does not navigate beyond max date constraint", async () => { + const handleChange = vi.fn(); + const maxDate = new Date(2024, 2, 20); // March 20, 2024 + const startDate = new Date(2024, 2, 19); // March 19, 2024 + const maxDateStr = format(maxDate, "yyyy-MM-dd"); + const startDateStr = format(startDate, "yyyy-MM-dd"); + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const initDate = result.getByTestId(startDateStr); + + await initDate.click(); + await userEvent.keyboard("{ArrowRight}"); // Try to move after max + await userEvent.keyboard("{Enter}"); // Try to select + + // Should still select March 19 (start date), not March 20 + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: expect.stringContaining(startDateStr), + }); + }); + }); + }); + + describe("Visual states", () => { + it("highlights today's date", async () => { + const handleChange = vi.fn(); + const today = new Date(); + const todayStr = format(today, "yyyy-MM-dd"); + + const Component = () => { + return ; + }; + + const result = render(); + const todayButton = result.getByTestId(todayStr); + + await vi.waitFor(() => { + expect(todayButton.element().classList.contains("today")).toBe(true); + }); + }); + + it("highlights the selected date", async () => { + const handleChange = vi.fn(); + const selectedDate = "2024-03-15"; + + const Component = () => { + return ( + + ); + }; + + const result = render(); + const selectedButton = result.getByTestId(selectedDate); + + await vi.waitFor(() => { + expect(selectedButton.element().classList.contains("selected")).toBe(true); + }); + }); + + it("displays days from previous and next months", async () => { + const handleChange = vi.fn(); + const testDate = "2024-03-15"; // March 2024 + + const Component = () => { + return ( + + ); + }; + + const result = render(); + + // March 1, 2024 is a Friday, so we should see days from previous month + // Last days of February 2024 + const lastDayOfFeb = result.getByTestId("2024-02-29"); // Leap year + + await vi.waitFor(() => { + expect(lastDayOfFeb.element()).toBeTruthy(); + expect(lastDayOfFeb.element().classList.contains("other-month")).toBe(true); + }); + }); + }); + + describe("Edge cases", () => { + it("handles leap year correctly", async () => { + const handleChange = vi.fn(); + const leapDay = "2024-02-29"; + + const Component = () => { + return ; + }; + + const result = render(); + const leapDayButton = result.getByTestId(leapDay); + + await vi.waitFor(() => { + expect(leapDayButton.element()).toBeTruthy(); + expect(leapDayButton.element().classList.contains("selected")).toBe(true); + }); + }); + + it("handles month transitions when selecting dates from other months", async () => { + const handleChange = vi.fn(); + const testDate = "2024-03-01"; // March 1, 2024 (Friday) + + const Component = () => { + return ( + + ); + }; + + const result = render(); + + // Click on a day from February (displayed at the beginning of March calendar) + const febDay = result.getByTestId("2024-02-29"); + await febDay.click(); + + await vi.waitFor(() => { + expect(handleChange).toHaveBeenCalledWith({ + name: "", + value: "2024-02-29", + }); + }); + }); + }); + describe("Bug fixes", () => { describe("3156", () => { it("should render all months", async () => { - // The calendar contained falsey values const Component = () => { - return ( - - ); + return ; }; const result = render(); - const falseyOption = result.getByTestId("dropdown-item-0"); + const falseyOption = result.getByTestId("dropdown-item-1"); await vi.waitFor(() => { expect(falseyOption.element()).toBeTruthy(); - }) - }) - }) - }) + }); + }); + }); + }); }); diff --git a/libs/react-components/specs/datepicker.browser.spec.tsx b/libs/react-components/specs/datepicker.browser.spec.tsx index eaa4c33af2..e64bbf4028 100644 --- a/libs/react-components/specs/datepicker.browser.spec.tsx +++ b/libs/react-components/specs/datepicker.browser.spec.tsx @@ -204,29 +204,6 @@ describe("DatePicker", () => { }); }); - it("applies width to input type datepicker container", async () => { - const Component = () => { - return ; - }; - - const result = render(); - - await vi.waitFor(() => { - // Select the host element and inspect shadow DOM container width - const host = result.container.querySelector("goa-date-picker") as HTMLElement; - - const shadow = host.shadowRoot as ShadowRoot | null; - const formItem = shadow.querySelector("goa-form-item") as HTMLElement | null; - - const computedStyle = window.getComputedStyle(formItem); - const containerWidth = parseFloat(computedStyle.width); - - // The width should be close to 500px - expect(containerWidth).toBeGreaterThan(490); - expect(containerWidth).toBeLessThan(510); - }); - }); - it("supports percentage width units", async () => { const Component = () => { return ( @@ -293,66 +270,59 @@ describe("Date Picker input type", () => { const result = render(); const datePickerMonth = result.getByTestId("input-month"); + const datePickerMonthMarch = result.getByTestId("dropdown-item-3"); const datePickerDay = result.getByTestId("input-day"); const datePickerYear = result.getByTestId("input-year"); - expect(datePickerMonth).toBeTruthy(); - expect(datePickerDay).toBeTruthy(); - expect(datePickerYear).toBeTruthy(); - const rootElChangeHandler = vi.fn(); result.container.addEventListener("_change", (e: Event) => { const ce = e as CustomEvent; - rootElChangeHandler(ce.detail.value); + rootElChangeHandler(ce.detail.valueStr); }); // Select month - if (datePickerMonth) { - await datePickerMonth.click(); - await userEvent.keyboard("{ArrowDown}"); - await userEvent.keyboard("{Enter}"); - } + await datePickerMonth.click(); + await userEvent.keyboard("{ArrowDown}"); + await userEvent.keyboard("{Enter}"); // should be null because date is invalid await vi.waitFor(() => { - expect(rootElChangeHandler).toHaveBeenCalledWith(null); + expect(rootElChangeHandler).toHaveBeenCalledWith(""); }); rootElChangeHandler.mockClear(); // Input day - if (datePickerDay) { - await datePickerDay.click(); - await userEvent.type(datePickerDay, "1"); - } + await datePickerDay.click(); + await userEvent.type(datePickerDay, "1"); + + // Select month + await userEvent.click(datePickerMonth); + await userEvent.click(datePickerMonthMarch); // should be null because date is still invalid await vi.waitFor(() => { - expect(rootElChangeHandler).toHaveBeenCalledWith(null); + expect(rootElChangeHandler).toHaveBeenCalledWith(""); }); rootElChangeHandler.mockClear(); // Input year - if (datePickerYear) { - await datePickerYear.click(); - await userEvent.type(datePickerYear, "1999"); - } + await datePickerYear.click(); + await userEvent.type(datePickerYear, "1999"); // should not be null because date became valid await vi.waitFor(() => { - expect(rootElChangeHandler).toHaveBeenCalledWith("1999-01-01"); + expect(rootElChangeHandler).toHaveBeenCalledWith("1999-03-01"); }); rootElChangeHandler.mockClear(); // Clear day input - if (datePickerDay) { - await datePickerDay.click(); - await userEvent.keyboard("{ArrowRight}"); - await userEvent.keyboard("{Backspace}"); - } + await datePickerDay.click(); + await userEvent.keyboard("{ArrowRight}"); + await userEvent.keyboard("{Backspace}"); // should be null because date became invalid await vi.waitFor(() => { - expect(rootElChangeHandler).toHaveBeenCalledWith(null); + expect(rootElChangeHandler).toHaveBeenCalledWith(""); }); rootElChangeHandler.mockClear(); }); diff --git a/libs/react-components/src/lib/calendar/calendar.spec.tsx b/libs/react-components/src/lib/calendar/calendar.spec.tsx index 1aba85654e..0317e03c0d 100644 --- a/libs/react-components/src/lib/calendar/calendar.spec.tsx +++ b/libs/react-components/src/lib/calendar/calendar.spec.tsx @@ -27,9 +27,9 @@ describe("Calendar", () => { }); it("should set the props correctly", () => { - const value = new Date(); - const min = addMonths(value, -1); - const max = addMonths(value, 1); + const value = "2025-02-03"; + const min = "2024-01-01" + const max = "2025-01-01" const { baseElement } = render( { ); const el = baseElement.querySelector("goa-calendar"); expect(baseElement).toBeTruthy(); - expect(el?.getAttribute("value")).toBe(value.toISOString()); - expect(el?.getAttribute("min")).toBe(min.toISOString()); - expect(el?.getAttribute("max")).toBe(max.toISOString()); + expect(el?.getAttribute("value")).toBe(value); + expect(el?.getAttribute("min")).toBe(min); + expect(el?.getAttribute("max")).toBe(max); expect(el?.getAttribute("testid")).toBe("foo"); }); }); diff --git a/libs/react-components/src/lib/calendar/calendar.tsx b/libs/react-components/src/lib/calendar/calendar.tsx index 84db318873..913c3bb323 100644 --- a/libs/react-components/src/lib/calendar/calendar.tsx +++ b/libs/react-components/src/lib/calendar/calendar.tsx @@ -20,9 +20,9 @@ declare module "react" { } export interface GoabCalendarProps extends Margins { name?: string; - value?: Date; - min?: Date; - max?: Date; + value?: string; + min?: string; + max?: string; testId?: string; onChange: (details: GoabCalendarOnChangeDetail) => void; } @@ -40,26 +40,32 @@ export function GoabCalendar({ onChange, }: GoabCalendarProps): JSX.Element { const ref = useRef(null); + useEffect(() => { if (!ref.current) { return; } const current = ref.current; - current.addEventListener("_change", (e: Event) => { + const listener = (e: Event) => { onChange({ name: name || "", value: (e as CustomEvent).detail.value, }); - }); - }); + } + current.addEventListener("_change", listener); + + return () => { + current.removeEventListener("_change", listener); + } + }, []); return ( (null); + useEffect(() => { + if (value && typeof value !== "string") { + console.warn("Using a `Date` type for value is deprecated. Instead use a string of the format `yyyy-mm-dd`") + } + }, []); + useEffect(() => { if (!ref.current) { return; @@ -100,12 +106,12 @@ export function GoabDatePicker({ { + describe("parse", () => { + it("parses a string date in YYYY-MM-DD format", () => { + const result = CalendarDate.parse("2024-03-15"); + expect(result).toEqual([2024, 3, 15]); + }); + + it("parses a string date with ISO timestamp", () => { + const result = CalendarDate.parse("2024-03-15T10:30:00Z"); + expect(result).toEqual([2024, 3, 15]); + }); + + it("parses a Date object", () => { + const date = new Date(2024, 2, 15); // Month is 0-indexed + const result = CalendarDate.parse(date); + expect(result).toEqual([2024, 3, 15]); // Month is 1-indexed in result + }); + + it("parses an object with year, month, day", () => { + const result = CalendarDate.parse({ year: 2024, month: 3, day: 15 }); + expect(result).toEqual([2024, 3, 15]); + }); + }); + + describe("constructor", () => { + it("creates a CalendarDate from a string", () => { + const calDate = new CalendarDate("2024-03-15"); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(15); + }); + + it("creates a CalendarDate from a Date object", () => { + const date = new Date(2024, 2, 15); + const calDate = new CalendarDate(date); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(15); + }); + + it("creates a CalendarDate from an object", () => { + const calDate = new CalendarDate({ year: 2024, month: 3, day: 15 }); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(15); + }); + + it("creates a CalendarDate with current date when no value provided", () => { + const calDate = new CalendarDate(); + const now = new Date(); + expect(calDate.year).toBe(now.getFullYear()); + expect(calDate.month).toBe(now.getMonth() + 1); + expect(calDate.day).toBe(now.getDate()); + }); + }); + + describe("getters", () => { + const calDate = new CalendarDate("2024-03-15"); + + it("returns the year", () => { + expect(calDate.year).toBe(2024); + }); + + it("returns the month", () => { + expect(calDate.month).toBe(3); + }); + + it("returns the day", () => { + expect(calDate.day).toBe(15); + }); + + it("returns the date as a Date object", () => { + const date = calDate.date; + expect(date).toBeInstanceOf(Date); + expect(date.getFullYear()).toBe(2024); + expect(date.getMonth()).toBe(2); // 0-indexed + expect(date.getDate()).toBe(15); + }); + + it("returns the day of week", () => { + // March 15, 2024 is a Friday (5) + expect(calDate.dayOfWeek).toBe(5); + }); + + it("returns the number of days in the month", () => { + // March has 31 days + expect(calDate.daysInMonth).toBe(31); + }); + + it("returns the first day of the month", () => { + const firstDay = calDate.firstDayOfMonth; + expect(firstDay.year).toBe(2024); + expect(firstDay.month).toBe(3); + expect(firstDay.day).toBe(1); + }); + + it("returns the last day of the month", () => { + const lastDay = calDate.lastDayOfMonth; + expect(lastDay.year).toBe(2024); + expect(lastDay.month).toBe(3); + expect(lastDay.day).toBe(31); + }); + + it("returns the previous day", () => { + const prevDay = calDate.previousDay; + expect(prevDay.year).toBe(2024); + expect(prevDay.month).toBe(3); + expect(prevDay.day).toBe(14); + }); + + it("returns the next day", () => { + const nextDay = calDate.nextDay; + expect(nextDay.year).toBe(2024); + expect(nextDay.month).toBe(3); + expect(nextDay.day).toBe(16); + }); + + it("returns the previous month", () => { + const prevMonth = calDate.previousMonth; + expect(prevMonth.year).toBe(2024); + expect(prevMonth.month).toBe(2); + expect(prevMonth.day).toBe(15); + }); + + it("returns the next month", () => { + const nextMonth = calDate.nextMonth; + expect(nextMonth.year).toBe(2024); + expect(nextMonth.month).toBe(4); + expect(nextMonth.day).toBe(15); + }); + }); + + describe("setters", () => { + it("sets the year", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.setYear(2025); + expect(calDate.year).toBe(2025); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(15); + }); + + it("sets the month", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.setMonth(6); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(6); + expect(calDate.day).toBe(15); + }); + + it("sets the day and returns the instance", () => { + const calDate = new CalendarDate("2024-03-15"); + const result = calDate.setDay(20); + expect(result).toBe(calDate); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(20); + }); + }); + + describe("addYears", () => { + it("adds positive years", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addYears(2); + expect(calDate.year).toBe(2026); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(15); + }); + + it("adds negative years", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addYears(-2); + expect(calDate.year).toBe(2022); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(15); + }); + + it("returns the instance for chaining", () => { + const calDate = new CalendarDate("2024-03-15"); + const result = calDate.addYears(1); + expect(result).toBe(calDate); + }); + }); + + describe("addMonths", () => { + it("adds positive months within the same year", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addMonths(2); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(5); + expect(calDate.day).toBe(15); + }); + + it("adds positive months across years", () => { + const calDate = new CalendarDate("2024-11-15"); + calDate.addMonths(3); + expect(calDate.year).toBe(2025); + expect(calDate.month).toBe(2); + expect(calDate.day).toBe(15); + }); + + it("adds negative months", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addMonths(-2); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(1); + expect(calDate.day).toBe(15); + }); + + it("handles month overflow correctly", () => { + const calDate = new CalendarDate("2024-01-31"); + calDate.addMonths(1); + // January 31 + 1 month = February 29, 2024 (leap year) + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(2); + expect(calDate.day).toBe(29); + }); + + it("returns the instance for chaining", () => { + const calDate = new CalendarDate("2024-03-15"); + const result = calDate.addMonths(1); + expect(result).toBe(calDate); + }); + }); + + describe("addDays", () => { + it("adds positive days within the same month", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addDays(5); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(20); + }); + + it("adds positive days across months", () => { + const calDate = new CalendarDate("2024-03-29"); + calDate.addDays(5); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(4); + expect(calDate.day).toBe(3); + }); + + it("adds positive days across years", () => { + const calDate = new CalendarDate("2024-12-30"); + calDate.addDays(5); + expect(calDate.year).toBe(2025); + expect(calDate.month).toBe(1); + expect(calDate.day).toBe(4); + }); + + it("adds negative days", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addDays(-5); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(3); + expect(calDate.day).toBe(10); + }); + + it("adds negative days across months", () => { + const calDate = new CalendarDate("2024-03-02"); + calDate.addDays(-5); + expect(calDate.year).toBe(2024); + expect(calDate.month).toBe(2); + expect(calDate.day).toBe(26); + }); + + it("returns the instance for chaining", () => { + const calDate = new CalendarDate("2024-03-15"); + const result = calDate.addDays(1); + expect(result).toBe(calDate); + }); + }); + + describe("comparison methods", () => { + describe("isSameDay", () => { + it("returns true for the same day", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-15"); + expect(date1.isSameDay(date2)).toBe(true); + }); + + it("returns false for different days", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-16"); + expect(date1.isSameDay(date2)).toBe(false); + }); + + it("returns false for same day in different months", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-04-15"); + expect(date1.isSameDay(date2)).toBe(false); + }); + }); + + describe("isSameMonth", () => { + it("returns true for the same month", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-20"); + expect(date1.isSameMonth(date2)).toBe(true); + }); + + it("returns false for different months", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-04-15"); + expect(date1.isSameMonth(date2)).toBe(false); + }); + + it("returns false for same month in different years", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2025-03-15"); + expect(date1.isSameMonth(date2)).toBe(false); + }); + }); + + describe("isBefore", () => { + it("returns true when date is before comparison date", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-20"); + expect(date1.isBefore(date2)).toBe(true); + }); + + it("returns false when date is after comparison date", () => { + const date1 = new CalendarDate("2024-03-20"); + const date2 = new CalendarDate("2024-03-15"); + expect(date1.isBefore(date2)).toBe(false); + }); + + it("returns false when dates are the same", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-15"); + expect(date1.isBefore(date2)).toBe(false); + }); + }); + + describe("isAfter", () => { + it("returns true when date is after comparison date", () => { + const date1 = new CalendarDate("2024-03-20"); + const date2 = new CalendarDate("2024-03-15"); + expect(date1.isAfter(date2)).toBe(true); + }); + + it("returns false when date is before comparison date", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-20"); + expect(date1.isAfter(date2)).toBe(false); + }); + + it("returns false when dates are the same", () => { + const date1 = new CalendarDate("2024-03-15"); + const date2 = new CalendarDate("2024-03-15"); + expect(date1.isAfter(date2)).toBe(false); + }); + }); + }); + + describe("clone", () => { + it("creates a new instance with the same date", () => { + const original = new CalendarDate("2024-03-15"); + const cloned = original.clone(); + + expect(cloned).not.toBe(original); + expect(cloned.year).toBe(original.year); + expect(cloned.month).toBe(original.month); + expect(cloned.day).toBe(original.day); + }); + + it("creates an independent instance", () => { + const original = new CalendarDate("2024-03-15"); + const cloned = original.clone(); + + cloned.addDays(5); + + expect(original.day).toBe(15); + expect(cloned.day).toBe(20); + }); + }); + + describe("isValid", () => { + it("returns true for a valid date", () => { + const calDate = new CalendarDate("2024-03-15"); + expect(calDate.isValid()).toBe(true); + }); + + it("returns true for edge case dates", () => { + const leapDay = new CalendarDate("2024-02-29"); + expect(leapDay.isValid()).toBe(true); + }); + }); + + describe("format", () => { + it("formats the date using date-fns format string", () => { + const calDate = new CalendarDate("2024-03-15"); + expect(calDate.format("yyyy-MM-dd")).toBe("2024-03-15"); + }); + + it("formats with different patterns", () => { + const calDate = new CalendarDate("2024-03-15"); + expect(calDate.format("MMM d, yyyy")).toBe("Mar 15, 2024"); + }); + + it("formats with full month name", () => { + const calDate = new CalendarDate("2024-03-15"); + expect(calDate.format("MMMM d, yyyy")).toBe("March 15, 2024"); + }); + }); + + describe("toString", () => { + it("returns the date as hyphen-separated values", () => { + const calDate = new CalendarDate({ year: 2024, month: 3, day: 15 }); + const result = calDate.toString(); + expect(result).toBe("2024-03-15"); + }); + + it("can be used to create a new CalendarDate", () => { + const original = new CalendarDate({ year: 2024, month: 3, day: 15 }); + const str = original.toString(); + const cloned = new CalendarDate(str); + expect(cloned.year).toBe(original.year); + expect(cloned.month).toBe(original.month); + expect(cloned.day).toBe(original.day); + }); + }); + + describe("edge cases", () => { + it("handles leap year correctly", () => { + const leapDay = new CalendarDate("2024-02-29"); + expect(leapDay.daysInMonth).toBe(29); + expect(leapDay.isValid()).toBe(true); + }); + + it("handles non-leap year correctly", () => { + const feb2023 = new CalendarDate("2023-02-15"); + expect(feb2023.daysInMonth).toBe(28); + }); + + it("handles month boundaries when adding days", () => { + const endOfMonth = new CalendarDate("2024-01-31"); + endOfMonth.addDays(1); + expect(endOfMonth.month).toBe(2); + expect(endOfMonth.day).toBe(1); + }); + + it("handles year boundaries when adding months", () => { + const endOfYear = new CalendarDate("2024-12-15"); + endOfYear.addMonths(1); + expect(endOfYear.year).toBe(2025); + expect(endOfYear.month).toBe(1); + }); + + it("handles chaining multiple operations", () => { + const calDate = new CalendarDate("2024-03-15"); + calDate.addYears(1).addMonths(2).addDays(5); + expect(calDate.year).toBe(2025); + expect(calDate.month).toBe(5); + expect(calDate.day).toBe(20); + }); + }); +}); diff --git a/libs/web-components/src/common/calendar-date.ts b/libs/web-components/src/common/calendar-date.ts new file mode 100644 index 0000000000..55fa06b03a --- /dev/null +++ b/libs/web-components/src/common/calendar-date.ts @@ -0,0 +1,195 @@ +import { + addMonths as _addMonths, + addDays as _addDays, + format as _format, + getDaysInMonth as _getDaysInMonth, + isSameDay as _isSameDay, + lastDayOfMonth as _lastDayOfMonth, + setDate as _setDate, + isSameMonth as _isSameMonth, + isBefore as _isBefore, + addYears as _addYears, + isAfter as _isAfter, +} from "date-fns"; + +type CalendarDateInput = + | string + | Date + | 0 + | { year: number; month: number; day: number }; + +export class CalendarDate { + private _dateNums: number[]; + + static parse(value: CalendarDateInput): number[] { + if (typeof value === "string") { + value = value.split("T")[0]; + return value.split("-").map((v) => +v); + } else if (value instanceof Date) { + return [value.getFullYear(), value.getMonth() + 1, value.getDate()]; + } else if (value === 0) { + return [0, 0, 0]; + } else { + return [value.year, value.month, value.day]; + } + } + + static init(): CalendarDate { + return new CalendarDate(0); + } + + constructor(value?: CalendarDateInput) { + if (value || value === 0) { + this._dateNums = CalendarDate.parse(value); + } else { + this._dateNums = CalendarDate.parse(new Date()); + } + } + + // Used internally to get the date value for the date_fns + get date(): Date { + return new Date( + this._dateNums[0], + this._dateNums[1] - 1, + this._dateNums[2], + ); + } + + get year(): number { + return this._dateNums[0]; + } + + get month(): number { + return this._dateNums[1]; + } + + get day(): number { + return this._dateNums[2]; + } + + get dayOfWeek(): number { + return this.date.getDay(); + } + + get daysInMonth(): number { + return _getDaysInMonth(this.date); + } + + get firstDayOfMonth(): CalendarDate { + return new CalendarDate({ year: this.year, month: this.month, day: 1 }); + } + + get lastDayOfMonth(): CalendarDate { + return new CalendarDate(_lastDayOfMonth(this.date)); + } + + get previousDay(): CalendarDate { + return this.clone().addDays(-1); + } + + get nextDay(): CalendarDate { + return this.clone().addDays(1); + } + + get previousWeek(): CalendarDate { + return this.clone().addDays(-7); + } + + get nextWeek(): CalendarDate { + return this.clone().addDays(7); + } + + get previousMonth(): CalendarDate { + return this.clone().addMonths(-1); + } + + get nextMonth(): CalendarDate { + return this.clone().addMonths(1); + } + + clone(): CalendarDate { + return new CalendarDate(this.toString()); + } + + setYear(val: number) { + this._dateNums[0] = val; + } + + setMonth(val: number) { + this._dateNums[1] = val; + } + + setDay(val: number): CalendarDate { + this._dateNums[2] = val; + return this; + } + + addYears(count: number): CalendarDate { + this._dateNums[0] += count; + return this; + } + + addMonths(count: number): CalendarDate { + this._dateNums = CalendarDate.parse(_addMonths(this.date, count)); + return this; + } + + addDays(count: number): CalendarDate { + this._dateNums = CalendarDate.parse(_addDays(this.date, count)); + return this; + } + + isSameDay(cmp: CalendarDate): boolean { + return _isSameDay(this.date, cmp.date); + } + + isSameMonth(value: CalendarDate): boolean { + return _isSameMonth(this.date, value.date); + } + + isBefore(cmp: CalendarDate): boolean { + return _isBefore(this.date, cmp.date); + } + + isAfter(cmp: CalendarDate): boolean { + return _isAfter(this.date, cmp.date); + } + + isZero(): boolean { + return ( + this._dateNums[0] === 0 && + this._dateNums[1] === 0 && + this._dateNums[2] === 0 + ); + } + + isValid(): boolean { + // ensure it's a valid date + // E.g. "2025-02-31" would be invalid because the date does not exist + const comparisonDate = new Date(this.toString()); + if ( + isNaN(comparisonDate.getTime()) || + this.toString() !== comparisonDate.toISOString().split("T")[0] + ) { + return false; + } + + return true; + } + + format(tmpl: string): string { + if (this.isZero()) { + return ""; + } + return _format(this.date, tmpl); + } + + toString(): string { + if (this.isZero()) { + return ""; + } + return this._dateNums + .map((num) => (`${num}`.length < 2 ? `0${num}` : `${num}`)) + .join("-"); + } +} diff --git a/libs/web-components/src/components/calendar/Calendar.svelte b/libs/web-components/src/components/calendar/Calendar.svelte index eb71fe61e3..ab8a73250c 100644 --- a/libs/web-components/src/components/calendar/Calendar.svelte +++ b/libs/web-components/src/components/calendar/Calendar.svelte @@ -2,23 +2,9 @@ @@ -327,11 +305,11 @@ data-testid="months" width="160px" maxheight="240px" - value={_calendarDate?.getMonth()} + value={_calendarDate?.month} on:_change={setMonth} > {#each _months as month, i} - + {/each}
@@ -344,7 +322,7 @@ data-testid="years" width="104px" maxheight="240px" - value={_calendarDate?.getFullYear()} + value={_calendarDate?.year} on:_change={setYear} > {#each _years as year} @@ -364,49 +342,46 @@
Sat
{#each _previousMonthDays as d} {/each} {#each _monthDays as d} {/each} {#each _nextMonthDays as d} {/each} diff --git a/libs/web-components/src/components/calendar/calendar.spec.ts b/libs/web-components/src/components/calendar/calendar.spec.ts index 9eae33a8a0..5d4ec3b081 100644 --- a/libs/web-components/src/components/calendar/calendar.spec.ts +++ b/libs/web-components/src/components/calendar/calendar.spec.ts @@ -19,7 +19,7 @@ function toDayStart(d: Date): Date { it("it renders", async () => { const { container, queryByTestId } = render(Calendar); - await tick() + await tick(); const monthsEl = queryByTestId("months"); const yearsEl = queryByTestId("years"); @@ -34,7 +34,7 @@ it("it renders", async () => { const d = new Date(lastDate); d.setDate(i); const dayEl = container - ?.querySelector(`[data-date="${d.getTime()}"]`) + ?.querySelector(`[data-date="${getDateStamp(d)}"]`) ?.querySelector("[data-testid=date]"); expect(dayEl).toBeTruthy(); } @@ -42,15 +42,16 @@ it("it renders", async () => { // today's date const today = toDayStart(new Date()); const todayEl = container - ?.querySelector(`.today[data-date="${today.getTime()}"]`) + ?.querySelector(`.today[data-date="${getDateStamp(today)}"]`) ?.querySelector("[data-testid=date]"); expect(todayEl).toBeTruthy(); // months - const monthEls = queryByTestId("months")?.querySelectorAll("goa-dropdown-item"); + const monthEls = + queryByTestId("months")?.querySelectorAll("goa-dropdown-item"); expect(monthEls?.length).toBe(12); - for (let i = 0; i < 12; i++) { + for (let i = 1; i <= 12; i++) { const month = queryByTestId("months")?.querySelector( `goa-dropdown-item[value="${i}"]`, ); @@ -60,7 +61,7 @@ it("it renders", async () => { it("should have no date selected if one not provided", async () => { const { container } = render(Calendar); - await tick() + await tick(); const selectedDate = container.querySelector(".selected"); expect(selectedDate).toBeFalsy(); @@ -69,12 +70,12 @@ it("should have no date selected if one not provided", async () => { it("sets the preset date value", async () => { const date = new Date().toISOString(); const { container } = render(Calendar, { value: date }); - await tick() + await tick(); const timestamp = toDayStart(new Date(date)); const dayEl = container - .querySelector(`.selected[data-date="${timestamp.getTime()}"]`) - .querySelector("[data-testid=date]"); + .querySelector(`.selected[data-date="${getDateStamp(timestamp)}"]`) + ?.querySelector("[data-testid=date]"); expect(dayEl).toBeTruthy(); }); @@ -84,13 +85,13 @@ it("provides the defined year range", async () => { const min = new Date(now.getFullYear() - diff, now.getMonth(), now.getDate()); const max = new Date(now.getFullYear() + diff, now.getMonth(), now.getDate()); const { queryByTestId } = render(Calendar, { min, max }); - await tick() + await tick(); - const years = queryByTestId("years").querySelectorAll("goa-dropdown-item"); + const years = queryByTestId("years")?.querySelectorAll("goa-dropdown-item"); - expect(years.length).toBe(11); // has to be one more than the count to include the first and last + expect(years?.length).toBe(11); // has to be one more than the count to include the first and last for (let i = 0; i < diff * 2 + 1; i++) { - const year = queryByTestId("years").querySelector( + const year = queryByTestId("years")?.querySelector( `goa-dropdown-item[value="${min.getFullYear() + i}"]`, ); expect(year).toBeTruthy(); @@ -111,13 +112,13 @@ it("show the default year range", async () => { now.getDate(), ); const { queryByTestId } = render(Calendar, { min, max }); - await tick() + await tick(); - const years = queryByTestId("years").querySelectorAll("goa-dropdown-item"); + const years = queryByTestId("years")?.querySelectorAll("goa-dropdown-item"); - expect(years.length).toBe(21); // has to be one more than the count to include the first and last + expect(years?.length).toBe(21); // has to be one more than the count to include the first and last for (let i = 0; i < defaultDiff * 2 + 1; i++) { - const year = queryByTestId("years").querySelector( + const year = queryByTestId("years")?.querySelector( `goa-dropdown-item[value="${min.getFullYear() + i}"]`, ); expect(year).toBeTruthy(); @@ -127,30 +128,34 @@ it("show the default year range", async () => { it("emits an event when a date is selected", async () => { const name = "birthdate"; const { container, queryByTestId } = render(Calendar, { name }); - await tick() + await tick(); const today = toDayStart(new Date()); const todayEl = container.querySelector( - `button.today[data-date="${today.getTime()}"]`, + `button.today[data-date="${getDateStamp(today)}"]`, ); expect(todayEl).toBeTruthy(); const onChange = vi.fn(); const calendarEl = queryByTestId("calendar"); - calendarEl.addEventListener("_change", (e) => { + calendarEl?.addEventListener("_change", (e) => { onChange((e as CustomEvent).detail); }); - await fireEvent.click(todayEl); + await fireEvent.click(todayEl!); await waitFor(() => { expect(onChange).toBeCalled(); - expect(onChange).toBeCalledWith({ type: "date", value: today, name }); + expect(onChange).toBeCalledWith({ + type: "string", + value: getDateStamp(today), + name, + }); }); }); it("updates the calendar when a new month is selected", async () => { const { container, queryByTestId } = render(Calendar); - await tick() + await tick(); const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const monthsEl = queryByTestId("months"); @@ -160,53 +165,61 @@ it("updates the calendar when a new month is selected", async () => { const date = toDayStart(new Date()); date.setDate(1); const dayOfWeek = date.getDay(); - const buttonEl = container.querySelector(`[data-date="${date.getTime()}"]`); - const dayEl = buttonEl.querySelector("[data-testid=date]"); - expect(dayEl.innerHTML).toBe("1"); + const buttonEl = container.querySelector( + `[data-date="${getDateStamp(date)}"]`, + ); + const dayEl = buttonEl?.querySelector("[data-testid=date]"); + expect(dayEl?.innerHTML).toBe("1"); expect((buttonEl as HTMLElement).dataset.day).toBe(dayNames[dayOfWeek]); } // change month - const otherMonthZeroIndex = (new Date().getMonth() + 1) % 11; - monthsEl.dispatchEvent( + const otherMonth = ((new Date().getMonth() + 1) % 12) + 1; // +1 since getMonth is zero based we need some +1s + monthsEl?.dispatchEvent( new CustomEvent("_change", { - detail: { value: otherMonthZeroIndex }, + detail: { value: otherMonth }, }), ); await waitFor(() => { const date = toDayStart(new Date()); - date.setMonth(otherMonthZeroIndex); + date.setMonth(otherMonth - 1); // revert to 0-index value date.setDate(1); const dayOfWeek = date.getDay(); - const buttonEl = container.querySelector(`[data-date="${date.getTime()}"]`); - const dayEl = buttonEl.querySelector("[data-testid=date]"); - expect(dayEl.innerHTML).toBe("1"); + const buttonEl = queryByTestId(getDateStamp(date)); + + expect(buttonEl).toBeTruthy(); + const dayEl = buttonEl?.querySelector("[data-testid=date]"); + expect(dayEl?.innerHTML).toBe("1"); expect((buttonEl as HTMLElement).dataset.day).toBe(dayNames[dayOfWeek]); }); }); it("updates the calendar when a new year is selected", async () => { const { container, queryByTestId } = render(Calendar); - await tick() + await tick(); const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const yearsEl = queryByTestId("years"); // validate the day of the first day for the current month - { - const date = toDayStart(new Date()); - date.setDate(1); - const dayOfWeek = date.getDay(); - const buttonEl = container.querySelector(`[data-date="${date.getTime()}"]`); - const dayEl = buttonEl.querySelector("[data-testid=date]"); - expect(dayEl.innerHTML).toBe("1"); + const date = toDayStart(new Date()); + date.setDate(1); + const dayOfWeek = date.getDay(); + const buttonEl = queryByTestId(getDateStamp(date)); + + expect(buttonEl).toBeTruthy(); + const dayEl = buttonEl?.querySelector("[data-testid=date]"); + + await waitFor(() => { + expect(dayEl).toBeTruthy(); + expect(dayEl?.innerHTML).toBe("1"); expect((buttonEl as HTMLElement).dataset.day).toBe(dayNames[dayOfWeek]); - } + }); // change year const otherYearZeroIndex = new Date().getFullYear() + 1; - yearsEl.dispatchEvent( + yearsEl?.dispatchEvent( new CustomEvent("_change", { detail: { value: otherYearZeroIndex }, }), @@ -217,20 +230,25 @@ it("updates the calendar when a new year is selected", async () => { date.setFullYear(otherYearZeroIndex); date.setDate(1); const dayOfWeek = date.getDay(); - const buttonEl = container.querySelector(`[data-date="${date.getTime()}"]`); - const dayEl = buttonEl.querySelector("[data-testid=date]"); + const buttonEl = queryByTestId(getDateStamp(date)); + const dayEl = buttonEl?.querySelector("[data-testid=date]"); expect(dayEl).toBeTruthy(); - expect(dayEl.innerHTML).toBe("1"); + expect(dayEl?.innerHTML).toBe("1"); expect((buttonEl as HTMLElement).dataset.day).toBe(dayNames[dayOfWeek]); }); }); it("handle the arrow key presses", async () => { const { container, queryByTestId } = render(Calendar, { value: new Date() }); - await tick() + await tick(); let timestamp = toDayStart(new Date()); const calendarEl = queryByTestId("calendar"); + expect(calendarEl).toBeTruthy(); + + if (!calendarEl) { + return; + } const arrowLeftEvent = createEvent.keyDown(calendarEl, { key: "ArrowLeft" }); const arrowRightEvent = createEvent.keyDown(calendarEl, { @@ -244,9 +262,9 @@ it("handle the arrow key presses", async () => { await fireEvent(calendarEl, arrowLeftEvent); await waitFor(() => { const current = container - .querySelector(`[tabindex="0"]`) - .querySelector("[data-testid=date]"); - expect(current.innerHTML).toBe(`${timestamp.getDate()}`); + ?.querySelector(`[tabindex="0"]`) + ?.querySelector("[data-testid=date]"); + expect(current?.innerHTML).toBe(`${timestamp.getDate()}`); }); // Right arrow @@ -254,9 +272,9 @@ it("handle the arrow key presses", async () => { await fireEvent(calendarEl, arrowRightEvent); await waitFor(() => { const current = container - .querySelector(`[tabindex="0"]`) - .querySelector("[data-testid=date]"); - expect(current.innerHTML).toBe(`${timestamp.getDate()}`); + ?.querySelector(`[tabindex="0"]`) + ?.querySelector("[data-testid=date]"); + expect(current?.innerHTML).toBe(`${timestamp.getDate()}`); }); // Up arrow @@ -264,9 +282,9 @@ it("handle the arrow key presses", async () => { await fireEvent(calendarEl, arrowUpEvent); await waitFor(() => { const current = container - .querySelector(`[tabindex="0"]`) - .querySelector("[data-testid=date]"); - expect(current.innerHTML).toBe(`${timestamp.getDate()}`); + ?.querySelector(`[tabindex="0"]`) + ?.querySelector("[data-testid=date]"); + expect(current?.innerHTML).toBe(`${timestamp.getDate()}`); }); // // Down arrow @@ -274,9 +292,9 @@ it("handle the arrow key presses", async () => { await fireEvent(calendarEl, arrowDownEvent); await waitFor(() => { const current = container - .querySelector(`[tabindex="0"]`) - .querySelector("[data-testid=date]"); - expect(current.innerHTML).toBe(`${timestamp.getDate()}`); + ?.querySelector(`[tabindex="0"]`) + ?.querySelector("[data-testid=date]"); + expect(current?.innerHTML).toBe(`${timestamp.getDate()}`); }); }); @@ -285,29 +303,28 @@ it("prevents date click selection outside of allowed range", async () => { const max = new Date(); const today = startOfDay(new Date()); const { container, queryByTestId } = render(Calendar, { min, max }); - await tick() + await tick(); const yesterday = addDays(today, -1); const tomorrow = addDays(today, +1); const onChange = vi.fn(); const calendarEl = queryByTestId("calendar"); - calendarEl.addEventListener("_change", (e) => { + calendarEl?.addEventListener("_change", (e) => { onChange((e as CustomEvent).detail); }); const yesterdayEl = container.querySelector( - `[data-date="${yesterday.getTime()}"]`, + `[data-date="${getDateStamp(yesterday)}"]`, ); - if (yesterdayEl) { - await fireEvent.click(yesterdayEl); - } + expect(yesterdayEl).toBeTruthy(); + await fireEvent.click(yesterdayEl!); + const tomorrowEl = container.querySelector( - `[data-date="${tomorrow.getTime()}"]`, + `[data-date="${getDateStamp(tomorrow)}"]`, ); - if (tomorrowEl) { - await fireEvent.click(tomorrowEl); - } + expect(tomorrowEl).toBeTruthy(); + await fireEvent.click(tomorrowEl!); expect(onChange).not.toBeCalled(); }); @@ -316,14 +333,18 @@ it("prevents date keyboard selection outside of allowed range", async () => { const min = new Date(); // today is the only date selectable const max = new Date(); const { queryByTestId } = render(Calendar, { min, max }); - await tick() + await tick(); const onChange = vi.fn(); const calendarEl = queryByTestId("calendar"); - calendarEl.addEventListener("_change", (e) => { + calendarEl?.addEventListener("_change", (e) => { onChange((e as CustomEvent).detail); }); + if (!calendarEl) { + return; + } + const arrowLeftEvent = createEvent.keyDown(calendarEl, { key: "ArrowLeft" }); const arrowRightEvent = createEvent.keyDown(calendarEl, { key: "ArrowRight", @@ -341,88 +362,16 @@ it("prevents date keyboard selection outside of allowed range", async () => { }); }); -it("reacts to dynamic min date changes", async () => { - // Create a min date that's definitely before today - // Use the day before yesterday to ensure today is enabled - const initialMin = addDays(new Date(), -2); - initialMin.setHours(0, 0, 0, 0); // Set to start of day - - const { component, container } = render(Calendar, { - min: initialMin.toISOString(), - }); - await tick(); - - // Verify today is initially enabled - const today = startOfDay(new Date()); - let todayEl = container.querySelector(`[data-date="${today.getTime()}"]`); - expect(todayEl).not.toHaveClass("disabled"); - - // Update min to tomorrow - const newMin = addDays(today, 1); - newMin.setHours(0, 0, 0, 0); // Set to start of day - await component.$set({ min: newMin.toISOString() }); - await tick(); - - // Today should now be disabled - todayEl = container.querySelector(`[data-date="${today.getTime()}"]`); - expect(todayEl).toHaveClass("disabled"); - - // Tomorrow should be enabled - const tomorrow = addDays(today, 1); - const tomorrowEl = container.querySelector( - `[data-date="${tomorrow.getTime()}"]`, - ); - expect(tomorrowEl).not.toHaveClass("disabled"); -}); - -it("reacts to dynamic max date changes", async () => { - // Create a max date that's definitely after today - // Use the day after tomorrow to ensure today is enabled - const initialMax = addDays(new Date(), 2); - initialMax.setHours(0, 0, 0, 0); // Set to start of day - - const { component, container } = render(Calendar, { - max: initialMax.toISOString(), - }); - await tick(); - - // Verify today is initially enabled - const today = startOfDay(new Date()); - let todayEl = container.querySelector(`[data-date="${today.getTime()}"]`); - expect(todayEl).not.toHaveClass("disabled"); - - // Verify tomorrow is initially enabled - const tomorrow = addDays(today, 1); - let tomorrowEl = container.querySelector( - `[data-date="${tomorrow.getTime()}"]`, - ); - expect(tomorrowEl).not.toHaveClass("disabled"); - - // Update max to yesterday - const newMax = addDays(today, -1); - newMax.setHours(0, 0, 0, 0); // Set to start of day - await component.$set({ max: newMax.toISOString() }); - await tick(); - - // Today should now be disabled - todayEl = container.querySelector(`[data-date="${today.getTime()}"]`); - if(todayEl) { - expect(todayEl).toHaveClass("disabled"); - } - - // Tomorrow should be disabled - tomorrowEl = container.querySelector(`[data-date="${tomorrow.getTime()}"]`); - if(tomorrowEl) { - expect(tomorrowEl).toHaveClass("disabled"); - } +function pad(num: number): string { + return num >= 10 ? `${num}` : `0${num}`; +} - // Yesterday should be enabled - const yesterday = addDays(today, -1); - const yesterdayEl = container.querySelector( - `[data-date="${yesterday.getTime()}"]`, - ); - expect(yesterdayEl).not.toHaveClass("disabled"); -}); +function getDateStamp(date: Date): string { + const y = date.getFullYear(); + const m = pad(date.getMonth() + 1); + const d = pad(date.getDate()); + return `${y}-${m}-${d}`; +} it("updates year dropdown when min date changes", async () => { // Set initial min/max to create a specific range of years @@ -431,40 +380,48 @@ it("updates year dropdown when min date changes", async () => { const { component, queryByTestId } = render(Calendar, { min: initialMin.toISOString(), - max: initialMax.toISOString() + max: initialMax.toISOString(), }); await tick(); // Check initial years in dropdown let yearDropdown = queryByTestId("years"); - let yearOptions = yearDropdown.querySelectorAll("goa-dropdown-item"); + let yearOptions = yearDropdown?.querySelectorAll("goa-dropdown-item"); // Should be 6 years: 2020, 2021, 2022, 2023, 2024, 2025 - expect(yearOptions.length).toBe(6); + expect(yearOptions?.length).toBe(6); // Verify first and last year options - expect(yearOptions[0].getAttribute("value")).toBe("2020"); - expect(yearOptions[yearOptions.length - 1].getAttribute("value")).toBe("2025"); + expect(yearOptions?.[0].getAttribute("value")).toBe("2020"); + expect(yearOptions?.[yearOptions.length - 1].getAttribute("value")).toBe( + "2025", + ); // Update min to 2022, reducing the range const newMin = new Date(2022, 0, 1); - await component.$set({ min: newMin.toISOString() }); + component.$set({ min: newMin.toISOString() }); await tick(); // Check updated years in dropdown yearDropdown = queryByTestId("years"); - yearOptions = yearDropdown.querySelectorAll("goa-dropdown-item"); + yearOptions = yearDropdown?.querySelectorAll("goa-dropdown-item"); // Should now be 4 years: 2022, 2023, 2024, 2025 - expect(yearOptions.length).toBe(4); + expect(yearOptions?.length).toBe(4); // Verify updated first year and unchanged last year - expect(yearOptions[0].getAttribute("value")).toBe("2022"); - expect(yearOptions[yearOptions.length - 1].getAttribute("value")).toBe("2025"); + expect(yearOptions?.[0].getAttribute("value")).toBe("2022"); + expect(yearOptions?.[yearOptions.length - 1].getAttribute("value")).toBe( + "2025", + ); // Verify that 2020 and 2021 are no longer available - const year2020 = yearDropdown.querySelector("goa-dropdown-item[value='2020']"); - const year2021 = yearDropdown.querySelector("goa-dropdown-item[value='2021']"); + const year2020 = yearDropdown?.querySelector( + "goa-dropdown-item[value='2020']", + ); + const year2021 = yearDropdown?.querySelector( + "goa-dropdown-item[value='2021']", + ); expect(year2020).toBeFalsy(); expect(year2021).toBeFalsy(); }); @@ -476,40 +433,48 @@ it("updates year dropdown when max date changes", async () => { const { component, queryByTestId } = render(Calendar, { min: initialMin.toISOString(), - max: initialMax.toISOString() + max: initialMax.toISOString(), }); await tick(); // Check initial years in dropdown let yearDropdown = queryByTestId("years"); - let yearOptions = yearDropdown.querySelectorAll("goa-dropdown-item"); + let yearOptions = yearDropdown?.querySelectorAll("goa-dropdown-item"); // Should be 6 years: 2020, 2021, 2022, 2023, 2024, 2025 - expect(yearOptions.length).toBe(6); + expect(yearOptions?.length).toBe(6); // Verify first and last year options - expect(yearOptions[0].getAttribute("value")).toBe("2020"); - expect(yearOptions[yearOptions.length - 1].getAttribute("value")).toBe("2025"); + expect(yearOptions?.[0].getAttribute("value")).toBe("2020"); + expect(yearOptions?.[yearOptions.length - 1].getAttribute("value")).toBe( + "2025", + ); // Update max to 2023, reducing the range const newMax = new Date(2023, 0, 1); - await component.$set({ max: newMax.toISOString() }); + component.$set({ max: newMax.toISOString() }); await tick(); // Check updated years in dropdown yearDropdown = queryByTestId("years"); - yearOptions = yearDropdown.querySelectorAll("goa-dropdown-item"); + yearOptions = yearDropdown?.querySelectorAll("goa-dropdown-item"); // Should now be 4 years: 2020, 2021, 2022, 2023 - expect(yearOptions.length).toBe(4); + expect(yearOptions?.length).toBe(4); // Verify unchanged first year and updated last year - expect(yearOptions[0].getAttribute("value")).toBe("2020"); - expect(yearOptions[yearOptions.length - 1].getAttribute("value")).toBe("2023"); + expect(yearOptions?.[0].getAttribute("value")).toBe("2020"); + expect(yearOptions?.[yearOptions.length - 1].getAttribute("value")).toBe( + "2023", + ); // Verify that 2024 and 2025 are no longer available - const year2024 = yearDropdown.querySelector("goa-dropdown-item[value='2024']"); - const year2025 = yearDropdown.querySelector("goa-dropdown-item[value='2025']"); + const year2024 = yearDropdown?.querySelector( + "goa-dropdown-item[value='2024']", + ); + const year2025 = yearDropdown?.querySelector( + "goa-dropdown-item[value='2025']", + ); expect(year2024).toBeFalsy(); expect(year2025).toBeFalsy(); -}); \ No newline at end of file +}); diff --git a/libs/web-components/src/components/date-picker/DatePicker.svelte b/libs/web-components/src/components/date-picker/DatePicker.svelte index 3020786f2e..3603b80ad8 100644 --- a/libs/web-components/src/components/date-picker/DatePicker.svelte +++ b/libs/web-components/src/components/date-picker/DatePicker.svelte @@ -9,9 +9,8 @@
- {#if type === "success"} - - {/if} +
+ {#if type === "success"} + + {/if} - {#if type === "failure"} - - {/if} + {#if type === "failure"} + + {/if} - - {message} - + + {message} + +
{#if actionText}
@@ -82,18 +87,35 @@ flex-direction: row; flex-wrap: wrap; align-items: center; - border-radius: var(--goa-border-radius-m); - gap: var(--goa-space-m); - padding: var(--goa-space-m) var(--goa-space-l); - max-width: 640px; - color: var(--goa-color-text-light); - transition: transform 0.3s ease, opacity 0.3s ease; + border-radius: var(--goa-temporary-notification-borderRadius, var(--goa-border-radius-m)); + gap: var(--goa-temporary-notification-row-gap, var(--goa-space-m)); /* 16px between content and action */ + padding: var(--goa-temporary-notification-padding, var(--goa-space-m) var(--goa-space-l)); + max-width: var(--goa-temporary-notification-max-width, 640px); + color: var(--goa-temporary-notification-color-text, var(--goa-color-text-light)); + transition: + transform var(--goa-temporary-notification-transition-duration, 0.3s) ease, + opacity var(--goa-temporary-notification-transition-duration, 0.3s) ease; overflow: hidden; } + /* Add extra bottom padding when progress bar is present */ + .snackbar.progress, + .snackbar.indeterminate { + padding: var(--goa-temporary-notification-padding-with-progress, var(--goa-space-m) var(--goa-space-l) 22px var(--goa-space-l)); + } + + /* Content wrapper keeps icon and message together as a single flex item */ + .content { + display: flex; + align-items: flex-start; /* Icon aligns with first line of text */ + gap: var(--goa-temporary-notification-column-gap, var(--goa-space-s)); + flex: 1 1 auto; + min-width: 0; /* Allow content to shrink */ + } + @media (--not-mobile) { .snackbar { - min-width: 360px; + min-width: var(--goa-temporary-notification-min-width-desktop, 360px); } } @@ -107,8 +129,8 @@ .snackbar.basic, .snackbar.indeterminate, .snackbar.progress { - border: 1px solid var(--goa-color-greyscale-700); - background: var(--goa-color-greyscale-black); + border: var(--goa-temporary-notification-borderWidth, var(--goa-border-width-s)) solid var(--goa-temporary-notification-color-border, var(--goa-color-greyscale-700)); + background: var(--goa-temporary-notification-color-bg-basic, var(--goa-color-greyscale-black)); } .action { @@ -123,7 +145,24 @@ bottom: 0; left: 0; width: 100%; - height: 6px; + height: var(--goa-temporary-notification-progress-bar-height, 6px); + border-radius: 0 0 var(--goa-temporary-notification-progress-bar-borderRadius, 0) var(--goa-temporary-notification-progress-bar-borderRadius, 0); + } + + /* Progress bar browser-specific styling */ + progress::-webkit-progress-bar { + background-color: var(--goa-temporary-notification-progress-bar-color-bg, #adadad); + border-radius: 0 0 var(--goa-temporary-notification-progress-bar-borderRadius, 0) var(--goa-temporary-notification-progress-bar-borderRadius, 0); + } + + progress::-webkit-progress-value { + background-color: var(--goa-temporary-notification-progress-bar-color-fill, white); + border-radius: 0 0 var(--goa-temporary-notification-progress-bar-borderRadius, 0) var(--goa-temporary-notification-progress-bar-borderRadius, 0); + } + + progress::-moz-progress-bar { + background-color: var(--goa-temporary-notification-progress-bar-color-fill, white); + border-radius: 0 0 var(--goa-temporary-notification-progress-bar-borderRadius, 0) var(--goa-temporary-notification-progress-bar-borderRadius, 0); } .show { @@ -143,23 +182,28 @@ } .hide.animate-up { - transform: translateY(-100px); + transform: translateY(calc(-1 * var(--goa-temporary-notification-animation-distance, 100px))); } .hide.animate-down { - transform: translateY(100px); + transform: translateY(var(--goa-temporary-notification-animation-distance, 100px)); } .snackbar.success { - background: var(--goa-color-success-default); + background: var(--goa-temporary-notification-color-bg-success, var(--goa-color-success-default)); } .snackbar.failure { - background: var(--goa-color-emergency-default); + background: var(--goa-temporary-notification-color-bg-failure, var(--goa-color-emergency-default)); } .message { flex: 1 1 auto; - font: var(--goa-typography-body-m); + font: var(--goa-temporary-notification-typography, var(--goa-typography-body-m)); + } + + /* Add top margin to message when icon is present to vertically center first line with icon */ + .content:has(goa-icon) .message { + margin-top: var(--goa-temporary-notification-padding-text-top, var(--goa-space-2xs)); } From 73057cd5e6d823005493eb0d8c166a1812c21f75 Mon Sep 17 00:00:00 2001 From: syedszeeshan <47701214+syedszeeshan@users.noreply.github.com> Date: Wed, 8 Oct 2025 19:46:40 -0400 Subject: [PATCH 16/80] feat(#2361): increase clickable area for radio and checkbox --- .../specs/checkbox.browser.spec.tsx | 48 +++++++++++++++ .../specs/radio.browser.spec.tsx | 59 +++++++++++++++++++ .../src/components/checkbox/Checkbox.svelte | 24 ++++++-- .../components/radio-item/RadioItem.svelte | 11 ++++ 4 files changed, 136 insertions(+), 6 deletions(-) diff --git a/libs/react-components/specs/checkbox.browser.spec.tsx b/libs/react-components/specs/checkbox.browser.spec.tsx index 72d7e3fc0a..9cf69a6e33 100644 --- a/libs/react-components/specs/checkbox.browser.spec.tsx +++ b/libs/react-components/specs/checkbox.browser.spec.tsx @@ -84,4 +84,52 @@ describe("Checkbox", () => { expect(childValue.element().textContent).toBe("false"); }); }); + + it("should have a 44px x 44px touch target area", async () => { + const result = render( + + ); + + const checkbox = result.getByTestId("test-checkbox"); + await vi.waitFor(() => { + expect(checkbox.element()).toBeTruthy(); + }); + + const container = checkbox.element().querySelector(".container") as HTMLElement; + expect(container).toBeTruthy(); + + // Get computed styles for the ::before pseudo-element (touch target) + const beforeStyles = window.getComputedStyle(container, "::before"); + + // Verify the touch target dimensions + expect(beforeStyles.width).toBe("44px"); + expect(beforeStyles.height).toBe("44px"); + expect(beforeStyles.position).toBe("absolute"); + + // Verify the container itself has position: relative for proper positioning context + const containerStyles = window.getComputedStyle(container); + expect(containerStyles.position).toBe("relative"); + + // Verify the actual visual size of the container (24px) vs touch target (44px) + const containerRect = container.getBoundingClientRect(); + expect(containerRect.width).toBe(24); // Visual checkbox is 24px + expect(containerRect.height).toBe(24); // Visual checkbox is 24px + + // Verify the transform is applied correctly for centering + // CSS: transform: translate(-50%, -50%) converts to matrix(a, b, c, d, tx, ty) + // a,b,c,d: 2x2 transformation identity matrix + expect(beforeStyles.transform).toBe("matrix(1, 0, 0, 1, -22, -22)"); + + // Final verification: Check that all styles are applied and rendered + // After the page is fully loaded and all CSS is computed + await vi.waitFor(() => { + const finalContainerStyles = window.getComputedStyle(container); + const finalBeforeStyles = window.getComputedStyle(container, "::before"); + + // Verify final computed styles match expectations + expect(finalContainerStyles.position).toBe("relative"); + expect(finalBeforeStyles.width).toBe("44px"); + expect(finalBeforeStyles.height).toBe("44px"); + }); + }); }); diff --git a/libs/react-components/specs/radio.browser.spec.tsx b/libs/react-components/specs/radio.browser.spec.tsx index 5d15c59caa..959fd13360 100644 --- a/libs/react-components/specs/radio.browser.spec.tsx +++ b/libs/react-components/specs/radio.browser.spec.tsx @@ -98,4 +98,63 @@ describe("Radio", () => { expect(selectedValue.element().textContent).toBe("apple"); }); }); + + it("should have a 44px x 44px touch target area", async () => { + const result = render( + + + + ); + + const radioInput = result.getByTestId("radio-option-option1"); + await vi.waitFor(() => { + expect(radioInput.element()).toBeTruthy(); + }); + + // Get the parent label element and find the .icon element + const label = radioInput.element().closest("label"); + expect(label).toBeTruthy(); + + const icon = label?.querySelector(".icon") as HTMLElement; + expect(icon).toBeTruthy(); + + // Get computed styles for the ::before pseudo-element (touch target) + const beforeStyles = window.getComputedStyle(icon, "::before"); + + // Verify the touch target dimensions + expect(beforeStyles.width).toBe("44px"); + expect(beforeStyles.height).toBe("44px"); + expect(beforeStyles.position).toBe("absolute"); + + // Verify the icon itself has position: relative for proper positioning context + const iconStyles = window.getComputedStyle(icon); + expect(iconStyles.position).toBe("relative"); + + // Verify the actual visual size of the icon (24px) vs touch target (44px) + const iconRect = icon.getBoundingClientRect(); + expect(iconRect.width).toBe(24); // Visual icon is 24px + expect(iconRect.height).toBe(24); // Visual icon is 24px + + // Verify the transform is applied correctly for centering + // CSS: transform: translate(-50%, -50%) converts to matrix(a, b, c, d, tx, ty) + // a,b,c,d: 2x2 transformation identity matrix + expect(beforeStyles.transform).toBe("matrix(1, 0, 0, 1, -22, -22)"); + + // Check ::after pseudo-element (should not interfere with touch target) + const afterStyles = window.getComputedStyle(icon, "::after"); + // ::after should not have conflicting dimensions or positioning + expect(afterStyles.position).not.toBe("absolute"); + + // Final verification: Check that all styles are applied and rendered + // After the page is fully loaded and all CSS is computed + await vi.waitFor(() => { + const finalIconStyles = window.getComputedStyle(icon); + const finalBeforeStyles = window.getComputedStyle(icon, "::before"); + + // Verify final computed styles match expectations + expect(finalIconStyles.position).toBe("relative"); + expect(finalBeforeStyles.width).toBe("44px"); + expect(finalBeforeStyles.height).toBe("44px"); + }); + }); }); diff --git a/libs/web-components/src/components/checkbox/Checkbox.svelte b/libs/web-components/src/components/checkbox/Checkbox.svelte index 87b3224196..568f102847 100644 --- a/libs/web-components/src/components/checkbox/Checkbox.svelte +++ b/libs/web-components/src/components/checkbox/Checkbox.svelte @@ -165,12 +165,12 @@ const checkboxEl = (_rootEl?.getRootNode() as ShadowRoot)?.host as HTMLElement; const fromCheckboxList = checkboxEl?.closest("goa-checkbox-list") !== null; - relay( - _rootEl, - FormFieldMountMsg, - { name, el: _rootEl }, - { bubbles: !fromCheckboxList, timeout: 10 }, - ); + relay( + _rootEl, + FormFieldMountMsg, + { name, el: _rootEl }, + { bubbles: !fromCheckboxList, timeout: 10 }, + ); } function onChange(e: Event) { @@ -387,6 +387,7 @@ max-width: ${maxwidth}; /* Container */ .container { + position: relative; box-sizing: border-box; border: var(--goa-checkbox-border); border-radius: var(--goa-checkbox-border-radius); @@ -398,6 +399,17 @@ max-width: ${maxwidth}; justify-content: center; flex: 0 0 auto; /* prevent squishing of checkbox */ } + + .container::before { + content: ''; + position: absolute; + width: 44px; + height: 44px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + .container:hover { border: var(--goa-checkbox-border-hover); } diff --git a/libs/web-components/src/components/radio-item/RadioItem.svelte b/libs/web-components/src/components/radio-item/RadioItem.svelte index a48bc88f2b..f9ef00972f 100644 --- a/libs/web-components/src/components/radio-item/RadioItem.svelte +++ b/libs/web-components/src/components/radio-item/RadioItem.svelte @@ -342,6 +342,7 @@ } .icon { + position: relative; display: inline-block; height: var(--goa-radio-size); width: var(--goa-radio-size); @@ -354,6 +355,16 @@ margin-top: var(--font-valign-fix); } + .icon::before { + content: ''; + position: absolute; + width: 44px; + height: 44px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + .radio--disabled .label, .radio--disabled ~ .description { color: var(--goa-radio-label-color-disabled); From dff763d9ade3ed4c0ca9fe1d21c90661fa2053e0 Mon Sep 17 00:00:00 2001 From: Benji Franck Date: Fri, 21 Nov 2025 09:42:25 -0700 Subject: [PATCH 17/80] feat(#3142): update dropdown to v2 --- .../src/components/dropdown/Dropdown.svelte | 56 +++++++++++++++---- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/libs/web-components/src/components/dropdown/Dropdown.svelte b/libs/web-components/src/components/dropdown/Dropdown.svelte index af82eb7652..047ddd1cb5 100644 --- a/libs/web-components/src/components/dropdown/Dropdown.svelte +++ b/libs/web-components/src/components/dropdown/Dropdown.svelte @@ -66,6 +66,9 @@ export let error: string = "false"; export let multiselect: string = "false"; export let native: string = "false"; + export let size: "default" | "compact" = "default"; + export let version: "1" | "2" = "1"; + /*** * @deprecated This property has no effect and will be removed in a future version */ @@ -754,6 +757,8 @@ data-testid={testid || `${name}-dropdown`} class="dropdown" class:dropdown-native={_native} + class:compact={size === "compact"} + class:v2={version === "2"} style={` ${calculateMargin(mt, mr, mb, ml)}; --width: ${_width}; @@ -801,6 +806,7 @@ {/if} @@ -827,7 +833,7 @@ aria-haspopup="listbox" disabled={_disabled} readonly={!_filterable} - {placeholder} + placeholder={placeholder || (version === "2" ? "—Select—" : "")} {name} on:keydown={onInputKeyDown} on:keyup={onInputKeyUp} @@ -845,7 +851,8 @@ on:keydown={onClearIconKeyDown} class="dropdown-icon--clear" class:disabled={_disabled} - size="medium" + disabled={_disabled ? "true" : "false"} + size={size === "compact" ? "xsmall" : "medium"} theme="filled" variant="dark" icon="close" @@ -856,7 +863,7 @@ testid="chevron" id={name} class="dropdown-icon--arrow" - size="medium" + size={size === "compact" ? "small" : "medium"} type={_isMenuVisible ? "chevron-up" : "chevron-down"} /> {/if} @@ -936,20 +943,28 @@ cursor: pointer; width: 100%; } + .dropdown-input-group:hover { box-shadow: var(--goa-dropdown-border-hover); border: none; } - .dropdown-input-group:has(input:focus-visible) { + + .dropdown-input-group:has(input:focus-visible), + .dropdown-input-group.error:has(:focus-visible) { box-shadow: var(--goa-dropdown-border), var(--goa-dropdown-border-focus); } + + /* V2: Focus state has a single border */ + .v2 .dropdown-input-group:has(input:focus-visible), + .v2 .dropdown-input-group.error:has(:focus-visible) { + box-shadow: var(--goa-dropdown-border-focus); + } + .dropdown-input-group.error, .dropdown-input-group.error:hover { box-shadow: var(--goa-dropdown-border-error); } - .dropdown-input-group.error:has(:focus-visible) { - box-shadow: var(--goa-dropdown-border), var(--goa-dropdown-border-focus); - } + @container not (--mobile) { .dropdown-input-group { width: var(--width, 100%); @@ -1015,9 +1030,9 @@ /** menu **/ ul[role="listbox"] { - border-radius: var(--goa-dropdown-border-radius); + border-radius: var(--goa-dropdown-menu-border-radius, var(--goa-dropdown-border-radius)); padding: 0; - margin: 0; + margin: var(--goa-dropdown-menu-margin, 0); } /* dropdown items */ @@ -1031,8 +1046,10 @@ white-space: normal; /* Allows text to wrap */ word-break: break-word; /* Ensures long words break onto the next line */ overflow-wrap: break-word; /* Alternative for word wrapping */ + border-radius: var(--goa-dropdown-item-border-radius, 0); } + .dropdown-item:hover, .dropdown-item--highlighted { background: var(--goa-dropdown-item-color-bg-hover); @@ -1088,8 +1105,9 @@ .dropdown-native::after { content: ""; position: absolute; - right: 0.6rem; - top: 0.6rem; + right: var(--goa-dropdown-space-icon-text); + top: 50%; + transform: translateY(-50%); pointer-events: none; width: 1.5rem; height: 1.5rem; @@ -1109,4 +1127,20 @@ color: var(--goa-dropdown-color-text-placeholder); opacity: 1; } + + input:disabled::placeholder { + color: var(--goa-dropdown-color-text-disabled); + } + + /* Compact Size */ + .compact input, + .compact select { + padding: var(--goa-dropdown-compact-padding); + height: var(--goa-dropdown-compact-height); + font: var(--goa-dropdown-compact-typography); + } + + .compact .dropdown-item { + font: var(--goa-dropdown-compact-item-typography); + } From b4bd11caade71166a1df5ab4e36451234916f477 Mon Sep 17 00:00:00 2001 From: Benji Franck Date: Tue, 18 Nov 2025 16:00:05 -0700 Subject: [PATCH 18/80] feat(#3070): v2 badge with emphasis and size --- .../src/components/badge/Badge.svelte | 184 +++++++++++++++++- 1 file changed, 179 insertions(+), 5 deletions(-) diff --git a/libs/web-components/src/components/badge/Badge.svelte b/libs/web-components/src/components/badge/Badge.svelte index ed0ac5d79f..6569984ef9 100644 --- a/libs/web-components/src/components/badge/Badge.svelte +++ b/libs/web-components/src/components/badge/Badge.svelte @@ -8,7 +8,8 @@ import { typeValidator, toBoolean } from "../../common/utils"; import type { GoAIconType } from "../icon/Icon.svelte"; - // Validator + + // Validators const [Types, validateType] = typeValidator( "Badge type", [ @@ -39,12 +40,36 @@ "red-light", "violet-light", "yellow-light", + "sky", + "prairie", + "lilac", + "pasture", + "sunset", + "dawn", + "default", ], true, ); + const [badgeSizes, validateBadgeSize] = typeValidator("Badge size", [ + "medium", + "large", + ]); + + const [versions, validateVersion] = typeValidator("Badge version", [ + "1", + "2", + ]); + + const [emphasisLevels, validateEmphasisLevel] = typeValidator( + "Badge emphasis level", + ["subtle", "strong"], + ); + //Type type BadgeType = (typeof Types)[number]; + type BadgeSize = (typeof badgeSizes)[number]; + type BadgeVersion = (typeof versions)[number]; export let type: BadgeType; @@ -54,6 +79,9 @@ export let icon: string = ""; export let icontype: GoAIconType | null = null; export let arialabel: string = ""; + export let size: BadgeSize = "medium"; + export let emphasis: (typeof emphasisLevels)[number] = "strong"; + export let version: BadgeVersion = "1"; // margin export let mt: Spacing = null; @@ -94,10 +122,20 @@ "red-light": "information-circle", "violet-light": "information-circle", "yellow-light": "information-circle", + sky: "information-circle", + prairie: "information-circle", + lilac: "information-circle", + pasture: "information-circle", + sunset: "information-circle", + dawn: "information-circle", + default: "information-circle", }[type]; onMount(() => { validateType(type); + validateBadgeSize(size); + validateEmphasisLevel(emphasis); + validateVersion(version); if (!showIcon && !content) { console.warn( @@ -115,15 +153,16 @@ style={calculateMargin(mt, mr, mb, ml)} data-testid={testid} data-type="goa-badge" - class="goa-badge badge-{type}" + class="goa-badge badge-{type} badge-{size} badge-{emphasis}" class:icon-only={showIconOnly} + class:v2={version === "2"} > {#if showIcon} {:else}
@@ -207,8 +246,14 @@ } .goa-badge.badge-archived { - background-color: var(--goa-color-greyscale-700); - color: var(--goa-badge-dark-color-content); + background-color: var( + --goa-badge-archived-color-bg, + var(--goa-color-greyscale-700) + ); + color: var( + --goa-badge-archived-color-content, + var(--goa-badge-dark-color-content) + ); } .goa-badge.badge-aqua { @@ -305,4 +350,133 @@ background-color: var(--goa-color-extended-light-yellow); color: var(--goa-badge-light-color-content); } + + .v2 .goa-badge-content { + padding-bottom: 0; + } + + /* Version 2: Default Colours */ + .v2.badge-default { + background-color: var(--goa-badge-default-color-bg); + color: var(--goa-badge-default-color-content); + } + + .v2.badge-default.badge-subtle { + background-color: var(--goa-badge-default-subtle-color-bg); + box-shadow: var(--goa-badge-default-subtle-border); + color: var(--goa-badge-default-subtle-color-content); + } + + .v2.goa-badge.badge-archived.badge-subtle { + background-color: var(--goa-badge-archived-subtle-color-bg); + box-shadow: var(--goa-badge-archived-subtle-border); + color: var(--goa-badge-archived-subtle-color-content); + } + + /* Version 2: Extended Colours */ + + .v2.badge-sky { + background-color: var(--goa-color-extended-sky-default); + color: var(--goa-color-extended-sky-text); + } + + .v2.badge-prairie { + background-color: var(--goa-color-extended-prairie-default); + color: var(--goa-color-extended-prairie-text); + } + + .v2.badge-lilac { + background-color: var(--goa-color-extended-lilac-default); + color: var(--goa-color-extended-lilac-text); + } + + .v2.badge-pasture { + background-color: var(--goa-color-extended-pasture-default); + color: var(--goa-color-extended-pasture-text); + } + + .v2.badge-sunset { + background-color: var(--goa-color-extended-sunset-default); + color: var(--goa-color-extended-sunset-text); + } + + .v2.badge-dawn { + background-color: var(--goa-color-extended-dawn-default); + color: var(--goa-color-extended-dawn-text); + } + + .v2.badge-subtle.badge-sky { + background-color: var(--goa-color-extended-sky-light); + box-shadow: var(--goa-color-extended-sky-subtle-border); + color: var(--goa-color-extended-sky-text); + } + + .v2.badge-subtle.badge-prairie { + background-color: var(--goa-color-extended-prairie-light); + box-shadow: var(--goa-color-extended-prairie-subtle-border); + color: var(--goa-color-extended-prairie-text); + } + + .v2.badge-subtle.badge-lilac { + background-color: var(--goa-color-extended-lilac-light); + box-shadow: var(--goa-color-extended-lilac-subtle-border); + color: var(--goa-color-extended-lilac-text); + } + + .v2.badge-subtle.badge-pasture { + background-color: var(--goa-color-extended-pasture-light); + box-shadow: var(--goa-color-extended-pasture-subtle-border); + color: var(--goa-color-extended-pasture-text); + } + + .v2.badge-subtle.badge-sunset { + background-color: var(--goa-color-extended-sunset-light); + box-shadow: var(--goa-color-extended-sunset-subtle-border); + color: var(--goa-color-extended-sunset-text); + } + + .v2.badge-subtle.badge-dawn { + background-color: var(--goa-color-extended-dawn-light); + box-shadow: var(--goa-color-extended-dawn-subtle-border); + color: var(--goa-color-extended-dawn-text); + } + + /* Version 2: Subtle emphasis for standard colours */ + + .v2.goa-badge.badge-subtle.badge-information { + background-color: var(--goa-badge-info-subtle-color-bg); + color: var(--goa-badge-info-subtle-color-content); + box-shadow: var(--goa-badge-info-subtle-border); + } + + .v2.goa-badge.badge-subtle.badge-success { + background-color: var(--goa-badge-success-subtle-color-bg); + color: var(--goa-badge-success-subtle-color-content); + box-shadow: var(--goa-badge-success-subtle-border); + } + + .v2.goa-badge.badge-subtle.badge-important { + background-color: var(--goa-badge-important-subtle-color-bg); + color: var(--goa-badge-important-subtle-color-content); + box-shadow: var(--goa-badge-important-subtle-border); + } + + .v2.goa-badge.badge-subtle.badge-emergency { + background-color: var(--goa-badge-emergency-subtle-color-bg); + color: var(--goa-badge-emergency-subtle-color-content); + box-shadow: var(--goa-badge-emergency-subtle-border); + } + + /* Version 2: Large size */ + + .v2.goa-badge.badge-large { + height: var(--goa-badge-height-large); + padding: var(--goa-badge-padding-large); + --goa-icon-size: var(--goa-badge-icon-size-large); + } + + .v2.goa-badge.badge-large .goa-badge-content { + font-size: var(--goa-badge-font-size-large); + line-height: var(--goa-badge-line-height-large); + } From 4544049cd25530e56765aead389fd045617178a0 Mon Sep 17 00:00:00 2001 From: Benji Franck Date: Thu, 20 Nov 2025 08:05:22 -0700 Subject: [PATCH 19/80] feat(#2915): callout v2 layout and emphasis levels --- .../src/components/callout/Callout.svelte | 210 ++++++++++++++++-- 1 file changed, 196 insertions(+), 14 deletions(-) diff --git a/libs/web-components/src/components/callout/Callout.svelte b/libs/web-components/src/components/callout/Callout.svelte index 8784830b77..394226a1c4 100644 --- a/libs/web-components/src/components/callout/Callout.svelte +++ b/libs/web-components/src/components/callout/Callout.svelte @@ -19,16 +19,23 @@ "medium", "large", ]); + const [CalloutEmphasis, validateCalloutEmphasis] = typeValidator( + "Callout emphasis", + ["high", "medium", "low"], + ); const [AriaLive, validateAriaLive] = typeValidator("Aria live", [ "off", "assertive", "polite", ]); + const [Version, validateVersion] = typeValidator("Version", ["1", "2"]); // Types type CalloutType = (typeof Types)[number]; type CalloutSize = (typeof CalloutSizes)[number]; + type CalloutEmphasisType = (typeof CalloutEmphasis)[number]; type AriaLiveType = (typeof AriaLive)[number]; + type VersionType = (typeof Version)[number]; // margin export let mt: Spacing = null; @@ -38,11 +45,13 @@ export let size: CalloutSize = "large"; export let type: CalloutType; + export let emphasis: CalloutEmphasisType = "medium"; export let heading: string = ""; export let maxwidth: string = "none"; export let testid: string = ""; export let arialive: AriaLiveType = "off"; export let icontheme: IconTheme = "outline"; + export let version: VersionType = "1"; // Private @@ -68,7 +77,9 @@ onMount(() => { validateCalloutSize(size); + validateCalloutEmphasis(emphasis); validateAriaLive(arialive); + validateVersion(version); setTimeout(() => { validateType(type); @@ -85,24 +96,35 @@ ${calculateMargin(mt, mr, mb, ml)}; max-width: ${maxwidth}; `} - class="notification {type}" + class="notification {type} emphasis-{emphasis}" class:medium={isMediumCallout} + class:v2={version === "2"} data-testid={testid} aria-live={arialive} > - - - - - {#if heading} -

{heading}

- {/if} - -
+ {#if version === "2"} +
+ +

{heading}

+
+
+ +
+ {:else} + + + + + {#if heading} +

{heading}

+ {/if} + +
+ {/if}
@@ -221,4 +243,164 @@ .notification.medium .icon { padding: var(--goa-callout-m-statusbar-padding); } + + /* Version two: Layout */ + + .v2.notification { + flex-direction: column; + border: var(--goa-callout-border); + } + + .v2.notification .heading { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: var(--goa-callout-heading-padding); + gap: var(--goa-callout-heading-gap); + color: var(--goa-callout-heading-color); + } + + .v2.notification .heading-label { + margin-top: var(--goa-space-3xs); + margin-bottom: var(--goa-space-3xs); + font: var(--goa-callout-heading-typography); + } + + .v2.notification .body { + padding: var(--goa-callout-body-padding); + color: var(--goa-callout-body-color); + font: var(--goa-callout-body-typography); + } + + /* Version two: Low emphasis layout */ + + .v2.emphasis-low .body { + padding: var(--goa-callout-l-with-heading-body-padding); + } + + .v2.emphasis-low:has(.heading-label:empty) { + flex-direction: row; + align-items: start; + } + + .v2.emphasis-low .heading-label:empty { + display: none; + } + + .v2.emphasis-low:has(.heading-label:empty) .heading { + padding-right: var(--goa-space-xs); + } + + .v2.emphasis-low:has(.heading-label:empty) .body { + padding: var(--goa-callout-l-without-heading-body-padding); + } + + .v2.information { + background-color: var(--goa-callout-info-content-bg-color); + } + + /* Version two: Types */ + + .v2.information .heading { + background-color: var(--goa-callout-info-heading-bg-color); + --fill-color: var(--goa-callout-info-icon-color); + } + + .v2.information.emphasis-low { + border-color: var(--goa-callout-l-info-border-color); + background-color: var(--goa-callout-l-info-content-bg-color); + } + + .v2.information.emphasis-high { + border-color: var(--goa-callout-h-info-border-color); + background-color: var(--goa-callout-h-info-content-bg-color); + } + + .v2.information.emphasis-high .heading { + background-color: var(--goa-callout-h-info-heading-bg-color); + color: var(--goa-callout-h-info-heading-color); + --fill-color: var(--goa-callout-h-info-icon-color); + } + + .v2.emergency { + background-color: var(--goa-callout-emergency-content-bg-color); + } + + .v2.emergency .heading { + background-color: var(--goa-callout-emergency-heading-bg-color); + --fill-color: var(--goa-callout-emergency-icon-color); + } + + .v2.emergency.emphasis-low { + border-color: var(--goa-callout-l-emergency-border-color); + background-color: var(--goa-callout-l-emergency-content-bg-color); + } + + .v2.emergency.emphasis-high { + border-color: var(--goa-callout-h-emergency-border-color); + background-color: var(--goa-callout-h-emergency-content-bg-color); + } + + .v2.emergency.emphasis-high .heading { + background-color: var(--goa-callout-h-emergency-heading-bg-color); + color: var(--goa-callout-h-emergency-heading-color); + --fill-color: var(--goa-callout-h-emergency-icon-color); + } + + .v2.important { + background-color: var(--goa-callout-important-content-bg-color); + } + + .v2.important .heading { + background-color: var(--goa-callout-important-heading-bg-color); + --fill-color: var(--goa-callout-important-icon-color); + } + + .v2.important.emphasis-low { + border-color: var(--goa-callout-l-important-border-color); + background-color: var(--goa-callout-l-important-content-bg-color); + } + + .v2.important.emphasis-high { + border-color: var(--goa-callout-h-important-border-color); + background-color: var(--goa-callout-h-important-content-bg-color); + } + + .v2.important.emphasis-high .heading { + background-color: var(--goa-callout-h-important-heading-bg-color); + color: var(--goa-callout-h-important-heading-color); + --fill-color: var(--goa-callout-h-important-icon-color); + } + + .v2.success { + background-color: var(--goa-callout-success-content-bg-color); + } + + .v2.success .heading { + background-color: var(--goa-callout-success-heading-bg-color); + --fill-color: var(--goa-callout-success-icon-color); + } + + .v2.success.emphasis-low { + border-color: var(--goa-callout-l-success-border-color); + background-color: var(--goa-callout-l-success-content-bg-color); + } + + .v2.success.emphasis-high { + border-color: var(--goa-callout-h-success-border-color); + background-color: var(--goa-callout-h-success-content-bg-color); + } + + .v2.success.emphasis-high .heading { + background-color: var(--goa-callout-h-success-heading-bg-color); + color: var(--goa-callout-h-success-heading-color); + --fill-color: var(--goa-callout-h-success-icon-color); + } + + .v2.information.emphasis-low .heading, + .v2.important.emphasis-low .heading, + .v2.emergency.emphasis-low .heading, + .v2.success.emphasis-low .heading { + background-color: transparent; + } From c4e0ef17de9817c209531a2ae15f15c0bc7383a1 Mon Sep 17 00:00:00 2001 From: Chris Olsen Date: Fri, 21 Nov 2025 21:36:22 -0700 Subject: [PATCH 20/80] fix: ensure date based tests don't fail later in the day --- .../specs/datepicker.browser.spec.tsx | 35 +++++++------------ .../src/components/calendar/calendar.spec.ts | 5 ++- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/libs/react-components/specs/datepicker.browser.spec.tsx b/libs/react-components/specs/datepicker.browser.spec.tsx index e64bbf4028..51fefb3706 100644 --- a/libs/react-components/specs/datepicker.browser.spec.tsx +++ b/libs/react-components/specs/datepicker.browser.spec.tsx @@ -70,27 +70,20 @@ describe("DatePicker", () => { describe("DatePicker Keyboard Navigation", () => { [ - { desc: "previous day", key: "{ArrowLeft}", diff: { fn: addDays, value: -1 } }, - { desc: "next day", key: "{ArrowRight}", diff: { fn: addDays, value: 1 } }, - { desc: "previous week", key: "{ArrowUp}", diff: { fn: addDays, value: -7 } }, - { desc: "next week", key: "{ArrowDown}", diff: { fn: addDays, value: 7 } }, - { desc: "previous month", key: "{PageUp}", diff: { fn: addMonths, value: -1 } }, - { desc: "next month", key: "{PageDown}", diff: { fn: addMonths, value: 1 } }, - { desc: "previous year", key: "{Shift>}{PageUp}", diff: { fn: addYears, value: -1 } }, - { desc: "next year", key: "{Shift>}{PageDown}", diff: { fn: addYears, value: 1 } }, - ].forEach(({ desc, key, diff }) => { + { value: "2025-03-01", expected: "2025-02-28", formatted: "February 28, 2025", desc: "previous day", key: "{ArrowLeft}" }, + { value: "2025-03-01", expected: "2025-03-02", formatted: "March 2, 2025", desc: "next day", key: "{ArrowRight}" }, + { value: "2025-03-01", expected: "2025-02-22", formatted: "February 22, 2025", desc: "previous week", key: "{ArrowUp}" }, + { value: "2025-03-01", expected: "2025-03-08", formatted: "March 8, 2025", desc: "next week", key: "{ArrowDown}" }, + { value: "2025-03-01", expected: "2025-02-01", formatted: "February 1, 2025", desc: "previous month", key: "{PageUp}" }, + { value: "2025-03-01", expected: "2025-04-01", formatted: "April 1, 2025", desc: "next month", key: "{PageDown}" }, + { value: "2025-03-01", expected: "2024-03-01", formatted: "March 1, 2024", desc: "previous year", key: "{Shift>}{PageUp}" }, + { value: "2025-03-01", expected: "2026-03-01", formatted: "March 1, 2026", desc: "next year", key: "{Shift>}{PageDown}" }, + ].forEach(({ value, expected, formatted, desc, key }) => { it(`navigates to the ${desc} when ${key} is pressed`, async () => { - const inputDate = new Date(); - const currentDate = new Date( - inputDate.getFullYear(), - inputDate.getMonth(), - inputDate.getDate() - ); - const handleChange = vi.fn(); const Component = () => { - return { - handleChange(detail.value) + return { + handleChange(detail.valueStr) }} />; }; @@ -99,12 +92,10 @@ describe("DatePicker", () => { await userEvent.type(input, key); - const expectedDate = diff.fn(currentDate, diff.value); await vi.waitFor(() => { const inputEl = input.element() as HTMLInputElement; - const newValue = format(expectedDate, "MMMM d, yyyy"); - expect(inputEl.value).toBe(newValue); - expect(handleChange).toBeCalledWith(expectedDate) + expect(inputEl.value).toBe(formatted); + expect(handleChange).toBeCalledWith(expected) }); }) }) diff --git a/libs/web-components/src/components/calendar/calendar.spec.ts b/libs/web-components/src/components/calendar/calendar.spec.ts index 5d4ec3b081..4f716ba837 100644 --- a/libs/web-components/src/components/calendar/calendar.spec.ts +++ b/libs/web-components/src/components/calendar/calendar.spec.ts @@ -68,13 +68,12 @@ it("should have no date selected if one not provided", async () => { }); it("sets the preset date value", async () => { - const date = new Date().toISOString(); + const date = "2025-03-01"; const { container } = render(Calendar, { value: date }); await tick(); - const timestamp = toDayStart(new Date(date)); const dayEl = container - .querySelector(`.selected[data-date="${getDateStamp(timestamp)}"]`) + .querySelector(`.selected[data-date="${date}"]`) ?.querySelector("[data-testid=date]"); expect(dayEl).toBeTruthy(); }); From 716a9e6e983a876bfc41325c00ee68d10af9e4f4 Mon Sep 17 00:00:00 2001 From: Benji Franck Date: Sat, 22 Nov 2025 12:43:15 -0700 Subject: [PATCH 21/80] feat(#2952): update filter chip to v2 --- .../components/filter-chip/FilterChip.spec.ts | 4 +- .../components/filter-chip/FilterChip.svelte | 196 ++++++++++++++---- 2 files changed, 155 insertions(+), 45 deletions(-) diff --git a/libs/web-components/src/components/filter-chip/FilterChip.spec.ts b/libs/web-components/src/components/filter-chip/FilterChip.spec.ts index afa32b35d1..99502d5fce 100644 --- a/libs/web-components/src/components/filter-chip/FilterChip.spec.ts +++ b/libs/web-components/src/components/filter-chip/FilterChip.spec.ts @@ -43,7 +43,9 @@ describe("FilterChip", () => { }); const chip = container.querySelector(".chip"); - expect(chip).toHaveStyle("min-width: 56px"); + expect(chip).toHaveStyle( + "min-width: var(--goa-filter-chip-min-width, 56px)", + ); }); it("should have an unfilled close icon by default for deletable chips", async () => { diff --git a/libs/web-components/src/components/filter-chip/FilterChip.svelte b/libs/web-components/src/components/filter-chip/FilterChip.svelte index 32d2fb92f7..536883300c 100644 --- a/libs/web-components/src/components/filter-chip/FilterChip.svelte +++ b/libs/web-components/src/components/filter-chip/FilterChip.svelte @@ -5,6 +5,7 @@ import { toBoolean } from "../../common/utils"; import type { Spacing } from "../../common/styling"; import { calculateMargin } from "../../common/styling"; + import type { GoAIconType } from "../icon/Icon.svelte"; // margin export let mt: Spacing = null; @@ -15,8 +16,11 @@ // Props export let error: string = "false"; export let content: string; + export let secondarytext: string = ""; + export let leadingicon: GoAIconType | null = null; export let testid: string = ""; export let ariaLabel: string = ""; + export let version: "1" | "2" = "1"; // Private variables let el: HTMLElement; @@ -47,53 +51,108 @@ -
(_hovering = true)} - on:mouseout={() => (_hovering = false)} - on:focus={() => (_focused = true)} - on:blur={() => (_focused = false)} -> -
- {content} +{#if version === "2"} + - -
+{:else} +
(_hovering = true)} + on:mouseout={() => (_hovering = false)} + on:focus={() => (_focused = true)} + on:blur={() => (_focused = false)} + > +
+ {content} +
+ +
+{/if} From e7424e24bcc21981d655ba8145d7c12ad1bf971d Mon Sep 17 00:00:00 2001 From: Thomas Jeffery Date: Tue, 21 Oct 2025 15:07:32 -0600 Subject: [PATCH 22/80] feat(#2953): update icon-button to v2 --- .../components/icon-button/IconButton.svelte | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/libs/web-components/src/components/icon-button/IconButton.svelte b/libs/web-components/src/components/icon-button/IconButton.svelte index da6a1014cc..2bfdba4337 100644 --- a/libs/web-components/src/components/icon-button/IconButton.svelte +++ b/libs/web-components/src/components/icon-button/IconButton.svelte @@ -104,7 +104,7 @@ } .goa-icon-button--small { - padding: var(--goa-icon-button-medium-padding); + padding: var(--goa-icon-button-small-padding, var(--goa-icon-button-medium-padding)); } .goa-icon-button--medium { @@ -115,10 +115,6 @@ padding: var(--goa-icon-button-large-padding); } - .goa-icon-button--xlarge { - padding: var(--goa-icon-button-large-padding); - } - button { display: inline-flex; align-items: center; @@ -127,9 +123,7 @@ background: transparent; cursor: pointer; border: none; - border-radius: var(--goa-icon-button-medium-border-radius); - padding: var(--padding); - cursor: pointer; + border-radius: var(--goa-icon-button-border-radius, var(--goa-icon-button-medium-border-radius)); transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, transform 0.1s ease-in-out; @@ -144,7 +138,7 @@ } button:focus-visible { - box-shadow: 0 0 0 3px var(--goa-color-interactive-focus); + box-shadow: 0 0 0 var(--goa-icon-button-focus-border-width, 3px) var(--goa-icon-button-focus-border-color, var(--goa-color-interactive-focus)); outline: none; } @@ -165,8 +159,7 @@ fill: var(--goa-icon-button-default-color); } - .color:hover, - .color:focus-visible { + .color:hover:not(:focus-visible) { color: var(--goa-icon-button-default-hover-color); fill: var(--goa-icon-button-default-hover-color); background-color: var(--goa-icon-button-default-hover-color-bg); @@ -183,15 +176,14 @@ fill: var(--goa-icon-button-dark-color); } - .dark:hover, - .dark:focus-visible, + .dark:hover:not(:focus-visible), .dark:active { background-color: var(--goa-icon-button-dark-hover-color-bg); } .dark:disabled { - color: var(--goa-icon-button-dark-disabled-color-bg); - fill: var(--goa-icon-button-dark-disabled-color-bg); + color: var(--goa-icon-button-dark-disabled-color, var(--goa-icon-button-dark-disabled-color-bg)); + fill: var(--goa-icon-button-dark-disabled-color, var(--goa-icon-button-dark-disabled-color-bg)); } /* Type: nocolor (same as dark, not documented) */ @@ -200,8 +192,7 @@ fill: var(--goa-icon-button-dark-color); } - .nocolor:hover, - .nocolor:focus-visible, + .nocolor:hover:not(:focus-visible), .nocolor:active { background-color: var(--goa-icon-button-dark-hover-color-bg); } @@ -212,9 +203,10 @@ fill: var(--goa-icon-button-destructive-color); } - .destructive:hover, - .destructive:focus-visible, + .destructive:hover:not(:focus-visible), .destructive:active { + color: var(--goa-icon-button-destructive-hover-color); + fill: var(--goa-icon-button-destructive-hover-color); background-color: var(--goa-icon-button-destructive-hover-color-bg); } @@ -229,8 +221,7 @@ fill: var(--goa-icon-button-light-color); } - .light:hover, - .light:focus-visible, + .light:hover:not(:focus-visible), .light:active { background-color: var(--goa-icon-button-light-hover-color-bg); } @@ -246,8 +237,7 @@ fill: var(--goa-icon-button-light-color); } - .inverted:hover, - .inverted:focus-visible, + .inverted:hover:not(:focus-visible), .inverted:active { background-color: var(--goa-icon-button-light-hover-color-bg); } From cd7e20e804f04a574a12282e7b1a33623f1e5861 Mon Sep 17 00:00:00 2001 From: Thomas Jeffery Date: Thu, 23 Oct 2025 13:47:10 -0600 Subject: [PATCH 23/80] feat(#3134): update form-item to v2 --- .../src/components/form-item/FormItem.svelte | 133 +++++++++++++++--- 1 file changed, 113 insertions(+), 20 deletions(-) diff --git a/libs/web-components/src/components/form-item/FormItem.svelte b/libs/web-components/src/components/form-item/FormItem.svelte index 5bf6253e5d..aaaf4771a5 100644 --- a/libs/web-components/src/components/form-item/FormItem.svelte +++ b/libs/web-components/src/components/form-item/FormItem.svelte @@ -44,12 +44,20 @@ ); const [LABEL_SIZE_TYPES, validateLabelSize] = typeValidator( "Label size type", - ["regular", "large"], + ["compact", "regular", "large"], + false, + ); + const [Version, validateVersion] = typeValidator("Version", ["1", "2"]); + const [INPUT_TYPES, validateType] = typeValidator( + "Input type", + ["", "text-input", "textarea", "checkbox-list", "radio-group"], false, ); type RequirementType = (typeof REQUIREMENT_TYPES)[number]; type LabelSizeType = (typeof LABEL_SIZE_TYPES)[number]; + type VersionType = (typeof Version)[number]; + type InputType = (typeof INPUT_TYPES)[number]; // margin export let mt: Spacing = null; @@ -65,6 +73,8 @@ export let error: string = ""; export let requirement: RequirementType = ""; export let maxwidth: string = "none"; + export let version: VersionType = "1"; + export let type: InputType = ""; // **For the public-form only** // Overrides the label value within the form-summary to provide a shorter description of the value @@ -77,9 +87,15 @@ let _helpTextId = `helptext-${generateRandomId()}`; let _hasError = false; + // Computed: Error icon size based on form item size + // Compact: xsmall (16px), Regular/Large: small (18px) + $: errorIconSize = labelsize === 'compact' ? 'xsmall' : 'small'; + onMount(() => { validateRequirementType(requirement); validateLabelSize(labelsize); + validateVersion(version); + validateType(type); receive(_rootEl, (action, data) => { switch (action) { @@ -178,6 +194,8 @@
- {#if $$slots.error || error} - - {/if} + {#if ($$slots.error || error) || ($$slots.helptext || helptext)} +
+ {#if $$slots.error || error} + + {/if} - {#if $$slots.helptext || helptext} -
- - {helptext} - + {#if $$slots.helptext || helptext} +
+ + {helptext} + +
+ {/if}
{/if}
@@ -237,32 +261,101 @@ padding-bottom: var(--goa-form-item-label-large-padding-bottom); } + /* V2 ONLY: Compact size variant */ + .v2.compact .label { + font: var(--goa-form-item-label-compact-typography); + padding-bottom: var(--goa-form-item-label-compact-padding-bottom); + } + .label em { font: var(--goa-form-item-optional-label-typography); color: var(--goa-form-item-optional-label-color); margin-left: var(--goa-space-2xs); /* Space between label and requirement */ } + /* Messages container - spacing from input */ + /* V1: Always 12px (--goa-form-item-message-margin-top) */ + /* V2: Size and input-type specific spacing */ + + .large .messages-container { + margin-top: var(--goa-form-item-message-margin-top-large, var(--goa-form-item-message-margin-top, 0.75rem)); /* V2: 16px, V1: 12px */ + } + + .regular .messages-container { + margin-top: var(--goa-form-item-message-margin-top-regular, var(--goa-form-item-message-margin-top, 0.75rem)); /* V2: 12px, V1: 12px */ + } + + /* V2 ONLY: Compact size messages container spacing */ + .v2.compact .messages-container { + margin-top: var(--goa-form-item-message-margin-top-compact, 0.5rem); /* V2: 8px */ + } + + /* InputType overrides: checkbox-list, radio-group for adjusted sizing */ + .large.checkbox-list .messages-container, + .large.radio-group .messages-container { + margin-top: var(--goa-form-item-message-margin-top-selection-large, var(--goa-form-item-message-margin-top, 0.75rem)); /* V2: 20px, V1: 12px */ + } + + .regular.checkbox-list .messages-container, + .regular.radio-group .messages-container { + margin-top: var(--goa-form-item-message-margin-top-selection-regular, var(--goa-form-item-message-margin-top, 0.75rem)); /* V2: 16px, V1: 12px */ + } + + /* V2 ONLY: Compact size inputType pattern overrides for adjusted sizing */ + .v2.compact.checkbox-list .messages-container, + .v2.compact.radio-group .messages-container { + margin-top: var(--goa-form-item-message-margin-top-selection-compact, 0.75rem); /* V2: 12px */ + } + + /* V2: Flex layout for error + helper stacking */ + .v2 .messages-container { + display: flex; + flex-direction: column; + gap: var(--goa-form-item-message-stack-gap); + } + + .v2 .messages-container.compact { + gap: var(--goa-form-item-message-stack-gap-compact); + } + + /* Error message */ .error-msg { display: flex; align-items: flex-start; - gap: var(--goa-space-2xs); + gap: var(--goa-form-item-message-gap); font: var(--goa-form-item-message-typography); color: var(--goa-form-item-error-message-color); - margin-top: var(--goa-form-item-message-margin-top); } - .error-msg goa-icon { - transform: translateY(calc(var(--goa-space-2xs) * -1)); + /* V2 ONLY: Compact icon gap */ + .v2.compact .error-msg { + gap: var(--goa-form-item-message-gap-compact); + } + + /* Error text alignment - 1px margin-top for regular/large to align with icon */ + .error-text { + margin-top: 0.0625rem; /* 1px */ } + /* V2 ONLY: Compact error text alignment (icon and text align naturally) */ + .v2.compact .error-text { + margin-top: 0; + } + + /* Helper message */ .help-msg { font: var(--goa-form-item-message-typography); color: var(--goa-form-item-help-message-color); - margin-top: var(--goa-form-item-message-margin-top); } + /* V1: Gap between error and helper when both present (sibling selector) */ + /* Uses space.xs (8px) for proper vertical stacking */ .error-msg + .help-msg { - margin-top: var(--goa-form-item-message-gap); + margin-top: var(--goa-form-item-message-stack-gap, var(--goa-space-xs, 0.5rem)); + } + + /* V2: Remove sibling margin (flex gap handles spacing) */ + .v2 .error-msg + .help-msg { + margin-top: 0; } From 7f3f51bf8e675a2a3e8134f63b610efd62f6562c Mon Sep 17 00:00:00 2001 From: Benji Franck Date: Fri, 21 Nov 2025 10:09:01 -0700 Subject: [PATCH 24/80] feat(#2949): update checkbox to v2 --- .../src/components/checkbox/Checkbox.svelte | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/libs/web-components/src/components/checkbox/Checkbox.svelte b/libs/web-components/src/components/checkbox/Checkbox.svelte index 568f102847..9c22bc77b1 100644 --- a/libs/web-components/src/components/checkbox/Checkbox.svelte +++ b/libs/web-components/src/components/checkbox/Checkbox.svelte @@ -39,11 +39,13 @@ export let description: string = ""; export let revealarialabel: string = ""; // screen reader will announce this when reveal slot is displayed export let maxwidth: string = "none"; + export let size: "default" | "compact" = "default"; + export let version: "1" | "2" = "1"; // margin export let mt: Spacing = null; export let mr: Spacing = null; - export let mb: Spacing = "m"; + export let mb: Spacing = null; export let ml: Spacing = null; // Private @@ -84,6 +86,7 @@ // hold on to the initial value to prevent losing it on check changes _value = value; _descriptionId = `description_${name}`; + mb ??= size === "compact" ? "s" : "m"; addRelayListener(); addRevealSlotListener(); @@ -244,6 +247,8 @@
- {#if isIndeterminate} + {#if isIndeterminate && version === "2"} + + + + {:else if isIndeterminate} + {:else if isChecked && version === "2"} + + + {:else if isChecked} From c88e63f5d55fa24e881a6aebd90db92a17c8fa98 Mon Sep 17 00:00:00 2001 From: Thomas Jeffery Date: Thu, 30 Oct 2025 12:42:54 -0600 Subject: [PATCH 25/80] feat(#3143): update table to v2 --- .../src/assets/css/components.css | 173 ++++++++++++++++-- .../src/components/table/Table.svelte | 22 ++- .../components/table/TableSortHeader.svelte | 12 +- 3 files changed, 180 insertions(+), 27 deletions(-) diff --git a/libs/web-components/src/assets/css/components.css b/libs/web-components/src/assets/css/components.css index 7494b72b08..440e4b7c4c 100644 --- a/libs/web-components/src/assets/css/components.css +++ b/libs/web-components/src/assets/css/components.css @@ -46,27 +46,39 @@ goa-table.sticky thead { goa-table td { font: var(--goa-typography-body-m); - padding: var(--goa-space-xs) var(--goa-space-m) var(--goa-space-xs); - border-bottom: var(--goa-border-width-s) solid var(--goa-color-greyscale-200); + padding: var(--goa-table-padding-data, var(--goa-space-xs) var(--goa-space-m) var(--goa-space-xs)); + background-color: var(--goa-table-color-bg-data, var(--goa-color-greyscale-white)); + border-bottom: var(--goa-table-data-border, 1px solid var(--goa-color-greyscale-200)); vertical-align: top; box-sizing: border-box; - height: var(--goa-space-2xl); + min-height: var(--goa-table-height-data, var(--goa-space-2xl)); } +/* V2: Remove bottom border from last row to prevent doubling with table border */ +goa-table[version="2"] tbody tr:last-child td { + border-bottom: none; +} + +/* V2: Zebra striping for readability */ +goa-table[version="2"][striped="true"] tbody tr:nth-child(even) td { + background-color: var(--goa-color-greyscale-50); +} + +/* Relaxed variant - larger spacing and height for data cells only */ goa-table[variant="relaxed"] td { - padding: var(--goa-space-m); + padding: var(--goa-table-padding-data-relaxed, var(--goa-space-m)); + min-height: var(--goa-table-height-data-relaxed, 64px); } goa-table thead th { - background-color: var(--goa-color-greyscale-white); - color: var(--goa-table-color-heading); - padding: - var(--goa-space-s) - var(--goa-table-header-padding, var(--goa-space-m)) - var(--goa-space-xs) var(--goa-table-header-padding, var(--goa-space-m)); + background-color: var(--goa-table-color-bg-heading, var(--goa-color-greyscale-white)); + color: var(--goa-table-color-heading, var(--goa-color-greyscale-600)); + font: var(--goa-table-typography-heading, var(--goa-typography-heading-s)); + padding: var(--goa-table-padding-heading, var(--goa-space-s) var(--goa-space-m) var(--goa-space-xs)); text-align: left; - border-bottom: var(--goa-border-width-m) solid var(--goa-table-color-border-heading); + border-bottom: var(--goa-table-heading-border, 2px solid var(--goa-color-greyscale-600)); vertical-align: bottom; + min-height: var(--goa-table-height-heading, 56px); } .goa-table-number-column { @@ -78,24 +90,147 @@ goa-table thead th { text-align: right; } -.goa-table-number-header:not(:has(goa-table-sort-header)) { - padding-bottom: var(--goa-space-xs); -} - goa-table thead th:has(goa-table-sort-header) { - padding-bottom: 1px; - padding-top: 0; + padding: 0; } goa-table thead th:has(goa-table-sort-header):hover { - background-color: var(--goa-color-greyscale-100); - color: var(--goa-color-interactive-hover); + background-color: var(--goa-table-color-bg-heading-hover, var(--goa-color-greyscale-100)); + color: var(--goa-table-color-heading-hover, var(--goa-color-interactive-hover)); } goa-table tfoot td { background-color: var(--goa-color-greyscale-100); } +/* V2: Outer edge padding - larger padding on first and last columns */ + +/* Headers without TableSortHeader - apply padding to th */ +goa-table[version="2"] thead th:first-child:not(:has(goa-table-sort-header)) { + padding-left: var(--goa-space-l, 24px); +} + +goa-table[version="2"] thead th:last-child:not(:has(goa-table-sort-header)) { + padding-right: var(--goa-space-l, 24px); +} + +/* Headers with TableSortHeader - override token padding for button inside (all variants) */ +goa-table[version="2"] thead th:first-child:has(goa-table-sort-header):not(:last-child) { + --goa-table-padding-heading: 18px var(--goa-space-m) var(--goa-space-m) var(--goa-space-l); +} + +goa-table[version="2"] thead th:last-child:has(goa-table-sort-header):not(:first-child) { + --goa-table-padding-heading: 18px var(--goa-space-l) var(--goa-space-m) var(--goa-space-m); +} + +goa-table[version="2"] thead th:first-child:last-child:has(goa-table-sort-header) { + --goa-table-padding-heading: 18px var(--goa-space-l) var(--goa-space-m) var(--goa-space-l); +} + +/* Data and footer cells */ +goa-table[version="2"] tbody td:first-child, +goa-table[version="2"] tfoot td:first-child { + padding-left: var(--goa-space-l, 24px); +} + +goa-table[version="2"] tbody td:last-child, +goa-table[version="2"] tfoot td:last-child { + padding-right: var(--goa-space-l, 24px); +} + + +/* Normal Variant Cell Types */ + +goa-table td.goa-table-cell--text { + padding-top: var(--goa-table-padding-cell-text, var(--goa-space-s)) !important; + padding-bottom: var(--goa-table-padding-cell-text, var(--goa-space-s)) !important; +} + +goa-table td.goa-table-cell--checkbox { + padding-top: var(--goa-table-padding-cell-checkbox, 2px) !important; + padding-bottom: var(--goa-table-padding-cell-checkbox, 2px) !important; + text-align: center; +} + +goa-table td.goa-table-cell--form-field { + padding-top: var(--goa-table-padding-cell-form-field, 3px) !important; + padding-bottom: var(--goa-table-padding-cell-form-field, 3px) !important; +} + +goa-table td.goa-table-cell--badge { + padding-top: var(--goa-table-padding-cell-badge, var(--goa-space-s)) !important; + padding-bottom: var(--goa-table-padding-cell-badge, var(--goa-space-s)) !important; +} + +/* Alignment helper classes */ + +goa-table td.goa-table-cell--numeric, +goa-table th.goa-table-cell--numeric { + text-align: right; + font-variant-numeric: tabular-nums; +} + +goa-table th.goa-table-cell--numeric goa-table-sort-header { + text-align: right; + justify-content: flex-end; +} + +goa-table td.goa-table-cell--numeric { + padding-top: var(--goa-table-padding-cell-text, var(--goa-space-s)) !important; + padding-bottom: var(--goa-table-padding-cell-text, var(--goa-space-s)) !important; +} + +goa-table td.goa-table-cell--actions { + padding-top: var(--goa-table-padding-cell-actions, 7px) !important; + padding-bottom: var(--goa-table-padding-cell-actions, 7px) !important; + text-align: right; + white-space: nowrap; +} + +goa-table td.goa-table-cell--text-supporting { + padding-top: var(--goa-table-padding-cell-text, var(--goa-space-s)) !important; + padding-bottom: var(--goa-table-padding-cell-text, var(--goa-space-s)) !important; +} + +/* Relaxed variant overrides for alignment/layout classes */ + +goa-table[variant="relaxed"] td.goa-table-cell--numeric { + padding-top: var(--goa-table-padding-cell-text-relaxed, 20px) !important; + padding-bottom: var(--goa-table-padding-cell-text-relaxed, 20px) !important; +} + +goa-table[variant="relaxed"] td.goa-table-cell--actions { + padding-top: var(--goa-table-padding-cell-actions-relaxed, 14px) !important; + padding-bottom: var(--goa-table-padding-cell-actions-relaxed, 14px) !important; +} + +goa-table[variant="relaxed"] td.goa-table-cell--text-supporting { + padding-top: var(--goa-table-padding-cell-text-supporting-relaxed, 8px) !important; + padding-bottom: var(--goa-table-padding-cell-text-supporting-relaxed, 8px) !important; +} + +/* Relaxed Variant Cell Types */ + +goa-table[variant="relaxed"] td.goa-table-cell--text { + padding-top: var(--goa-table-padding-cell-text-relaxed, 20px) !important; + padding-bottom: var(--goa-table-padding-cell-text-relaxed, 20px) !important; +} + +goa-table[variant="relaxed"] td.goa-table-cell--checkbox { + padding-top: var(--goa-table-padding-cell-checkbox-relaxed, 10px) !important; + padding-bottom: var(--goa-table-padding-cell-checkbox-relaxed, 10px) !important; +} + +goa-table[variant="relaxed"] td.goa-table-cell--form-field { + padding-top: var(--goa-table-padding-cell-form-field-relaxed, 11px) !important; + padding-bottom: var(--goa-table-padding-cell-form-field-relaxed, 11px) !important; +} + +goa-table[variant="relaxed"] td.goa-table-cell--badge { + padding-top: var(--goa-table-padding-cell-badge-relaxed, 20px) !important; + padding-bottom: var(--goa-table-padding-cell-badge-relaxed, 20px) !important; +} + goa-table tfoot tr:first-child td { border-top: var(--goa-border-width-m) solid var(--goa-color-greyscale-200); } diff --git a/libs/web-components/src/components/table/Table.svelte b/libs/web-components/src/components/table/Table.svelte index 879a8517dd..f20de3ce54 100644 --- a/libs/web-components/src/components/table/Table.svelte +++ b/libs/web-components/src/components/table/Table.svelte @@ -1,7 +1,9 @@ @@ -20,11 +22,16 @@ ); type Variant = (typeof Variants)[number]; + const [Version, validateVersion] = typeValidator("Version", ["1", "2"]); + type VersionType = (typeof Version)[number]; + // Public export let width: string = ""; export let stickyheader: string = "false"; + export let striped: string = "false"; export let variant: Variant = "normal"; + export let version: VersionType = "1"; export let testid: string = ""; export let mt: Spacing = null; @@ -40,11 +47,13 @@ // Reactive $: _stickyHeader = toBoolean(stickyheader); + $: _striped = toBoolean(striped); // Hooks onMount(() => { validateVariant(variant); + validateVersion(version); // without setTimeout it won't properly sort in Safari setTimeout(attachSortEventHandling, 0); @@ -122,7 +131,9 @@
diff --git a/libs/web-components/src/components/table/TableSortHeader.svelte b/libs/web-components/src/components/table/TableSortHeader.svelte index 85291a2e11..2b57ecc6c8 100644 --- a/libs/web-components/src/components/table/TableSortHeader.svelte +++ b/libs/web-components/src/components/table/TableSortHeader.svelte @@ -14,10 +14,8 @@ if (_rootEl) { // Add styling if an ancestor has a class to style number columns, const hostEl = _rootEl.getRootNode().host; - const parentThead = hostEl?.closest("th"); - parentThead?.style.setProperty("--goa-table-header-padding", "0"); - const ancestor = hostEl?.closest("th.goa-table-number-header"); + const ancestor = hostEl?.closest("th.goa-table-number-header, th.goa-table-cell--numeric"); if (ancestor) { _rootEl.style.setProperty("--header-text-align", "flex-end"); _rootEl.style.setProperty("--header-align", "right"); @@ -57,18 +55,18 @@ line-height: inherit; height: inherit; width: 100%; - padding: var(--goa-space-s) var(--goa-space-m) var(--goa-space-xs); + padding: var(--goa-table-padding-heading, var(--goa-space-s) var(--goa-space-m) var(--goa-space-xs)); justify-content: var(--header-text-align, flex-start); - gap: var(--goa-space-2xs); + gap: var(--goa-table-sort-header-gap, var(--goa-space-2xs)); align-items: flex-end; text-align: var(--header-align, left); } /* User set classes */ button:hover { - background-color: var(--goa-color-greyscale-100); + background-color: var(--goa-table-color-bg-heading-hover, var(--goa-color-greyscale-100)); cursor: pointer; - color: var(--goa-color-interactive-hover); + color: var(--goa-table-color-heading-hover, var(--goa-color-interactive-hover)); } button goa-icon { From 31c4749743d722b0b4e8d79ad01e5a4a8570f0f2 Mon Sep 17 00:00:00 2001 From: Thomas Jeffery Date: Mon, 20 Oct 2025 23:39:52 -0600 Subject: [PATCH 26/80] feat(#3126): update modal to v2 --- .../src/components/modal/Modal.svelte | 155 +++++++++++++++--- 1 file changed, 135 insertions(+), 20 deletions(-) diff --git a/libs/web-components/src/components/modal/Modal.svelte b/libs/web-components/src/components/modal/Modal.svelte index 06319bba5a..323d7b6b71 100644 --- a/libs/web-components/src/components/modal/Modal.svelte +++ b/libs/web-components/src/components/modal/Modal.svelte @@ -12,6 +12,7 @@ type CalloutVariant = (typeof CALLOUT_VARIANT)[number]; type Transition = (typeof Transitions)[number]; + type VersionType = (typeof Version)[number]; // ****** // Public @@ -24,6 +25,7 @@ export let calloutvariant: CalloutVariant | null = null; export let maxwidth: string = "60ch"; export let testid: string = "modal"; + export let version: VersionType = "1"; // @deprecated: use maxwidth export let width: string = ""; @@ -55,6 +57,8 @@ "none", ]); + const [Version, validateVersion] = typeValidator("Version", ["1", "2"]); + // ******** // Reactive // ******** @@ -101,6 +105,7 @@ onMount(() => { validateCalloutVariant(calloutvariant); validateTransition(transition); + validateVersion(version); // event listeners window.addEventListener("keydown", onInputKeyDown); @@ -212,13 +217,14 @@ in:fly={{ duration: _transitionTime, y: 200 }} out:fly={{ delay: _transitionTime, duration: _transitionTime, y: -100 }} class="modal-pane" + class:v2={version === "2"} tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="goa-modal-heading" data-first-focus="true" > - {#if calloutvariant !== null} + {#if calloutvariant !== null && version !== "2"}
-