diff --git a/change/@fluentui-react-card-e65cb71c-d483-449c-b1cb-b6b23d07f25b.json b/change/@fluentui-react-card-e65cb71c-d483-449c-b1cb-b6b23d07f25b.json new file mode 100644 index 0000000000000..0a54204a51cf7 --- /dev/null +++ b/change/@fluentui-react-card-e65cb71c-d483-449c-b1cb-b6b23d07f25b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: infer a11y id from immediate header element", + "packageName": "@fluentui/react-card", + "email": "marcosvmmoura@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-card/src/components/Card/Card.cy.tsx b/packages/react-components/react-card/src/components/Card/Card.cy.tsx index c664a14329213..99bc6667ad560 100644 --- a/packages/react-components/react-card/src/components/Card/Card.cy.tsx +++ b/packages/react-components/react-card/src/components/Card/Card.cy.tsx @@ -60,6 +60,29 @@ const CardSample = (props: CardProps) => ( ); +const CardWithCustomHeader = ({ + customHeaderId = 'custom-header-id', + ...props +}: CardProps & { customHeaderId: string }) => ( + <> +

+ Before +

+ + + } + header={App Name} + description={Developer} + /> + + +

+ After +

+ +); + const CardWithPreview = (props: CardProps) => ( <>

@@ -423,6 +446,17 @@ describe('Card', () => { }); }); + it('should sync selectable aria-labelledby with card header immediate child', () => { + const customHeaderId = 'custom-header'; + + mountFluent(); + + cy.get(`.${cardHeaderClassNames.header}`).should('not.have.attr', 'id'); + cy.get(`.${cardClassNames.checkbox}`).then(slot => { + cy.get(`#${customHeaderId}`).then(() => expect(customHeaderId).equals(slot.attr('aria-labelledby'))); + }); + }); + it('should sync selectable aria-label with card preview alt', () => { mountFluent(); diff --git a/packages/react-components/react-card/src/components/CardHeader/useCardHeader.ts b/packages/react-components/react-card/src/components/CardHeader/useCardHeader.ts index 3f1f487923d6c..b74a0634fe39c 100644 --- a/packages/react-components/react-card/src/components/CardHeader/useCardHeader.ts +++ b/packages/react-components/react-card/src/components/CardHeader/useCardHeader.ts @@ -4,6 +4,44 @@ import type { CardHeaderProps, CardHeaderState } from './CardHeader.types'; import { useCardContext_unstable } from '../Card/CardContext'; import { cardHeaderClassNames } from './useCardHeaderStyles.styles'; +/** + * Finds the first child of CardHeader with an id prop. + * + * @param header - the header prop of CardHeader + */ +function getChildWithId(header: CardHeaderProps['header']) { + function isReactElementWithIdProp(element: React.ReactNode): element is React.ReactElement { + return React.isValidElement(element) && Boolean(element.props.id); + } + + return React.Children.toArray(header).find(isReactElementWithIdProp); +} + +/** + * Returns the id to use for the CardHeader root element. + * + * @param headerId - the id prop of the CardHeader component + * @param childWithId - the first child of the CardHeader component with an id prop + * @param generatedId - a generated id + * + * @returns the id to use for the CardHeader root element + */ +function getReferenceId( + headerId: string | undefined, + childWithId: React.ReactElement | undefined, + generatedId: string, +): string { + if (headerId) { + return headerId; + } + + if (childWithId?.props.id) { + return childWithId.props.id; + } + + return generatedId; +} + /** * Create the state required to render CardHeader. * @@ -21,15 +59,17 @@ export const useCardHeader_unstable = (props: CardHeaderProps, ref: React.Ref(null); + const hasChildId = React.useRef(false); const generatedId = useId(cardHeaderClassNames.header, referenceId); React.useEffect(() => { - if (header && headerRef.current) { - const { id } = headerRef.current; + const headerId = !hasChildId.current ? headerRef.current?.id : undefined; + const childWithId = getChildWithId(header); + + hasChildId.current = Boolean(childWithId); - setReferenceId(id ? id : generatedId); - } - }, [header, setReferenceId, generatedId]); + setReferenceId(getReferenceId(headerId, childWithId, generatedId)); + }, [generatedId, header, setReferenceId]); return { components: { @@ -49,7 +89,7 @@ export const useCardHeader_unstable = (props: CardHeaderProps, ref: React.Ref