Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: infer a11y id from immediate header element",
"packageName": "@fluentui/react-card",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ const CardSample = (props: CardProps) => (
</>
);

const CardWithCustomHeader = ({
customHeaderId = 'custom-header-id',
...props
}: CardProps & { customHeaderId: string }) => (
<>
<p tabIndex={0} id="before">
Before
</p>

<Card id="card" {...props}>
<CardHeader
image={<img src={resolveAsset('powerpoint_logo.svg')} alt="Microsoft PowerPoint logo" />}
header={<b id={customHeaderId}>App Name</b>}
description={<span>Developer</span>}
/>
</Card>

<p tabIndex={0} id="after">
After
</p>
</>
);

const CardWithPreview = (props: CardProps) => (
<>
<p tabIndex={0} id="before">
Expand Down Expand Up @@ -423,6 +446,17 @@ describe('Card', () => {
});
});

it('should sync selectable aria-labelledby with card header immediate child', () => {
const customHeaderId = 'custom-header';

mountFluent(<CardWithCustomHeader customHeaderId={customHeaderId} selected />);

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(<CardWithPreview selected />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -21,15 +59,17 @@ export const useCardHeader_unstable = (props: CardHeaderProps, ref: React.Ref<HT
} = useCardContext_unstable();
const headerRef = React.useRef<HTMLDivElement>(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: {
Expand All @@ -49,7 +89,7 @@ export const useCardHeader_unstable = (props: CardHeaderProps, ref: React.Ref<HT
required: true,
defaultProps: {
ref: headerRef,
id: referenceId,
id: !hasChildId.current ? referenceId : undefined,
},
}),
description: resolveShorthand(description),
Expand Down