diff --git a/packages/eui/changelogs/upcoming/8907.md b/packages/eui/changelogs/upcoming/8907.md new file mode 100644 index 00000000000..0274c14d418 --- /dev/null +++ b/packages/eui/changelogs/upcoming/8907.md @@ -0,0 +1 @@ +- Enhanced `EuiCheckableCard` to make non-interactive children clickable for card selection diff --git a/packages/eui/src/components/card/checkable_card/__snapshots__/checkable_card.test.tsx.snap b/packages/eui/src/components/card/checkable_card/__snapshots__/checkable_card.test.tsx.snap index 55547d8d909..a6ad896bd48 100644 --- a/packages/eui/src/components/card/checkable_card/__snapshots__/checkable_card.test.tsx.snap +++ b/packages/eui/src/components/card/checkable_card/__snapshots__/checkable_card.test.tsx.snap @@ -120,6 +120,7 @@ exports[`EuiCheckableCard renders children 1`] = `
Child
diff --git a/packages/eui/src/components/card/checkable_card/checkable_card.spec.tsx b/packages/eui/src/components/card/checkable_card/checkable_card.spec.tsx index 81d2b27de55..0d7ffdfe86e 100644 --- a/packages/eui/src/components/card/checkable_card/checkable_card.spec.tsx +++ b/packages/eui/src/components/card/checkable_card/checkable_card.spec.tsx @@ -10,7 +10,7 @@ /// /// -import React, { FunctionComponent, useState } from 'react'; +import { FunctionComponent, useState } from 'react'; import { EuiCheckableCard, type EuiCheckableCardProps } from '../index'; @@ -34,12 +34,40 @@ describe('EuiCheckableCard', () => { }; describe('Click behavior', () => { - it('fired onChange only once when the checkbox is clicked', () => { + it('fires onChange only once when the checkbox is clicked', () => { cy.realMount(); cy.get('[data-test-subj=checkableCard]').realClick(); cy.get('[data-test-subj=checkableCard]').should('be.checked'); }); + + it('fires onChange when clicking on non-interactive children', () => { + cy.realMount( + +
+ Non-interactive text content +
+
+ ); + + cy.get('[data-test-subj=non-interactive-content]').realClick(); + + cy.get('[data-test-subj=checkableCard]').should('be.checked'); + }); + + it('does not fire onChange when clicking on interactive children', () => { + cy.realMount( + + + + ); + + cy.get('[data-test-subj=interactive-button]').realClick(); + + cy.get('[data-test-subj=checkableCard]').should('not.be.checked'); + }); }); }); diff --git a/packages/eui/src/components/card/checkable_card/checkable_card.stories.tsx b/packages/eui/src/components/card/checkable_card/checkable_card.stories.tsx index 6b791ef3e9e..64d2cf6a19f 100644 --- a/packages/eui/src/components/card/checkable_card/checkable_card.stories.tsx +++ b/packages/eui/src/components/card/checkable_card/checkable_card.stories.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { EuiCheckableCard, EuiCheckableCardProps } from './checkable_card'; @@ -48,3 +49,63 @@ export const Playground: Story = { label: 'Checkable option', }, }; + +export const WithNonInteractiveChildren: Story = { + args: { + id: 'checkable-card-non-interactive', + label: 'Service Plan', + children: ( +
+

Basic plan includes:

+
    +
  • Up to 5 users
  • +
  • 10GB storage
  • +
  • Email support
  • +
+

Perfect for small teams getting started.

+
+ ), + }, +}; + +export const WithInteractiveChildren: Story = { + args: { + id: 'checkable-card-interactive', + label: 'Advanced Configuration', + children: ( +
+

Customize your settings:

+ +

+ { + e.preventDefault(); + e.stopPropagation(); + alert('Link clicked!'); + }} + > + Learn more about advanced features + +

+
+ ), + }, +}; diff --git a/packages/eui/src/components/card/checkable_card/checkable_card.tsx b/packages/eui/src/components/card/checkable_card/checkable_card.tsx index da3e8e77800..ff3c3b98627 100644 --- a/packages/eui/src/components/card/checkable_card/checkable_card.tsx +++ b/packages/eui/src/components/card/checkable_card/checkable_card.tsx @@ -6,8 +6,15 @@ * Side Public License, v 1. */ -import React, { FunctionComponent, ReactNode, useRef } from 'react'; +import React, { + FunctionComponent, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; import classNames from 'classnames'; +import { tabbable } from 'tabbable'; import { EuiRadio, @@ -76,8 +83,19 @@ export const EuiCheckableCard: FunctionComponent = ({ const { id } = rest; const labelEl = useRef(null); const inputEl = useRef(null); + const childrenWrapperEl = useRef(null); + const classes = classNames('euiCheckableCard', className); + const [hasInteractiveChildren, setHasInteractiveChildren] = useState(false); + + useEffect(() => { + const interactiveElements = childrenWrapperEl.current + ? tabbable(childrenWrapperEl.current) + : []; + setHasInteractiveChildren(interactiveElements.length > 0); + }, [children, childrenWrapperEl]); + let checkableElement; if (checkableType === 'radio') { checkableElement = ( @@ -101,9 +119,13 @@ export const EuiCheckableCard: FunctionComponent = ({ const labelClasses = classNames('euiCheckableCard__label'); const onChangeAffordance = (e: React.MouseEvent) => { - if (labelEl.current && e.target !== inputEl.current) { - labelEl.current.click(); - } + if (!labelEl.current || e.target === inputEl.current) return; + labelEl.current.click(); + }; + + const onChildrenClick = () => { + if (hasInteractiveChildren) return; + labelEl.current?.click(); }; return ( @@ -136,9 +158,15 @@ export const EuiCheckableCard: FunctionComponent = ({ {children && (
{children}