-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
feat: export TabPanel #6896
feat: export TabPanel #6896
Changes from 28 commits
73a0519
4ac5b19
6d456b5
316f4a5
7d1be88
83116ef
0a007a1
45d761a
bcdb95b
c6b3717
087ac23
c2aed65
9adf944
5472c30
1f2d078
fdf8652
5b91852
b773ae3
75a6efc
241696b
150687f
f8794af
110923a
955d4f1
aabcc6b
dc3f729
7881c58
8f202ae
fc9d945
2254a18
3fbc070
9f9c0b6
6b820bd
0bd052b
72bfa73
12a665b
01cb908
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* Copyright 2024 Palantir Technologies, Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import classNames from "classnames"; | ||
import * as React from "react"; | ||
|
||
import { AbstractPureComponent, Classes, Utils } from "../../common"; | ||
|
||
import { type TabProps } from "./tab"; | ||
import type { TabsProps } from "./tabs"; | ||
import { generateTabIds, type TabTitleProps } from "./tabTitle"; | ||
|
||
export interface TabPanelProps | ||
extends Pick<TabProps, "className" | "id" | "panel">, | ||
Pick<TabsProps, "renderActiveTabPanelOnly">, | ||
Pick<TabTitleProps, "parentId"> { | ||
/** | ||
* Used for setting `aria-hidden` prop. | ||
*/ | ||
isHidden: boolean; | ||
} | ||
|
||
/** | ||
* Wraps the passed `panel`. | ||
*/ | ||
export class TabPanel extends AbstractPureComponent<TabPanelProps> { | ||
public render() { | ||
const { className, id, parentId, panel, isHidden, renderActiveTabPanelOnly } = this.props; | ||
|
||
if (panel === undefined || (renderActiveTabPanelOnly && isHidden)) { | ||
return undefined; | ||
} | ||
|
||
const { tabTitleId, tabPanelId } = generateTabIds(parentId, id); | ||
|
||
return ( | ||
<div | ||
aria-labelledby={tabTitleId} | ||
aria-hidden={isHidden} | ||
className={classNames(Classes.TAB_PANEL, className)} | ||
id={tabPanelId} | ||
role="tabpanel" | ||
> | ||
{Utils.isFunction(panel) ? panel({ tabTitleId, tabPanelId }) : panel} | ||
</div> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ import { DISPLAYNAME_PREFIX, removeNonHTMLProps } from "../../common/props"; | |
import { Icon } from "../icon/icon"; | ||
import { Tag } from "../tag/tag"; | ||
|
||
import type { TabId, TabProps } from "./tab"; | ||
import type { TabId, TabIdProps, TabProps } from "./tab"; | ||
|
||
export interface TabTitleProps extends TabProps { | ||
/** Optional contents. */ | ||
|
@@ -55,18 +55,20 @@ export class TabTitle extends AbstractPureComponent<TabTitleProps> { | |
tagProps, | ||
...htmlProps | ||
} = this.props; | ||
|
||
const intent = selected ? Intent.PRIMARY : Intent.NONE; | ||
const { tabPanelId, tabTitleId } = generateTabIds(parentId, id); | ||
|
||
return ( | ||
<div | ||
{...removeNonHTMLProps(htmlProps)} | ||
aria-controls={generateTabPanelId(parentId, id)} | ||
aria-controls={tabPanelId} | ||
aria-disabled={disabled} | ||
aria-expanded={selected} | ||
aria-selected={selected} | ||
className={classNames(Classes.TAB, className)} | ||
data-tab-id={id} | ||
id={generateTabTitleId(parentId, id)} | ||
id={tabTitleId} | ||
onClick={disabled ? undefined : this.handleClick} | ||
role="tab" | ||
tabIndex={disabled ? undefined : selected ? 0 : -1} | ||
|
@@ -91,10 +93,9 @@ export class TabTitle extends AbstractPureComponent<TabTitleProps> { | |
private handleClick = (e: React.MouseEvent<HTMLElement>) => this.props.onClick(this.props.id, e); | ||
} | ||
|
||
export function generateTabPanelId(parentId: TabId, tabId: TabId) { | ||
return `${Classes.TAB_PANEL}_${parentId}_${tabId}`; | ||
} | ||
|
||
export function generateTabTitleId(parentId: TabId, tabId: TabId) { | ||
return `${Classes.TAB}-title_${parentId}_${tabId}`; | ||
export function generateTabIds(parentId: TabId, tabId: TabId) { | ||
return { | ||
tabPanelId: `${Classes.TAB_PANEL}_${parentId}_${tabId}`, | ||
tabTitleId: `${Classes.TAB}-title_${parentId}_${tabId}`, | ||
} satisfies TabIdProps; | ||
Comment on lines
+96
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Bingo, exactly! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,8 @@ import * as React from "react"; | |
import { AbstractPureComponent, Classes, DISPLAYNAME_PREFIX, type Props, Utils } from "../../common"; | ||
|
||
import { Tab, type TabId, type TabProps } from "./tab"; | ||
import { generateTabPanelId, generateTabTitleId, TabTitle } from "./tabTitle"; | ||
import { TabPanel } from "./tabPanel"; | ||
import { TabTitle } from "./tabTitle"; | ||
|
||
/** | ||
* Component that may be inserted between any two children of `<Tabs>` to right-align all subsequent children. | ||
|
@@ -330,20 +331,14 @@ export class Tabs extends AbstractPureComponent<TabsProps, TabsState> { | |
return undefined; | ||
} | ||
|
||
const tabTitleId = generateTabTitleId(this.props.id, id); | ||
const tabPanelId = generateTabPanelId(this.props.id, id); | ||
|
||
return ( | ||
<div | ||
aria-labelledby={tabTitleId} | ||
aria-hidden={id !== this.state.selectedTabId} | ||
className={classNames(Classes.TAB_PANEL, className, panelClassName)} | ||
id={tabPanelId} | ||
<TabPanel | ||
{...tab.props} | ||
key={id} | ||
role="tabpanel" | ||
> | ||
{Utils.isFunction(panel) ? panel({ tabTitleId, tabPanelId }) : panel} | ||
</div> | ||
className={classNames(className, panelClassName)} | ||
isHidden={id !== this.state.selectedTabId} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since any consumer would need to set this same logic up I wonder if we should have the prop be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Eh the logic may not always be the same. Take, for example, this case where import * as React from "react";
import { Tab, Tabs, TabPanel, type TabId } from "@blueprintjs/core";
...
const TABS_PARENT_ID = React.useId();
const [selectedTabId, setSelectedTabId] = React.useState<TabId>("Home");
<Tabs
id={TABS_PARENT_ID}
onChange={setSelectedTabId}
selectedTabId={selectedTabId}
>
<Tab id="Home" title="Home" />
<Tab id="Files" title="Files" />
</Tabs>
...
<TabPanel
id={selectedTabId}
isHidden={false}
parentId={TABS_PARENT_ID}
panel={<p>The current panel id is: "{selectedTabId}"</p>}
/> But then again, if we changed it to <TabPanel
id={selectedTabId}
selectedTabId={selectedTabId}
parentId={TABS_PARENT_ID}
panel={<p>The current panel id is: "{selectedTabId}"</p>}
/> I'm down for either, let me know which you like better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yea I think I'm leaning towards There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 12a665b |
||
parentId={this.props.id} | ||
/> | ||
); | ||
}; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking maybe we just make this
required
if the expected usage is that oneTabPanel
is provided for each tab, like is done in the uncontrolled usage ofTabs
. In your example you essentially share aTabPanel
but I'm wondering if making this optional encourages that pattern.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure