Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] feat(Section): a11y improvements, titleRenderer prop #6506

Merged
55 changes: 33 additions & 22 deletions packages/core/src/components/section/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ChevronDown, ChevronUp, type IconName } from "@blueprintjs/icons";

import { Classes, Elevation, Utils } from "../../common";
import { DISPLAYNAME_PREFIX, type HTMLDivProps, type MaybeElement, type Props } from "../../common/props";
import { uniqueId } from "../../common/utils";
import { Card } from "../card/card";
import { Collapse, type CollapseProps } from "../collapse/collapse";
import { H6 } from "../html/html";
Expand Down Expand Up @@ -110,6 +111,15 @@ export interface SectionProps extends Props, Omit<HTMLDivProps, "title">, React.
* Note that the header will only be rendered if `title` is provided.
*/
title?: JSX.Element | string;

/**
* Optional title renderer function. If provided, it is recommended to include a Blueprint `<H6>` element
* as part of the title. The render function is supplied with `className` and `id` attributes which you must
* forward to the DOM.
*
* @default H6
*/
titleRenderer?: React.FC<React.HTMLAttributes<HTMLElement>>;
}

/**
Expand All @@ -129,6 +139,7 @@ export const Section: React.FC<SectionProps> = React.forwardRef((props, ref) =>
rightElement,
subtitle,
title,
titleRenderer = H6,
...htmlProps
} = props;
// Determine whether to use controlled or uncontrolled state.
Expand All @@ -147,9 +158,11 @@ export const Section: React.FC<SectionProps> = React.forwardRef((props, ref) =>
}
}, [collapseProps, isCollapsed, isControlled]);

const isHeaderLeftContainerVisible = title != null || icon != null || subtitle != null;
const isHeaderRightContainerVisible = rightElement != null || collapsible;

const sectionId = uniqueId("section");
const sectionTitleId = title ? uniqueId("section-title") : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const sectionTitleId = title ? uniqueId("section-title") : undefined;
const sectionTitleId = title != null ? uniqueId("section-title") : undefined;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 174 has logic

 {title && (
                <div

so we would want this logic to be the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could change that to title != null too though. Depends whether you want empty string to create the title element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, good point, let's keep what you have


return (
<Card
className={classNames(className, Classes.SECTION, {
Expand All @@ -158,38 +171,36 @@ export const Section: React.FC<SectionProps> = React.forwardRef((props, ref) =>
})}
elevation={elevation}
ref={ref}
aria-labelledby={sectionTitleId}
{...htmlProps}
id={sectionId}
>
{title && (
<div
role={collapsible ? "button" : undefined}
aria-pressed={collapsible ? isCollapsed : undefined}
aria-expanded={collapsible ? isCollapsed : undefined}
aria-controls={collapsible ? sectionId : undefined}
className={classNames(Classes.SECTION_HEADER, {
[Classes.INTERACTIVE]: collapsible,
})}
onClick={collapsible != null ? toggleIsCollapsed : undefined}
bvandercar-vt marked this conversation as resolved.
Show resolved Hide resolved
onClick={collapsible ? toggleIsCollapsed : undefined}
>
{isHeaderLeftContainerVisible && (
bvandercar-vt marked this conversation as resolved.
Show resolved Hide resolved
<>
<div className={Classes.SECTION_HEADER_LEFT}>
{icon && (
<Icon icon={icon} aria-hidden={true} tabIndex={-1} className={Classes.TEXT_MUTED} />
)}

<div>
<H6 className={Classes.SECTION_HEADER_TITLE}>{title}</H6>
{subtitle && (
<div
className={classNames(Classes.TEXT_MUTED, Classes.SECTION_HEADER_SUB_TITLE)}
>
{subtitle}
</div>
)}
<div className={Classes.SECTION_HEADER_LEFT}>
{icon && <Icon icon={icon} aria-hidden={true} tabIndex={-1} className={Classes.TEXT_MUTED} />}
<div>
{React.createElement(
titleRenderer,
{ className: Classes.SECTION_HEADER_TITLE, id: sectionTitleId },
title,
)}
{subtitle && (
<div className={classNames(Classes.TEXT_MUTED, Classes.SECTION_HEADER_SUB_TITLE)}>
{subtitle}
</div>
</div>
</>
)}

)}
</div>
</div>
{isHeaderRightContainerVisible && (
<div className={Classes.SECTION_HEADER_RIGHT}>
{rightElement}
Expand Down
9 changes: 8 additions & 1 deletion packages/core/test/section/sectionTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import * as React from "react";

import { IconNames } from "@blueprintjs/icons";

import { Classes, H6, Section, SectionCard } from "../../src";
import { Classes, H5, H6, Section, SectionCard } from "../../src";

describe("<Section>", () => {
let containerElement: HTMLElement | undefined;
Expand Down Expand Up @@ -73,6 +73,13 @@ describe("<Section>", () => {
assert.isTrue(wrapper.find(`.${Classes.SECTION_HEADER_SUB_TITLE}`).hostNodes().exists());
});

it("renders custom title element with titleRenderer", () => {
const wrapper = mount(<Section title="title" titleRenderer={H5} />, {
attachTo: containerElement,
});
assert.isTrue(wrapper.find(H5).exists());
});

describe("uncontrolled collapse mode", () => {
it("collapsible is open when defaultIsOpen={undefined}", () => {
const wrapper = mount(
Expand Down