Skip to content

Commit b282270

Browse files
committed
fix(#3102): add ability to set leading icon for the MenuButton
1 parent 93e6af3 commit b282270

File tree

6 files changed

+165
-8
lines changed

6 files changed

+165
-8
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { GoabMenuButton } from "./menu-button";
3+
import { GoabMenuAction } from "./menu-action";
4+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
5+
import { GoabButtonType, GoabIconType } from "@abgov/ui-components-common";
6+
import { By } from "@angular/platform-browser";
7+
import { fireEvent } from "@testing-library/dom";
8+
9+
@Component({
10+
template: `
11+
<goab-menu-button
12+
[text]="text"
13+
[type]="type"
14+
[leadingIcon]="leadingIcon"
15+
[testId]="testId"
16+
(onAction)="onAction($event)">
17+
<goab-menu-action
18+
text="Action 1"
19+
action="action1"
20+
icon="person-circle">
21+
</goab-menu-action>
22+
<goab-menu-action
23+
text="Action 2"
24+
action="action2"
25+
icon="notifications">
26+
</goab-menu-action>
27+
</goab-menu-button>
28+
`
29+
})
30+
class TestMenuButtonComponent {
31+
text?: string;
32+
type?: GoabButtonType;
33+
leadingIcon?: GoabIconType;
34+
testId?: string;
35+
36+
onAction(event: unknown) {
37+
/* do nothing */
38+
}
39+
}
40+
41+
describe("GoabMenuButton", () => {
42+
let fixture: ComponentFixture<TestMenuButtonComponent>;
43+
let component: TestMenuButtonComponent;
44+
45+
beforeEach(async () => {
46+
await TestBed.configureTestingModule({
47+
imports: [GoabMenuButton, GoabMenuAction],
48+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
49+
declarations: [TestMenuButtonComponent]
50+
}).compileComponents();
51+
52+
fixture = TestBed.createComponent(TestMenuButtonComponent);
53+
component = fixture.componentInstance;
54+
component.text = "Menu actions";
55+
component.type = "primary";
56+
component.leadingIcon = "alarm";
57+
component.testId = "test-menu-button";
58+
fixture.detectChanges();
59+
});
60+
61+
it("should render the properties", () => {
62+
const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement;
63+
expect(menuButtonElement.getAttribute("text")).toBe("Menu actions");
64+
expect(menuButtonElement.getAttribute("type")).toBe("primary");
65+
expect(menuButtonElement.getAttribute("leading-icon")).toBe("alarm");
66+
expect(menuButtonElement.getAttribute("testid")).toBe("test-menu-button");
67+
});
68+
69+
it("should render with leading icon", () => {
70+
const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement;
71+
expect(menuButtonElement.getAttribute("leading-icon")).toBe("alarm");
72+
});
73+
74+
it("should render without leading icon when not provided", () => {
75+
component.leadingIcon = undefined;
76+
fixture.detectChanges();
77+
78+
const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement;
79+
expect(menuButtonElement.getAttribute("leading-icon")).toBeNull();
80+
});
81+
82+
it("should respond to action event", () => {
83+
const onAction = jest.spyOn(component, "onAction");
84+
const menuButtonElement = fixture.debugElement.query(By.css("goa-menu-button")).nativeElement;
85+
86+
const mockDetail = { action: "action1", text: "Action 1" };
87+
fireEvent(menuButtonElement, new CustomEvent("_action", { detail: mockDetail }));
88+
89+
expect(onAction).toHaveBeenCalledWith(mockDetail);
90+
});
91+
});

libs/angular-components/src/lib/components/menu-button/menu-button.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { GoabButtonType, GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common";
2-
import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from "@angular/core";
1+
import {
2+
GoabButtonType,
3+
GoabIconType,
4+
GoabMenuButtonOnActionDetail,
5+
} from "@abgov/ui-components-common";
6+
import {
7+
CUSTOM_ELEMENTS_SCHEMA,
8+
Component,
9+
Input,
10+
Output,
11+
EventEmitter,
12+
} from "@angular/core";
313

414
@Component({
515
standalone: true,
@@ -9,6 +19,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from "
919
<goa-menu-button
1020
[attr.text]="text"
1121
[attr.type]="type"
22+
[attr.leading-icon]="leadingIcon"
1223
[attr.testid]="testId"
1324
(_action)="_onAction($event)"
1425
>
@@ -20,6 +31,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, Component, Input, Output, EventEmitter } from "
2031
export class GoabMenuButton {
2132
@Input() text?: string;
2233
@Input() type?: GoabButtonType;
34+
@Input() leadingIcon?: GoabIconType;
2335
@Input() testId?: string;
2436
@Output() onAction = new EventEmitter<GoabMenuButtonOnActionDetail>();
2537

libs/react-components/specs/menu-button.browser.spec.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,26 @@ describe("MenuButton", () => {
123123
expect(onAction.mock.calls[0][0].action).toBe("first");
124124
expect(onAction.mock.calls[1][0].action).toBe("second");
125125
});
126+
127+
128+
it("should render with leadingIcon", async () => {
129+
const onAction = vi.fn();
130+
131+
const Component = () => {
132+
return (
133+
<GoabMenuButton text="Dual icons" testId="menu-button" leadingIcon="calendar" onAction={onAction}>
134+
<GoabMenuAction text="Add item" action="add" testId="menu-action-add" icon="add" />
135+
<GoabMenuAction text="Delete item" action="delete" testId="menu-action-delete" icon="trash" />
136+
</GoabMenuButton>
137+
);
138+
};
139+
140+
const result = render(<Component />);
141+
142+
// Verify leading icon on button
143+
await vi.waitFor(async () => {
144+
const leadingIcon = result.getByTestId("icon-calendar");
145+
expect(leadingIcon).toBeDefined();
146+
});
147+
});
126148
});

libs/react-components/src/lib/menu-button/menu-button.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* It also includes TypeScript interfaces for improved type checking and development experience.
77
*/
88

9-
import { GoabButtonType, GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common";
9+
import { GoabButtonType, GoabIconType, GoabMenuButtonOnActionDetail } from "@abgov/ui-components-common";
1010
import { ReactNode, type JSX, useRef, useEffect } from "react";
1111

1212
/**
@@ -15,12 +15,14 @@ import { ReactNode, type JSX, useRef, useEffect } from "react";
1515
*
1616
* @property {string} text - The text label to be displayed on the button.
1717
* @property {GoabButtonType} type - The button type, e.g., "primary", "secondary", etc.
18+
* @property {GoaIconType} leadingIcon - Optional leading icon appearing within the button.
1819
* @property {string} [testid] - A test identifier for automated testing purposes.
1920
* @property {React.RefObject<HTMLElement | null>} ref - A reference object pointing to the Web Component's DOM element.
2021
*/
2122
interface WCProps {
2223
text: string;
2324
type: GoabButtonType;
25+
"leading-icon"?: GoabIconType;
2426
testid?: string;
2527
ref: React.RefObject<HTMLElement | null>;
2628
}
@@ -45,13 +47,15 @@ declare module "react" {
4547
*
4648
* @property {string} text - The text label to display on the button.
4749
* @property {GoabButtonType} [type="primary"] - The button type, e.g., "primary", "secondary". Defaults to "primary".
50+
* @property {GoaIconType} leadingIcon - Optional leading icon appearing within the button.
4851
* @property {string} [testId] - A test identifier for automated testing purposes.
4952
* @property {Function} [onAction] - Callback function invoked when an action event is emitted by the component.
5053
* @property {ReactNode} [children] - Optional child elements to be rendered inside the button.
5154
*/
5255
export interface GoabMenuButtonProps {
5356
text: string;
5457
type?: GoabButtonType;
58+
leadingIcon?: GoabIconType;
5559
testId?: string;
5660
onAction?: (detail: GoabMenuButtonOnActionDetail) => void;
5761
children?: ReactNode;
@@ -83,6 +87,7 @@ export interface GoabMenuButtonProps {
8387
export function GoabMenuButton({
8488
text,
8589
type = "primary",
90+
leadingIcon,
8691
testId,
8792
onAction,
8893
children,
@@ -111,7 +116,7 @@ export function GoabMenuButton({
111116
}, [el, onAction]);
112117

113118
return (
114-
<goa-menu-button ref={el} text={text} type={type} testid={testId}>
119+
<goa-menu-button ref={el} text={text} type={type} testid={testId} leading-icon={leadingIcon}>
115120
{children}
116121
</goa-menu-button>
117122
);

libs/web-components/src/components/menu-button/MenuButton.spec.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe("GoAMenuButton", () => {
1313
it("should render with testid", async () => {
1414
const { container } = render(GoAMenuButton, {
1515
text: "Menu Button",
16-
testid: "menu-button-test"
16+
testid: "menu-button-test",
1717
});
1818

1919
expect(container.innerHTML).toContain('data-testid="menu-button-test"');
@@ -24,7 +24,7 @@ describe("GoAMenuButton", () => {
2424
it(`should render ${type} type`, async () => {
2525
const { container } = render(GoAMenuButton, {
2626
text: "Menu Button",
27-
type: type as "primary" | "secondary" | "tertiary"
27+
type: type as "primary" | "secondary" | "tertiary",
2828
});
2929

3030
expect(container.innerHTML).toContain(`type="${type}"`);
@@ -88,4 +88,27 @@ describe("GoAMenuButton", () => {
8888
expect(container.innerHTML).toContain('slot="target"');
8989
});
9090
});
91+
92+
describe("leading icon", () => {
93+
it("should render without leadingIcon by default", async () => {
94+
const { container } = render(GoAMenuButton, {
95+
text: "Menu Button",
96+
});
97+
98+
const button = container.querySelector("goa-button");
99+
expect(button).toBeTruthy();
100+
expect(button?.getAttribute("leadingicon")).toBeNull();
101+
});
102+
103+
it("should render with leadingIcon when provided", async () => {
104+
const { container } = render(GoAMenuButton, {
105+
text: "Menu Button",
106+
leadingIcon: "add",
107+
});
108+
109+
const button = container.querySelector("goa-button");
110+
expect(button).toBeTruthy();
111+
expect(button?.getAttribute("leadingicon")).toBe("add");
112+
});
113+
});
91114
});

libs/web-components/src/components/menu-button/MenuButton.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
<svelte:options
2-
customElement={{
1+
<svelte:options customElement={{
32
tag: "goa-menu-button",
3+
props: {
4+
leadingIcon: { attribute: "leading-icon", type: "String" },
5+
}
46
}}
57
/>
68

@@ -15,6 +17,7 @@
1517
export let text: string;
1618
export let type: "primary" | "secondary" | "tertiary" = "primary";
1719
export let testid: string = "";
20+
export let leadingIcon: GoAIconType | undefined = undefined;
1821
1922
// Private props
2023
@@ -140,6 +143,7 @@
140143
bind:this={_targetEl}
141144
data-testid={testid}
142145
slot="target"
146+
leadingicon={leadingIcon}
143147
{type}
144148
trailingicon={_icon}
145149
>

0 commit comments

Comments
 (0)