diff --git a/libs/angular-components/src/lib/components/form-item/form-item.spec.ts b/libs/angular-components/src/lib/components/form-item/form-item.spec.ts index a58f43dee..fe2dc283d 100644 --- a/libs/angular-components/src/lib/components/form-item/form-item.spec.ts +++ b/libs/angular-components/src/lib/components/form-item/form-item.spec.ts @@ -66,6 +66,7 @@ describe("GoABFormItem", () => { component.mb = "l"; component.ml = "xl"; component.mr = "m"; + fixture.detectChanges(); tick(); fixture.detectChanges(); })); diff --git a/libs/angular-components/src/lib/components/menu-button/menu-button.spec.ts b/libs/angular-components/src/lib/components/menu-button/menu-button.spec.ts new file mode 100644 index 000000000..7a0970283 --- /dev/null +++ b/libs/angular-components/src/lib/components/menu-button/menu-button.spec.ts @@ -0,0 +1,91 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { GoabMenuButton } from "./menu-button"; +import { GoabMenuAction } from "./menu-action"; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { GoabButtonType, GoabIconType } from "@abgov/ui-components-common"; +import { By } from "@angular/platform-browser"; +import { fireEvent } from "@testing-library/dom"; + +@Component({ + template: ` + + + + + + + ` +}) +class TestMenuButtonComponent { + text?: string; + type?: GoabButtonType; + leadingIcon?: GoabIconType; + testId?: string; + + onAction(event: unknown) { + /* do nothing */ + } +} + +describe("GoabMenuButton", () => { + let fixture: ComponentFixture; + let component: TestMenuButtonComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [GoabMenuButton, GoabMenuAction], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + declarations: [TestMenuButtonComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(TestMenuButtonComponent); + component = fixture.componentInstance; + component.text = "Menu actions"; + component.type = "primary"; + component.leadingIcon = "alarm"; + component.testId = "test-menu-button"; + fixture.detectChanges(); + }); + + it("should render the properties", () => { + const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement; + expect(menuButtonElement.getAttribute("text")).toBe("Menu actions"); + expect(menuButtonElement.getAttribute("type")).toBe("primary"); + expect(menuButtonElement.getAttribute("leading-icon")).toBe("alarm"); + expect(menuButtonElement.getAttribute("testid")).toBe("test-menu-button"); + }); + + it("should render with leading icon", () => { + const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement; + expect(menuButtonElement.getAttribute("leading-icon")).toBe("alarm"); + }); + + it("should render without leading icon when not provided", () => { + component.leadingIcon = undefined; + fixture.detectChanges(); + + const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement; + expect(menuButtonElement.getAttribute("leading-icon")).toBeNull(); + }); + + it("should respond to action event", () => { + const onAction = jest.spyOn(component, "onAction"); + const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement; + + const mockDetail = { action: "action1", text: "Action 1" }; + fireEvent(menuButtonElement, new CustomEvent("_action", { detail: mockDetail })); + + expect(onAction).toHaveBeenCalledWith(mockDetail); + }); +}); diff --git a/libs/angular-components/src/lib/components/menu-button/menu-button.ts b/libs/angular-components/src/lib/components/menu-button/menu-button.ts index 043403301..c7461ce7e 100644 --- a/libs/angular-components/src/lib/components/menu-button/menu-button.ts +++ b/libs/angular-components/src/lib/components/menu-button/menu-button.ts @@ -1,5 +1,15 @@ -import { GoabButtonType, GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common"; -import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from "@angular/core"; +import { + GoabButtonType, + GoabIconType, + GoabMenuButtonOnActionDetail, +} from "@abgov/ui-components-common"; +import { + CUSTOM_ELEMENTS_SCHEMA, + Component, + Input, + Output, + EventEmitter, +} from "@angular/core"; @Component({ standalone: true, @@ -9,6 +19,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from " @@ -20,6 +31,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from " export class GoabMenuButton { @Input() text?: string; @Input() type?: GoabButtonType; + @Input() leadingIcon?: GoabIconType; @Input() testId?: string; @Output() onAction = new EventEmitter(); diff --git a/libs/react-components/specs/menu-button.browser.spec.tsx b/libs/react-components/specs/menu-button.browser.spec.tsx index 60bd94cfc..62746d77b 100644 --- a/libs/react-components/specs/menu-button.browser.spec.tsx +++ b/libs/react-components/specs/menu-button.browser.spec.tsx @@ -123,4 +123,26 @@ describe("MenuButton", () => { expect(onAction.mock.calls[0][0].action).toBe("first"); expect(onAction.mock.calls[1][0].action).toBe("second"); }); + + + it("should render with leadingIcon", async () => { + const onAction = vi.fn(); + + const Component = () => { + return ( + + + + + ); + }; + + const result = render(); + + // Verify leading icon on button + await vi.waitFor(async () => { + const leadingIcon = result.getByTestId("icon-calendar"); + expect(leadingIcon).toBeDefined(); + }); + }); }); diff --git a/libs/react-components/src/lib/menu-button/menu-button.tsx b/libs/react-components/src/lib/menu-button/menu-button.tsx index 50a853b54..6538de8e2 100644 --- a/libs/react-components/src/lib/menu-button/menu-button.tsx +++ b/libs/react-components/src/lib/menu-button/menu-button.tsx @@ -6,7 +6,7 @@ * It also includes TypeScript interfaces for improved type checking and development experience. */ -import { GoabButtonType, GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common"; +import { GoabButtonType, GoabIconType, GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common"; import { ReactNode, type JSX, useRef, useEffect } from "react"; /** @@ -15,12 +15,14 @@ import { ReactNode, type JSX, useRef, useEffect } from "react"; * * @property {string} text - The text label to be displayed on the button. * @property {GoabButtonType} type - The button type, e.g., "primary", "secondary", etc. + * @property {GoaIconType} leadingIcon - Optional leading icon appearing within the button. * @property {string} [testid] - A test identifier for automated testing purposes. * @property {React.RefObject} ref - A reference object pointing to the Web Component's DOM element. */ interface WCProps { text: string; type: GoabButtonType; + "leading-icon"?: GoabIconType; testid?: string; ref: React.RefObject; } @@ -45,6 +47,7 @@ declare module "react" { * * @property {string} text - The text label to display on the button. * @property {GoabButtonType} [type="primary"] - The button type, e.g., "primary", "secondary". Defaults to "primary". + * @property {GoaIconType} leadingIcon - Optional leading icon appearing within the button. * @property {string} [testId] - A test identifier for automated testing purposes. * @property {Function} [onAction] - Callback function invoked when an action event is emitted by the component. * @property {ReactNode} [children] - Optional child elements to be rendered inside the button. @@ -52,6 +55,7 @@ declare module "react" { export interface GoabMenuButtonProps { text: string; type?: GoabButtonType; + leadingIcon?: GoabIconType; testId?: string; onAction?: (detail: GoabMenuButtonOnActionDetail) => void; children?: ReactNode; @@ -83,6 +87,7 @@ export interface GoabMenuButtonProps { export function GoabMenuButton({ text, type = "primary", + leadingIcon, testId, onAction, children, @@ -111,7 +116,7 @@ export function GoabMenuButton({ }, [el, onAction]); return ( - + {children} ); diff --git a/libs/web-components/src/components/menu-button/MenuAction.svelte b/libs/web-components/src/components/menu-button/MenuAction.svelte index 7f5939daa..119ac14df 100644 --- a/libs/web-components/src/components/menu-button/MenuAction.svelte +++ b/libs/web-components/src/components/menu-button/MenuAction.svelte @@ -55,6 +55,7 @@ padding: 0 var(--goa-button-padding-lr); gap: var(--goa-button-gap); align-items: center; /* for leading and trailing icon vertical alignment */ + white-space: nowrap; width: 100%; background: none; diff --git a/libs/web-components/src/components/menu-button/MenuButton.spec.ts b/libs/web-components/src/components/menu-button/MenuButton.spec.ts index 933f64ffe..6c990b585 100644 --- a/libs/web-components/src/components/menu-button/MenuButton.spec.ts +++ b/libs/web-components/src/components/menu-button/MenuButton.spec.ts @@ -13,7 +13,7 @@ describe("GoAMenuButton", () => { it("should render with testid", async () => { const { container } = render(GoAMenuButton, { text: "Menu Button", - testid: "menu-button-test" + testid: "menu-button-test", }); expect(container.innerHTML).toContain('data-testid="menu-button-test"'); @@ -24,7 +24,7 @@ describe("GoAMenuButton", () => { it(`should render ${type} type`, async () => { const { container } = render(GoAMenuButton, { text: "Menu Button", - type: type as "primary" | "secondary" | "tertiary" + type: type as "primary" | "secondary" | "tertiary", }); expect(container.innerHTML).toContain(`type="${type}"`); @@ -88,4 +88,27 @@ describe("GoAMenuButton", () => { expect(container.innerHTML).toContain('slot="target"'); }); }); + + describe("leading icon", () => { + it("should render without leadingIcon by default", async () => { + const { container } = render(GoAMenuButton, { + text: "Menu Button", + }); + + const button = container.querySelector("goa-button"); + expect(button).toBeTruthy(); + expect(button?.getAttribute("leadingicon")).toBeNull(); + }); + + it("should render with leadingIcon when provided", async () => { + const { container } = render(GoAMenuButton, { + text: "Menu Button", + leadingIcon: "add", + }); + + const button = container.querySelector("goa-button"); + expect(button).toBeTruthy(); + expect(button?.getAttribute("leadingicon")).toBe("add"); + }); + }); }); diff --git a/libs/web-components/src/components/menu-button/MenuButton.svelte b/libs/web-components/src/components/menu-button/MenuButton.svelte index 4ac51e800..93d4e7984 100644 --- a/libs/web-components/src/components/menu-button/MenuButton.svelte +++ b/libs/web-components/src/components/menu-button/MenuButton.svelte @@ -1,6 +1,8 @@ - @@ -15,6 +17,7 @@ export let text: string; export let type: "primary" | "secondary" | "tertiary" = "primary"; export let testid: string = ""; + export let leadingIcon: GoAIconType | undefined = undefined; // Private props @@ -26,6 +29,8 @@ let _buttonIndex = 0; let _targetEl: HTMLElement; + + // width of the menu button which is the min-width of the popover menu let _menuWidth: number = 0; // Reactive @@ -131,12 +136,14 @@ on:_open={open} padded="false" tabindex="-1" + maxwidth="none" prevent-scroll-into-view={true} > diff --git a/libs/web-components/src/components/popover/Popover.svelte b/libs/web-components/src/components/popover/Popover.svelte index 48698aea2..08777da86 100644 --- a/libs/web-components/src/components/popover/Popover.svelte +++ b/libs/web-components/src/components/popover/Popover.svelte @@ -34,7 +34,7 @@ export let testid: string = "popover"; export let position: "above" | "below" | "auto" = "auto"; - export let maxwidth: string = "320px"; + export let maxwidth: string | "none" = "320px"; export let minwidth: string = ""; export let width: string = ""; export let height: "full" | "wrap-content" = "wrap-content"; @@ -360,7 +360,8 @@ targetRect.left > popoverRect.width; if (rightAligned) { - _popoverEl.style.left = `${targetRect.x - (popoverRect.width - targetRect.width)}px`; + _popoverEl.style.right = "0"; + _popoverEl.style.left = ""; } } @@ -395,7 +396,12 @@ -
+
@@ -427,7 +433,6 @@ display: inline; align-items: center; height: 100%; - position: relative; } .popover-target {