Skip to content

Commit

Permalink
feat(Card): improve the accessibility of the component
Browse files Browse the repository at this point in the history
  • Loading branch information
sarkaaa committed Feb 5, 2025
1 parent de47c89 commit 7a934f5
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 52 deletions.
53 changes: 46 additions & 7 deletions packages/orbit-components/src/Card/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type CardPropsAndCustomArgs = React.ComponentProps<typeof Card> & {
sectionTitle: string;
sectionDescription: string;
initialExpanded?: boolean;
onClick: () => void;
buttonClick: () => void;
};

const meta: Meta<CardPropsAndCustomArgs> = {
Expand Down Expand Up @@ -96,7 +98,7 @@ export const CardWithActions: Story = {
<Card
{...args}
actions={
<ButtonLink compact size="small">
<ButtonLink onClick={action("onClick")} compact size="small">
Button
</ButtonLink>
}
Expand Down Expand Up @@ -263,7 +265,7 @@ export const CardWithDefaultExpanded: Story = {
initialExpanded={initialExpanded}
onExpand={action("onExpand")}
actions={
<ButtonLink compact type="secondary" size="small">
<ButtonLink onClick={action("onClose")} compact type="secondary" size="small">
Close
</ButtonLink>
}
Expand Down Expand Up @@ -294,11 +296,11 @@ export const CardWithDefaultExpanded: Story = {
};

export const CardWithMixedSections: Story = {
render: ({ sectionTitle, sectionDescription, ...args }) => (
render: ({ sectionTitle, sectionDescription, onClick, buttonClick, ...args }) => (
<Card
{...args}
actions={
<ButtonLink compact size="small">
<ButtonLink onClick={buttonClick} compact size="small">
Button
</ButtonLink>
}
Expand All @@ -307,7 +309,7 @@ export const CardWithMixedSections: Story = {
expandable
title={sectionTitle}
actions={
<ButtonLink compact size="small" type="secondary">
<ButtonLink onClick={buttonClick} compact size="small" type="secondary">
Button
</ButtonLink>
}
Expand All @@ -321,16 +323,53 @@ export const CardWithMixedSections: Story = {
<CardSection title={sectionTitle} description={sectionDescription}>
Section Content
</CardSection>
<CardSection title={sectionTitle} actions={<ButtonLink>Button</ButtonLink>} />
<CardSection
title={sectionTitle}
actions={<ButtonLink onClick={buttonClick}>Button</ButtonLink>}
/>
<CardSection onClick={onClick} title={sectionTitle} description={sectionDescription}>
Section Content with onClick
</CardSection>
<CardSection
expandable
onClick={onClick}
actions={
<ButtonLink onClick={buttonClick} compact size="small">
Button
</ButtonLink>
}
title={sectionTitle}
description={sectionDescription}
>
Expandable section Content with onClick and actions
</CardSection>
<CardSection
onClick={onClick}
actions={
<ButtonLink onClick={buttonClick} compact size="small">
Button
</ButtonLink>
}
title={sectionTitle}
description={sectionDescription}
>
Non-expandable section Content with onClick and actions
</CardSection>
</Card>
),

args: {
onClick: action("onClick"),
buttonClick: action("buttonClick"),
},

parameters: {
controls: {
exclude: ["labelClose", "initialExpanded", "expanded"],
exclude: ["labelClose", "initialExpanded", "expanded", "onClick", "buttonClick"],
},
},
};

export const LoadingCard: Story = {
render: args => (
<Card {...args}>
Expand Down
132 changes: 88 additions & 44 deletions packages/orbit-components/src/Card/CardSection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,37 @@ import { ELEMENT_OPTIONS } from "../../Heading/consts";
import type { Props } from "./types";
import Header from "../components/Header";
import Expandable from "./components/Expandable";
import Stack from "../../Stack";
import handleKeyDown from "../../utils/handleKeyDown";

const ContentWrapper = ({
onClick,
className,
children,
}: Pick<Props, "onClick" | "children"> & { className?: string }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
role={onClick ? "button" : undefined}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={onClick ? 0 : undefined}
onKeyDown={onClick ? handleKeyDown(onClick) : undefined}
onClick={onClick}
className={cx(
"orbit-card-content-wrapper flex-1 focus:outline-none",
onClick && "before:rounded-100 before:absolute before:inset-0",
className,
)}
>
{children}
</div>
);

const Actions = ({ actions }) => (
<Stack inline grow={false} justify="end">
{actions}
</Stack>
);

export default function CardSection({
title,
titleAs = ELEMENT_OPTIONS.DIV,
Expand All @@ -36,6 +65,8 @@ export default function CardSection({
}, [isControlled, expanded]);

function handleClick() {
onClick?.();

if (!isControlled) {
setOpened(state => !state);
}
Expand All @@ -51,77 +82,90 @@ export default function CardSection({

return (
// Needs to capture bubbled click events from the <button> below
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className={cx(
"duration-fast lm:border-x border-b transition-all ease-in-out",
"duration-fast lm:border-x relative border-b transition-all ease-in-out",
opened && "my-200 rounded-100 shadow-level2 [&+*]:border-t",
onClick != null && "hover:bg-white-normal-hover cursor-pointer",
onClick && !expandable && "hover:bg-white-normal-hover",
onClick &&
"has-[.orbit-card-content-wrapper:focus]:focus-within:rounded-100 has-[.orbit-card-content-wrapper:focus]:focus-within:outline-blue-normal has-[.orbit-card-content-wrapper:focus]:focus-within:z-10 has-[.orbit-card-content-wrapper:focus]:focus-within:outline has-[.orbit-card-content-wrapper:focus]:focus-within:outline-2 has-[.orbit-card-content-wrapper:focus]:focus-within:transition-none",
)}
data-test={dataTest}
role={onClick == null ? undefined : "button"}
// See comment above
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={onClick == null ? undefined : 0}
onClick={onClick}
// Not needed once we can use <button> or <a> like we should
onKeyDown={onClick == null ? undefined : handleKeyDown(onClick)}
>
{(title != null || header != null) && expandable && (
<button
type="button"
className="p-400 lm:p-600 hover:bg-white-normal-hover w-full"
aria-expanded={opened}
aria-controls={slideID}
onClick={handleClick}
<div
className={cx(
"hover:bg-white-normal-hover p-400 lm:p-600 gap-300 flex cursor-pointer items-center",
"has-[.orbit-card-header-button:focus]:focus-within:rounded-100 has-[.orbit-card-header-button:focus]:focus-within:outline-blue-normal has-[.orbit-card-header-button:focus]:focus-within:relative has-[.orbit-card-header-button:focus]:focus-within:z-20 has-[.orbit-card-header-button:focus]:focus-within:outline has-[.orbit-card-header-button:focus]:focus-within:outline-2",
)}
>
<Header
title={title}
titleAs={titleAs}
description={description}
expandable={expandable}
header={header}
expanded={opened}
actions={actions}
isSection
/>
</button>
<button
type="button"
className={cx(
"orbit-card-header-button w-full focus:outline-none",
"before:absolute before:inset-0",
)}
aria-expanded={opened}
aria-controls={slideID}
onClick={handleClick}
>
<Header
title={title}
titleAs={titleAs}
description={description}
expandable={expandable}
header={header}
expanded={opened}
actions={Boolean(actions)}
isSection
/>
</button>
{actions && <Actions actions={actions} />}
</div>
)}

{(title != null || header != null) && !expandable && (
<div className="p-400 lm:p-600 w-full">
<Header
title={title}
titleAs={titleAs}
description={description}
expandable={expandable}
header={header}
expanded={opened}
actions={actions}
isSection
/>
<div className="gap-300 p-400 lm:p-600 flex items-center">
<ContentWrapper onClick={onClick}>
<Header
title={title}
titleAs={titleAs}
description={description}
header={header}
expanded={opened}
isSection
/>
</ContentWrapper>
{actions && <Actions actions={actions} />}
</div>
)}

{children && expandable && (
<Expandable expanded={opened} slideID={slideID} labelID={slideID}>
<div className="font-base text-normal text-primary-foreground px-400 lm:px-600 w-full leading-normal">
<div className="py-400 lm:py-600 border-elevation-flat-border-color border-t">
<div
className={cx(
"font-base text-normal text-primary-foreground px-400 lm:px-600 w-full leading-normal",
onClick && "hover:bg-white-normal-hover",
)}
>
<ContentWrapper
onClick={opened ? onClick : undefined}
className={cx("py-400 lm:py-600 border-elevation-flat-border-color border-t")}
>
{children}
</div>
</ContentWrapper>
</div>
</Expandable>
)}

{children && !expandable && (
<div
<ContentWrapper
className={cx(
"font-base text-normal text-primary-foreground px-400 lm:px-600 pb-400 lm:pb-600 w-full leading-normal",
title == null && header == null && "pt-400 lm:pt-600",
)}
>
{children}
</div>
</ContentWrapper>
)}
</div>
);
Expand Down
1 change: 0 additions & 1 deletion packages/orbit-components/src/Card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ import Card, { CardSection } from "@kiwicom/orbit-components/lib/Card";
| initialExpanded | `boolean` | `false` | Initial state of expandable CardSection when it mounts in uncontrolled variant. Can only be used if `expandable` is `true`. |
| noSeparator | `Boolean` | | Optional prop to turn off Separator between `header` and `children` |
| onClick | `event => void \| Promise` | | Function for handling the onClick event. |
| |
| onClose | `() => void \| Promise` | | Callback that is triggered when section is closing |
| onExpand | `() => void \| Promise` | | Callback that is triggered when section is expanding |
| title | `React.Node` | | The title of the CardSection |
Expand Down

0 comments on commit 7a934f5

Please sign in to comment.