diff --git a/.changeset/metal-crabs-develop.md b/.changeset/metal-crabs-develop.md new file mode 100644 index 0000000000..6b982efbea --- /dev/null +++ b/.changeset/metal-crabs-develop.md @@ -0,0 +1,7 @@ +--- +"@justeattakeaway/pie-card": patch +"pie-storybook": patch +"pie-docs": patch +--- + +[Fixed] - Ensure the card is not interactive when `disabled` is passed diff --git a/apps/pie-docs/src/components/card/code/props.json b/apps/pie-docs/src/components/card/code/props.json index 381afe9538..d1ad14c15c 100644 --- a/apps/pie-docs/src/components/card/code/props.json +++ b/apps/pie-docs/src/components/card/code/props.json @@ -58,7 +58,7 @@ "type": "code", "item": ["true", "false"] }, - "Whether or not the card should be disabled. This applies disabled styles and turns off interactivity.", + "Whether or not the card should be disabled. This applies disabled styles and turns off interactivity.
If the card is used as a link, the `href` attribute will be removed so the link can no longer be navigated.", { "type": "code", "item": ["false"] diff --git a/apps/pie-storybook/stories/pie-card.stories.ts b/apps/pie-storybook/stories/pie-card.stories.ts index 4c2213c4f2..aa5436b501 100644 --- a/apps/pie-storybook/stories/pie-card.stories.ts +++ b/apps/pie-storybook/stories/pie-card.stories.ts @@ -2,6 +2,7 @@ import { nothing } from 'lit'; import { html } from 'lit/static-html.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { type Meta } from '@storybook/web-components'; +import { action } from '@storybook/addon-actions'; import '@justeattakeaway/pie-card'; import { @@ -112,6 +113,8 @@ const cardStoryMeta: CardStoryMeta = { export default cardStoryMeta; +const clickAction = action('clicked'); + const Template: TemplateFunction = ({ tag, href, @@ -123,7 +126,10 @@ const Template: TemplateFunction = ({ variant, padding, isDraggable, -}) => html` +}) => { + const isButton = tag === 'button'; + + return html` = ({ ?disabled="${disabled}" .aria="${aria}" padding="${padding || nothing}" - ?isDraggable="${isDraggable}"> + ?isDraggable="${isDraggable}" + @click="${isButton ? clickAction : nothing}"> ${sanitizeAndRenderHTML(slot)} `; +}; const createCardStory = createStory(Template, defaultArgs); diff --git a/packages/components/pie-card/src/card.scss b/packages/components/pie-card/src/card.scss index 56cd3b6804..409d091bda 100644 --- a/packages/components/pie-card/src/card.scss +++ b/packages/components/pie-card/src/card.scss @@ -39,10 +39,6 @@ outline: none; text-decoration: none; - &:focus-visible { - @include p.focus; - } - &.c-card--disabled { --card-bg-color: var(--dt-color-disabled-01); @@ -95,4 +91,8 @@ &.c-card--draggable { @extend %has-grab-cursor; } + + &:focus-visible { + @include p.focus; + } } diff --git a/packages/components/pie-card/src/index.ts b/packages/components/pie-card/src/index.ts index c03517e94e..99fa387745 100644 --- a/packages/components/pie-card/src/index.ts +++ b/packages/components/pie-card/src/index.ts @@ -56,6 +56,14 @@ export class PieCard extends LitElement implements CardProps { @queryAssignedElements({ flatten: true }) private assignedElements?: HTMLElement[]; + private onClickHandler (event: Event) { + if (this.disabled) { + // needed to intercept/prevent click events when the card is disabled. + event.preventDefault(); + event.stopPropagation(); + } + } + /** * Renders the card as an anchor element. * @@ -63,21 +71,22 @@ export class PieCard extends LitElement implements CardProps { */ private renderAnchor (classes: ClassInfo): TemplateResult { const paddingCSS = this.generatePaddingCSS(); + const { + href, rel, target, disabled, aria, + } = this; return html` - - + `; } @@ -196,10 +205,11 @@ export class PieCard extends LitElement implements CardProps { class="${classMap(classes)}" data-test-id="pie-card" role="button" - tabindex="0" + tabindex=${disabled ? '-1' : '0'} aria-label=${aria?.label || nothing} aria-disabled=${disabled ? 'true' : 'false'} - style=${paddingCSS || ''}> + style=${paddingCSS || ''} + @click=${this.onClickHandler}> `; diff --git a/packages/components/pie-card/test/component/pie-card.spec.ts b/packages/components/pie-card/test/component/pie-card.spec.ts index 498e4663a9..6f214fcdf0 100644 --- a/packages/components/pie-card/test/component/pie-card.spec.ts +++ b/packages/components/pie-card/test/component/pie-card.spec.ts @@ -355,5 +355,114 @@ test.describe('PieCard - Component tests', () => { await expect(image).not.toHaveCSS('opacity', '0.5'); }); }); + + test.describe('when the prop `tag` is set to `button`', () => { + test('should set `aria-disabled` to `true` when disabled', async ({ mount, page }) => { + // Arrange + await mount(PieCard, { + props: { + tag: 'button', + disabled: true, + } as CardProps, + slots: { + default: slotContent, + }, + }); + + // Act + const component = page.locator(componentSelector); + + // Assert + await expect(component).toHaveAttribute('aria-disabled', 'true'); + }); + + test('should set `tabindex` to `-1` when disabled', async ({ mount, page }) => { + // Arrange + await mount(PieCard, { + props: { + tag: 'button', + disabled: true, + } as CardProps, + slots: { + default: slotContent, + }, + }); + + // Act + const component = page.locator(componentSelector); + + // Assert + await expect(component).toHaveAttribute('tabindex', '-1'); + }); + + test('should not trigger the click event when the tag prop is set to `button` and is `disabled`', async ({ mount, page }) => { + // Arrange + const messages: string[] = []; + + await mount(PieCard, { + props: { + tag: 'button', + disabled: true, + } as CardProps, + slots: { + default: slotContent, + }, + on: { + click: () => messages.push('1'), + }, + }); + + // Act + const component = page.locator(componentSelector); + await page.evaluate(() => { + const card = document.querySelector('pie-card'); + card?.shadowRoot?.querySelector('div')?.click(); + }); + + // Assert + await expect(component).toBeDisabled(); + expect(messages).toHaveLength(0); + }); + }); + + test.describe('when the prop `tag` is set to `a`', () => { + test('should set `aria-disabled` to `true` when disabled', async ({ mount, page }) => { + // Arrange + await mount(PieCard, { + props: { + tag: 'a', + disabled: true, + } as CardProps, + slots: { + default: slotContent, + }, + }); + + // Act + const component = page.locator(componentSelector); + + // Assert + await expect(component).toHaveAttribute('aria-disabled', 'true'); + }); + + test('should not set the href attribute when disabled', async ({ mount, page }) => { + // Arrange + await mount(PieCard, { + props: { + tag: 'a', + disabled: true, + } as CardProps, + slots: { + default: slotContent, + }, + }); + + // Act + const component = page.locator(componentSelector); + + // Assert + await expect(component).not.toHaveAttribute('href'); + }); + }); }); });