Skip to content

Commit b64e226

Browse files
authored
feat(atomic): add button function for lit components (#4857)
Add Lit equivalent to `<Button>` functional element. Also replaced `button.tsx` to `stencil-button.tsx` which is responsible for all the file changes ## Usage ### With Stencil ```tsx return ( <Button style="primary" text={i18n.t('load-more-results')} part="load-more-results-button" class="my-2 p-3 font-bold" onClick={() => onClick()} ></Button> ); ``` ### With Lit ```ts return button({ props: { style: 'primary', text: i18n.t('load-more-results'), part: 'load-more-results-button', class: 'my-2 p-3 font-bold', onClick: () => onClick(), }, }); ``` ## Question Lit does not natively support spreading attributes as we do in .tsx files, which can lead to code repetition. To address this, we could use [lit-helpers](https://open-wc.org/docs/development/lit-helpers/#spread-directives) to avoid duplicating attributes, properties, and events while maintaining consistency in our patterns. WDYT
1 parent c353e4d commit b64e226

File tree

64 files changed

+373
-75
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+373
-75
lines changed

packages/atomic/src/components/commerce/atomic-commerce-load-more-products/atomic-commerce-load-more-products.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import {
1414
InitializeBindings,
1515
} from '../../../utils/initialization-utils';
1616
import {createAppLoadedListener} from '../../common/interface/store';
17-
import {LoadMoreButton} from '../../common/load-more/button';
1817
import {LoadMoreContainer} from '../../common/load-more/container';
1918
import {LoadMoreGuard} from '../../common/load-more/guard';
2019
import {LoadMoreProgressBar} from '../../common/load-more/progress-bar';
20+
import {LoadMoreButton} from '../../common/load-more/stencil-button';
2121
import {LoadMoreSummary} from '../../common/load-more/summary';
2222
import {CommerceBindings} from '../atomic-commerce-interface/atomic-commerce-interface';
2323

packages/atomic/src/components/commerce/facets/facet-number-input/atomic-commerce-facet-number-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {isUndefined} from '@coveo/bueno';
22
import {NumericFacet} from '@coveo/headless/commerce';
33
import {Component, h, Prop, Event, EventEmitter, State} from '@stencil/core';
4-
import {Button} from '../../../common/button';
4+
import {Button} from '../../../common/stencil-button';
55
import {CommerceBindings as Bindings} from '../../atomic-commerce-interface/atomic-commerce-interface';
66

77
export type Range = {start: number; end: number};

packages/atomic/src/components/commerce/product-template-components/atomic-product-children/atomic-product-children.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
InitializeBindings,
2020
} from '../../../../utils/initialization-utils';
2121
import {filterProtocol} from '../../../../utils/xss-utils';
22-
import {Button} from '../../../common/button';
22+
import {Button} from '../../../common/stencil-button';
2323
import {CommerceBindings} from '../../atomic-commerce-interface/atomic-commerce-interface';
2424
import {ProductContext} from '../product-template-decorators';
2525

packages/atomic/src/components/common/breadbox/breadcrumb-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {FunctionalComponent, h} from '@stencil/core';
22
import {i18n} from 'i18next';
3-
import {Button} from '../button';
3+
import {Button} from '../stencil-button';
44
import {Breadcrumb} from './breadcrumb-types';
55
import {
66
joinBreadcrumbValues,

packages/atomic/src/components/common/breadbox/breadcrumb-clear-all.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {FunctionalComponent, h} from '@stencil/core';
22
import {i18n} from 'i18next';
3-
import {Button} from '../button';
3+
import {Button} from '../stencil-button';
44

55
export interface BreadcrumbClearAllProps {
66
setRef: (el: HTMLButtonElement) => void;

packages/atomic/src/components/common/breadbox/breadcrumb-show-less.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {h, FunctionalComponent} from '@stencil/core';
22
import {i18n} from 'i18next';
3-
import {Button} from '../button';
3+
import {Button} from '../stencil-button';
44

55
export interface BreadcrumbShowLessProps {
66
onShowLess: () => void;

packages/atomic/src/components/common/breadbox/breadcrumb-show-more.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {h, FunctionalComponent} from '@stencil/core';
22
import {i18n} from 'i18next';
3-
import {Button} from '../button';
3+
import {Button} from '../stencil-button';
44

55
export interface BreadcrumbShowMoreProps {
66
setRef: (el: HTMLButtonElement) => void;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import {createRipple} from '@/src/utils/ripple';
2+
import {fireEvent, within} from '@storybook/test';
3+
import {html, render} from 'lit';
4+
import {vi} from 'vitest';
5+
import {button, ButtonProps} from './button';
6+
7+
vi.mock('@/src/utils/ripple', () => ({
8+
createRipple: vi.fn(),
9+
}));
10+
11+
describe('button', () => {
12+
let container: HTMLElement;
13+
14+
beforeEach(() => {
15+
container = document.createElement('div');
16+
document.body.appendChild(container);
17+
});
18+
19+
afterEach(() => {
20+
document.body.removeChild(container);
21+
});
22+
23+
const renderButton = (props: Partial<ButtonProps>): HTMLButtonElement => {
24+
render(
25+
html`${button({
26+
props: {
27+
...props,
28+
style: props.style ?? 'primary',
29+
},
30+
children: html``,
31+
})}`,
32+
container
33+
);
34+
return within(container).getByRole('button') as HTMLButtonElement;
35+
};
36+
37+
it('should render a button in the document', () => {
38+
const props = {};
39+
const button = renderButton(props);
40+
expect(button).toBeInTheDocument();
41+
});
42+
43+
it('should render a button with the correct style', () => {
44+
const props: Partial<ButtonProps> = {
45+
style: 'outline-error',
46+
};
47+
48+
const button = renderButton(props);
49+
50+
expect(button).toHaveClass('btn-outline-error');
51+
});
52+
53+
it('should render a button with the correct text', () => {
54+
const props = {
55+
text: 'Click me',
56+
};
57+
58+
const button = renderButton(props);
59+
60+
expect(button.querySelector('span')?.textContent).toBe('Click me');
61+
});
62+
63+
it('should wrap the button text with a truncate class', () => {
64+
const props = {
65+
text: 'Click me',
66+
};
67+
68+
const button = renderButton(props);
69+
70+
expect(button.querySelector('span')).toHaveClass('truncate');
71+
});
72+
73+
it('should handle click event', async () => {
74+
const handleClick = vi.fn();
75+
const props = {
76+
onClick: handleClick,
77+
};
78+
79+
const button = renderButton(props);
80+
81+
await fireEvent.click(button);
82+
83+
expect(handleClick).toHaveBeenCalled();
84+
});
85+
86+
it('should apply disabled attribute', () => {
87+
const props = {
88+
disabled: true,
89+
};
90+
91+
const button = renderButton(props);
92+
93+
expect(button.hasAttribute('disabled')).toBe(true);
94+
});
95+
96+
it('should apply aria attributes', () => {
97+
const props: Partial<ButtonProps> = {
98+
ariaLabel: 'button',
99+
ariaPressed: 'true',
100+
};
101+
102+
const button = renderButton(props);
103+
104+
expect(button.getAttribute('aria-label')).toBe('button');
105+
expect(button.getAttribute('aria-pressed')).toBe('true');
106+
});
107+
108+
it('should apply custom class', () => {
109+
const props = {
110+
class: 'custom-class',
111+
};
112+
113+
const button = renderButton(props);
114+
115+
expect(button).toHaveClass('custom-class');
116+
expect(button).toHaveClass('btn-primary');
117+
});
118+
119+
it('should apply part attribute', () => {
120+
const props = {
121+
part: 'button-part',
122+
};
123+
124+
const button = renderButton(props);
125+
126+
expect(button.getAttribute('part')).toBe('button-part');
127+
});
128+
129+
it('should apply title attribute', () => {
130+
const props = {
131+
title: 'Button Title',
132+
};
133+
134+
const button = renderButton(props);
135+
136+
expect(button.getAttribute('title')).toBe('Button Title');
137+
});
138+
139+
it('should apply tabindex attribute', () => {
140+
const props = {
141+
tabIndex: 1,
142+
};
143+
144+
const button = renderButton(props);
145+
146+
expect(button.getAttribute('tabindex')).toBe('1');
147+
});
148+
149+
it('should apply role attribute', () => {
150+
const props: Partial<ButtonProps> = {
151+
role: 'button',
152+
};
153+
154+
const button = renderButton(props);
155+
156+
expect(button.getAttribute('role')).toBe('button');
157+
});
158+
159+
it('should call onMouseDown when the mousedown event is fired on the button', async () => {
160+
const props: Partial<ButtonProps> = {};
161+
const button = renderButton(props);
162+
await fireEvent.mouseDown(button);
163+
expect(createRipple).toHaveBeenCalled();
164+
});
165+
166+
it('should apply form attribute', () => {
167+
const props = {
168+
form: 'form-id',
169+
};
170+
171+
const button = renderButton(props);
172+
173+
expect(button.getAttribute('form')).toBe('form-id');
174+
});
175+
176+
it('should apply type attribute', () => {
177+
const props: Partial<ButtonProps> = {
178+
type: 'submit',
179+
};
180+
181+
const button = renderButton(props);
182+
183+
expect(button.getAttribute('type')).toBe('submit');
184+
});
185+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {html} from 'lit-html';
2+
import {ifDefined} from 'lit-html/directives/if-defined.js';
3+
import {createRipple} from '../../utils/ripple';
4+
import {
5+
getRippleColorForButtonStyle,
6+
getClassNameForButtonStyle,
7+
ButtonStyle,
8+
} from './button-style';
9+
10+
export interface ButtonProps {
11+
style: ButtonStyle;
12+
onClick?(event?: MouseEvent): void;
13+
class?: string;
14+
text?: string;
15+
part?: string;
16+
type?: 'button' | 'submit' | 'reset' | 'menu';
17+
form?: string;
18+
role?:
19+
| 'status'
20+
| 'application'
21+
| 'checkbox'
22+
| 'button'
23+
| 'dialog'
24+
| 'img'
25+
| 'radiogroup'
26+
| 'toolbar'
27+
| 'listitem'
28+
| 'list'
29+
| 'separator';
30+
disabled?: boolean;
31+
ariaLabel?: string;
32+
ariaExpanded?: 'true' | 'false';
33+
ariaPressed?: 'true' | 'false' | 'mixed';
34+
ariaChecked?: 'true' | 'false' | 'mixed';
35+
ariaCurrent?: 'page' | 'false';
36+
ariaControls?: string;
37+
ariaHidden?: 'true' | 'false';
38+
tabIndex?: number;
39+
title?: string;
40+
}
41+
42+
export const button = <T>({
43+
props,
44+
children,
45+
}: {
46+
props: ButtonProps;
47+
children: T;
48+
}) => {
49+
const rippleColor = getRippleColorForButtonStyle(props.style);
50+
const className = getClassNameForButtonStyle(props.style);
51+
52+
return html` <button
53+
type=${ifDefined(props.type)}
54+
title=${ifDefined(props.title)}
55+
tabindex=${ifDefined(props.tabIndex)}
56+
role=${ifDefined(props.role)}
57+
part=${ifDefined(props.part)}
58+
form=${ifDefined(props.form)}
59+
class=${props.class ? `${className} ${props.class}` : className}
60+
aria-pressed=${ifDefined(props.ariaPressed)}
61+
aria-label=${ifDefined(props.ariaLabel)}
62+
aria-hidden=${ifDefined(props.ariaHidden)}
63+
aria-expanded=${ifDefined(props.ariaExpanded)}
64+
aria-current=${ifDefined(props.ariaCurrent)}
65+
aria-controls=${ifDefined(props.ariaControls)}
66+
aria-checked=${ifDefined(props.ariaChecked)}
67+
@mousedown=${(e: MouseEvent) => createRipple(e, {color: rippleColor})}
68+
@click=${props.onClick}
69+
?disabled=${props.disabled}
70+
>
71+
{props.text ? <span class="truncate">${props.text}</span> : null}
72+
${children}
73+
</button>`;
74+
};

packages/atomic/src/components/common/carousel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {h, FunctionalComponent, Fragment} from '@stencil/core';
22
import {JSXBase} from '@stencil/core/internal';
33
import ArrowRight from '../../images/arrow-right.svg';
4-
import {Button} from './button';
54
import {AnyBindings} from './interface/bindings';
5+
import {Button} from './stencil-button';
66

77
export interface CarouselProps {
88
bindings: AnyBindings;

packages/atomic/src/components/common/expandable-text/expandable-text.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {FunctionalComponent, h} from '@stencil/core';
22
import MinusIcon from '../../../images/minus.svg';
33
import PlusIcon from '../../../images/plus.svg';
4-
import {Button} from '../button';
4+
import {Button} from '../stencil-button';
55

66
export type TruncateAfter = 'none' | '1' | '2' | '3' | '4';
77

packages/atomic/src/components/common/facets/category-facet/all-categories-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {FunctionalComponent, h} from '@stencil/core';
22
import {i18n} from 'i18next';
33
import LeftArrow from '../../../../images/arrow-left-rounded.svg';
4-
import {Button} from '../../button';
4+
import {Button} from '../../stencil-button';
55

66
interface CategoryFacetAllCategoryButtonProps {
77
i18n: i18n;

packages/atomic/src/components/common/facets/category-facet/parent-button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {FunctionalComponent, h} from '@stencil/core';
22
import {i18n} from 'i18next';
33
import LeftArrow from '../../../../images/arrow-left-rounded.svg';
44
import {getFieldValueCaption} from '../../../../utils/field-utils';
5-
import {Button} from '../../button';
5+
import {Button} from '../../stencil-button';
66

77
interface CategoryFacetParentButtonProps {
88
i18n: i18n;

packages/atomic/src/components/common/facets/category-facet/search-value.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {FunctionalComponent, h} from '@stencil/core';
22
import {i18n} from 'i18next';
33
import {getFieldValueCaption} from '../../../../utils/field-utils';
4-
import {Button} from '../../button';
4+
import {Button} from '../../stencil-button';
55
import {FacetValueLabelHighlight} from '../facet-value-label-highlight/facet-value-label-highlight';
66

77
interface CategoryFacetSearchValueProps {

packages/atomic/src/components/common/facets/facet-date-input/facet-date-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
} from '@coveo/headless';
66
import {Component, h, State, Prop, Event, EventEmitter} from '@stencil/core';
77
import {parseDate} from '../../../../utils/date-utils';
8-
import {Button} from '../../../common/button';
8+
import {Button} from '../../../common/stencil-button';
99
import {AnyBindings} from '../../interface/bindings';
1010

1111
/**

packages/atomic/src/components/common/facets/facet-header/facet-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {i18n} from 'i18next';
33
import ArrowBottomIcon from '../../../../images/arrow-bottom-rounded.svg';
44
import ArrowTopIcon from '../../../../images/arrow-top-rounded.svg';
55
import CloseIcon from '../../../../images/close.svg';
6-
import {Button} from '../../button';
76
import {Heading} from '../../heading';
7+
import {Button} from '../../stencil-button';
88

99
export interface FacetHeaderProps {
1010
i18n: i18n;

packages/atomic/src/components/common/facets/facet-number-input/facet-number-input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Component, h, State, Prop, Event, EventEmitter} from '@stencil/core';
2-
import {Button} from '../../button';
32
import {AnyBindings} from '../../interface/bindings';
3+
import {Button} from '../../stencil-button';
44
import {NumericFilter, NumericFilterState} from '../../types';
55
import {NumberInputType} from './number-input-type';
66

0 commit comments

Comments
 (0)