Skip to content

Commit

Permalink
[core] feat(Tabs): allow panel prop to be a renderer (#6621)
Browse files Browse the repository at this point in the history
Co-authored-by: Adi Dahiya <adahiya@palantir.com>
Co-authored-by: Adi Dahiya <adi.dahiya14@gmail.com>
  • Loading branch information
3 people authored Feb 9, 2024
1 parent 8768a30 commit c9941b7
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 9 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/components/hotkeys/hotkeysTarget2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import * as React from "react";

import * as Errors from "../../common/errors";
import { isNodeEnv } from "../../common/utils";
import { isFunction, isNodeEnv } from "../../common/utils";
import { type HotkeyConfig, useHotkeys, type UseHotkeysOptions } from "../../hooks";

/** Identical to the return type of `useHotkeys` hook. */
Expand Down Expand Up @@ -56,7 +56,7 @@ export const HotkeysTarget2 = ({ children, hotkeys, options }: HotkeysTarget2Pro
}
}, [hotkeys, children]);

if (typeof children === "function") {
if (isFunction(children)) {
return children({ handleKeyDown, handleKeyUp });
} else {
return children;
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/components/tabs/tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { IconName } from "@blueprintjs/icons";

import { AbstractPureComponent, Classes } from "../../common";
import { DISPLAYNAME_PREFIX, type HTMLDivProps, type MaybeElement, type Props } from "../../common/props";
import { isFunction } from "../../common/utils";
import type { TagProps } from "../tag/tag";

export type TabId = string | number;
Expand Down Expand Up @@ -48,8 +49,9 @@ export interface TabProps extends Props, Omit<HTMLDivProps, "id" | "title" | "on
/**
* Panel content, rendered by the parent `Tabs` when this tab is active.
* If omitted, no panel will be rendered for this tab.
* Can either be an element or a renderer.
*/
panel?: React.JSX.Element;
panel?: React.JSX.Element | ((props: { tabTitleId: string; tabPanelId: string }) => React.JSX.Element);

/**
* Space-delimited string of class names applied to tab panel container.
Expand Down Expand Up @@ -96,7 +98,7 @@ export class Tab extends AbstractPureComponent<TabProps> {
const { className, panel } = this.props;
return (
<div className={classNames(Classes.TAB_PANEL, className)} role="tablist">
{panel}
{isFunction(panel) ? panel({ tabTitleId: "", tabPanelId: "" }) : panel}
</div>
);
}
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -329,16 +329,20 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> {
if (panel === undefined) {
return undefined;
}

const tabTitleId = generateTabTitleId(this.props.id, id);
const tabPanelId = generateTabPanelId(this.props.id, id);

return (
<div
aria-labelledby={generateTabTitleId(this.props.id, id)}
aria-labelledby={tabTitleId}
aria-hidden={id !== this.state.selectedTabId}
className={classNames(Classes.TAB_PANEL, className, panelClassName)}
id={generateTabPanelId(this.props.id, id)}
id={tabPanelId}
key={id}
role="tabpanel"
>
{panel}
{Utils.isFunction(panel) ? panel({ tabTitleId, tabPanelId }) : panel}
</div>
);
};
Expand Down
20 changes: 18 additions & 2 deletions packages/core/test/tabs/tabsTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { spy } from "sinon";
import { Classes } from "../../src/common";
import { Tab } from "../../src/components/tabs/tab";
import { Tabs, type TabsProps, type TabsState } from "../../src/components/tabs/tabs";
import { generateTabPanelId, generateTabTitleId } from "../../src/components/tabs/tabTitle";

describe("<Tabs>", () => {
const ID = "tabsTests";
Expand Down Expand Up @@ -128,6 +129,21 @@ describe("<Tabs>", () => {
assert.lengthOf(wrapper.find(`.${panelClassName}`), 1);
});

it("passes correct tabTitleId and tabPanelId to panel renderer", () => {
mount(
<Tabs id={ID}>
<Tab
id="first"
panel={({ tabTitleId, tabPanelId }) => {
assert.equal(tabTitleId, generateTabTitleId(ID, "first"));
assert.equal(tabPanelId, generateTabPanelId(ID, "first"));
return <Panel title="a" />;
}}
/>
</Tabs>,
);
});

it("renderActiveTabPanelOnly only renders active tab panel", () => {
const wrapper = mount(
<Tabs id={ID} renderActiveTabPanelOnly={true}>
Expand All @@ -140,15 +156,15 @@ describe("<Tabs>", () => {
}
});

it("sets aria-* attributes with matching Ds", () => {
it("sets aria-* attributes with matching IDs", () => {
const wrapper = mount(<Tabs id={ID}>{getTabsContents()}</Tabs>);
wrapper.find(TAB).forEach(title => {
// title "controls" tab element
const titleControls = title.prop("aria-controls");
const tab = wrapper.find(`#${titleControls}`);
// tab element "labelled by" title element
assert.isTrue(tab.is(TAB_PANEL), "aria-controls isn't TAB_PANEL");
assert.deepEqual(tab.prop("aria-labelledby"), title.prop("id"), "mismatched Ds");
assert.deepEqual(tab.prop("aria-labelledby"), title.prop("id"), "mismatched IDs");
});
});

Expand Down

1 comment on commit c9941b7

@adidahiya
Copy link
Contributor

Choose a reason for hiding this comment

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

[core] feat(Tabs): allow panel prop to be a renderer (#6621)

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Please sign in to comment.