Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe("GoABFormItem", () => {
component.mb = "l";
component.ml = "xl";
component.mr = "m";
fixture.detectChanges();
tick();
fixture.detectChanges();
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test failed because after angular 20 upgrade, testbed likes to have everything standalone.

After this change it will pass the test:
image

image

template: `
<goab-menu-button
[text]="text"
[type]="type"
[leadingIcon]="leadingIcon"
[testId]="testId"
(onAction)="onAction($event)">
<goab-menu-action
text="Action 1"
action="action1"
icon="person-circle">
</goab-menu-action>
<goab-menu-action
text="Action 2"
action="action2"
icon="notifications">
</goab-menu-action>
</goab-menu-button>
`
})
class TestMenuButtonComponent {
text?: string;
type?: GoabButtonType;
leadingIcon?: GoabIconType;
testId?: string;

onAction(event: unknown) {
/* do nothing */
}
}

describe("GoabMenuButton", () => {
let fixture: ComponentFixture<TestMenuButtonComponent>;
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);
});
});
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -9,6 +19,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from "
<goa-menu-button
[attr.text]="text"
[attr.type]="type"
[attr.leading-icon]="leadingIcon"
[attr.testid]="testId"
(_action)="_onAction($event)"
>
Expand All @@ -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<GoabMenuButtonOnActionDetail>();

Expand Down
22 changes: 22 additions & 0 deletions libs/react-components/specs/menu-button.browser.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<GoabMenuButton text="Dual icons" testId="menu-button" leadingIcon="calendar" onAction={onAction}>
<GoabMenuAction text="Add item" action="add" testId="menu-action-add" icon="add" />
<GoabMenuAction text="Delete item" action="delete" testId="menu-action-delete" icon="trash" />
</GoabMenuButton>
);
};

const result = render(<Component />);

// Verify leading icon on button
await vi.waitFor(async () => {
const leadingIcon = result.getByTestId("icon-calendar");
expect(leadingIcon).toBeDefined();
});
});
});
9 changes: 7 additions & 2 deletions libs/react-components/src/lib/menu-button/menu-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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<HTMLElement | null>} 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<HTMLElement | null>;
}
Expand All @@ -45,13 +47,15 @@ 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.
*/
export interface GoabMenuButtonProps {
text: string;
type?: GoabButtonType;
leadingIcon?: GoabIconType;
testId?: string;
onAction?: (detail: GoabMenuButtonOnActionDetail) => void;
children?: ReactNode;
Expand Down Expand Up @@ -83,6 +87,7 @@ export interface GoabMenuButtonProps {
export function GoabMenuButton({
text,
type = "primary",
leadingIcon,
testId,
onAction,
children,
Expand Down Expand Up @@ -111,7 +116,7 @@ export function GoabMenuButton({
}, [el, onAction]);

return (
<goa-menu-button ref={el} text={text} type={type} testid={testId}>
<goa-menu-button ref={el} text={text} type={type} testid={testId} leading-icon={leadingIcon}>
{children}
</goa-menu-button>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
padding: 0 var(--goa-button-padding-lr);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, I'm not sure why. If you use a longer menu action then has room to display on the right side, in Angular it then displays it going to the left, but in React, it seems to display it centred if it can.

Angular:
image

React:
image

gap: var(--goa-button-gap);
align-items: center; /* for leading and trailing icon vertical alignment */
white-space: nowrap;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of this, long menu items don't wrap, and the Popover itself doesn't respect the max-width set, so it can go grow forever. This especially leads to issues with mobile, where the menu can literally be off screen if it's large enough.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also because of this, the Popover can expand to infinite width potentially, as there's nothing that can be used to control its width, so it just grows infinitely to fit the content.


width: 100%;
background: none;
Expand Down
27 changes: 25 additions & 2 deletions libs/web-components/src/components/menu-button/MenuButton.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"');
Expand All @@ -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}"`);
Expand Down Expand Up @@ -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");
});
});
});
11 changes: 9 additions & 2 deletions libs/web-components/src/components/menu-button/MenuButton.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<svelte:options
customElement={{
<svelte:options customElement={{
tag: "goa-menu-button",
props: {
leadingIcon: { attribute: "leading-icon", type: "String" },
}
}}
/>

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -131,12 +136,14 @@
on:_open={open}
padded="false"
tabindex="-1"
maxwidth="none"
prevent-scroll-into-view={true}
>
<goa-button
bind:this={_targetEl}
data-testid={testid}
slot="target"
leadingicon={leadingIcon}
{type}
trailingicon={_icon}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The acceptance criteria on the story asks that trailingicon can be changed by the user as well, so it doesn't necessarily have to be the chevron-down, this would instead be itss default.

>
Expand Down
15 changes: 10 additions & 5 deletions libs/web-components/src/components/popover/Popover.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = "";
}
}
</script>
Expand Down Expand Up @@ -395,7 +396,12 @@
<slot name="target" />
</button>

<div style={style("display", _open ? "block" : "none")}>
<div
style={styles(
style("display", _open ? "block" : "none"),
style("position", "relative"),
)}
>
<section
bind:clientHeight={_sectionHeight}
bind:this={_popoverEl}
Expand All @@ -404,7 +410,7 @@
style={styles(
style("width", width),
style("min-width", minwidth),
style("max-width", width ? `max(${width}, ${maxwidth})` : maxwidth),
style("max-width", maxwidth === "none" ? "" : maxwidth),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, Dropdown is impacted. The Popover in Dropdown no longer expands to the width of the Dropdown, but instead has a max width of 320px now. I think this might be because Dropdown passes _popoverMaxWidth to the width attribute of Popover, instead of maxwidth.

This PR:
image

Previously:
image

style("padding", _padded ? "var(--goa-space-m)" : "0"),
)}
>
Expand All @@ -427,7 +433,6 @@
display: inline;
align-items: center;
height: 100%;
position: relative;
}
.popover-target {
Expand Down
Loading