如何独立控制Carbon UI Shell的两个Header Panel展开状态?
如何独立控制Carbon UI Shell中多个Header Panel的展开/收起
我参考Carbon Design System的UI-shell-right-panel文档创建了两个Header Panel,分别用于通知和应用切换功能。但实际运行时第二个Panel总是覆盖第一个,无论点击哪个操作图标,都只会展开第二个Panel。请问如何实现点击对应图标展开对应Panel的独立控制?核心代码如下:
import React from "react"; import { HeaderContainer, Header, SkipToContent, HeaderMenuButton, HeaderName, HeaderNavigation, HeaderMenu, HeaderMenuItem, HeaderGlobalBar, HeaderGlobalAction, SideNav, SideNavItems, Content, SideNavMenu, SideNavMenuItem, Theme, ExpandableSearch, ToastNotification, HeaderPanel, SwitcherItem, SwitcherDivider, Switcher } from '@carbon/react'; import { Notification, Search, Switcher as SwitcherIcon, Fade, } from '@carbon/react/icons'; import { Route, Routes, BrowserRouter, Link } from 'react-router-dom'; import ErrorBoundary from "../../components/ErrorBoundary"; import LandingPage from '../LandingPage'; import NotFound from '../../components/NotFound'; import {Contributing} from "../contributing/contributing.tsx"; import {HeaderContainerRenderProps} from "@carbon/react/lib/components/UIShell/HeaderContainer"; interface StateType{ activeItem: string, expandingAction:string } export class UIShell extends React.Component { state: StateType; constructor(props:any) { super(props); this.state = { activeItem: `/${window.location.pathname.split('/')[1] ?? ''}`, expandingAction:'' }; this.setExpandingAction = this.setExpandingAction.bind(this); } async setExpandingAction(action:string){ await this.setState(()=>{ return {expandingAction:action}; }) } render() { return ( <BrowserRouter> <Theme theme='g90'> <HeaderContainer render={({ isSideNavExpanded, onClickSideNavExpand }:HeaderContainerRenderProps) => ( <div> <Header aria-label="IBM Executive Performance Platform"> <SkipToContent /> <HeaderMenuButton aria-label="Open menu" onClick={onClickSideNavExpand} isActive={isSideNavExpanded}/> <HeaderName href="/" prefix="Carbon">Template</HeaderName> <HeaderNavigation aria-label="Carbon React App"> <HeaderMenuItem href="/contributing">Contributing</HeaderMenuItem> <HeaderMenuItem>Contact</HeaderMenuItem> <HeaderMenu aria-label="How To" menuLinkName="How To"> <HeaderMenuItem href="#one">Sub-link 1</HeaderMenuItem> <HeaderMenuItem href="#two">Sub-link 2</HeaderMenuItem> <HeaderMenuItem href="#three">Sub-link 3</HeaderMenuItem> </HeaderMenu> </HeaderNavigation> <HeaderGlobalBar> <div > <ExpandableSearch size="lg" labelText="Search" closeButtonLabelText="Clear search input" id="search-expandable-1" onChange={() => {}} onKeyDown={() => {}} /> </div> <HeaderGlobalAction aria-label="Notifications" aria-expanded={isSideNavExpanded} isActive={isSideNavExpanded} onClick={()=>{onClickSideNavExpand();}} tooltipAlignment="end"> <Notification size={20} /> </HeaderGlobalAction> <HeaderGlobalAction aria-label={isSideNavExpanded ? 'Close switcher' : 'Open switcher'} aria-expanded={isSideNavExpanded} isActive={isSideNavExpanded} onClick={()=>{onClickSideNavExpand();}} tooltipAlignment="end" > <SwitcherIcon size={20} /> </HeaderGlobalAction> </HeaderGlobalBar> {/*notification panel */} <HeaderPanel expanded={isSideNavExpanded} onHeaderPanelFocus={onClickSideNavExpand} href="#notification"> <Switcher aria-label="Switcher Container" expanded={isSideNavExpanded}> <SwitcherItem aria-label="Link 1" href="#"> Successfully Created a performance report </SwitcherItem> <SwitcherDivider /> <SwitcherItem href="#" aria-label="Link 2"> You've got a evaluation from PEM </SwitcherItem> </Switcher> </HeaderPanel> {/*app switcher panel */} <HeaderPanel expanded={isSideNavExpanded} onHeaderPanelFocus={onClickSideNavExpand}> <Switcher aria-label="Switcher Container" expanded={isSideNavExpanded}> <SwitcherItem aria-label="Link 1" href="#"> Link 1 </SwitcherItem> <SwitcherDivider /> <SwitcherItem href="#" aria-label="Link 2"> Link 2 </SwitcherItem> <SwitcherItem href="#" aria-label="Link 3"> Link 3 </SwitcherItem> <SwitcherItem href="#" aria-label="Link 4"> Link 4 </SwitcherItem> <SwitcherItem href="#" aria-label="Link 5"> Link 5 </SwitcherItem> <SwitcherDivider /> <SwitcherItem href="#" aria-label="Link 6"> Link 6 </SwitcherItem> </Switcher> </HeaderPanel> <ErrorBoundary> <SideNav aria-label="Side navigation" expanded={isSideNavExpanded}> <SideNavItems> <SideNavMenuItem element={Link} to='/' isActive={this.state.activeItem === '/'} onClick={() => { this.setState({ activeItem: '/' }) }}> Overview </SideNavMenuItem> <SideNavMenu renderIcon={Fade} title="Inventory" defaultExpanded> <SideNavMenuItem element={Link} to='/inventory/items' isActive={this.state.activeItem === '/inventory/items'} onClick={() => { this.setState({ activeItem: '/inventory/items' }) }}> Items </SideNavMenuItem> </SideNavMenu> <SideNavMenu renderIcon={Fade} title="Management"> <SideNavMenuItem href="#"> Link </SideNavMenuItem> <SideNavMenuItem href="#"> Link </SideNavMenuItem> <SideNavMenuItem href="#"> Link </SideNavMenuItem> </SideNavMenu> <SideNavMenu renderIcon={Fade} title="Docs"> <SideNavMenuItem href="#"> Link </SideNavMenuItem> <SideNavMenuItem href="#"> Link </SideNavMenuItem> </SideNavMenu> </SideNavItems> </SideNav> </ErrorBoundary> </Header> </div> )} /> </Theme> <Content className='content'> <Routes> <Route path="/" element={<LandingPage />} /> <Route path="/contributing" element={<Contributing />} /> <Route path="*" element={<NotFound />} /> </Routes> </Content> </BrowserRouter> ); } }
问题根源
你用了同一个isSideNavExpanded状态控制两个Header Panel的展开/收起,且两个图标点击都触发同一个onClickSideNavExpand方法,导致无论点击哪个图标,两个Panel都会同时尝试展开。由于第二个Panel在DOM结构中靠后,会覆盖第一个Panel,最终只显示第二个。
解决方案:独立状态管理
为每个Panel维护独立的状态变量,修改点击逻辑实现精准控制:
import React from "react"; import { HeaderContainer, Header, SkipToContent, HeaderMenuButton, HeaderName, HeaderNavigation, HeaderMenu, HeaderMenuItem, HeaderGlobalBar, HeaderGlobalAction, SideNav, SideNavItems, Content, SideNavMenu, SideNavMenuItem, Theme, ExpandableSearch, ToastNotification, HeaderPanel, SwitcherItem, SwitcherDivider, Switcher } from '@carbon/react'; import { Notification, Search, Switcher as SwitcherIcon, Fade, } from '@carbon/react/icons'; import { Route, Routes, BrowserRouter, Link } from 'react-router-dom'; import ErrorBoundary from "../../components/ErrorBoundary"; import LandingPage from '../LandingPage'; import NotFound from '../../components/NotFound'; import {Contributing} from "../contributing/contributing.tsx"; import {HeaderContainerRenderProps} from "@carbon/react/lib/components/UIShell/HeaderContainer"; interface StateType{ activeItem: string, isNotificationPanelExpanded: boolean, isSwitcherPanelExpanded: boolean } export class UIShell extends React.Component { state: StateType; constructor(props:any) { super(props); this.state = { activeItem: `/${window.location.pathname.split('/')[1] ?? ''}`, isNotificationPanelExpanded: false, isSwitcherPanelExpanded: false }; this.toggleNotificationPanel = this.toggleNotificationPanel.bind(this); this.toggleSwitcherPanel = this.toggleSwitcherPanel.bind(this); this.closeNotificationPanel = this.closeNotificationPanel.bind(this); this.closeSwitcherPanel = this.closeSwitcherPanel.bind(this); } // 切换通知面板:展开时自动关闭切换器面板 toggleNotificationPanel() { this.setState(prevState => ({ isNotificationPanelExpanded: !prevState.isNotificationPanelExpanded, isSwitcherPanelExpanded: false })); } // 切换切换器面板:展开时自动关闭通知面板 toggleSwitcherPanel() { this.setState(prevState => ({ isSwitcherPanelExpanded: !prevState.isSwitcherPanelExpanded, isNotificationPanelExpanded: false })); } // 关闭通知面板 closeNotificationPanel() { this.setState({ isNotificationPanelExpanded: false }); } // 关闭切换器面板 closeSwitcherPanel() { this.setState({ isSwitcherPanelExpanded: false }); } render() { return ( <BrowserRouter> <Theme theme='g90'> <HeaderContainer render={({ isSideNavExpanded, onClickSideNavExpand }:HeaderContainerRenderProps) => ( <div> <Header aria-label="IBM Executive Performance Platform"> <SkipToContent /> <HeaderMenuButton aria-label="Open menu" onClick={onClickSideNavExpand} isActive={isSideNavExpanded}/> <HeaderName href="/" prefix="Carbon">Template</HeaderName> <HeaderNavigation aria-label="Carbon React App"> <HeaderMenuItem href="/contributing">Contributing</HeaderMenuItem> <HeaderMenuItem>Contact</HeaderMenuItem> <HeaderMenu aria-label="How To" menuLinkName="How To"> <HeaderMenuItem href="#one">Sub-link 1</HeaderMenuItem> <HeaderMenuItem href="#two">Sub-link 2</HeaderMenuItem> <HeaderMenuItem href="#three">Sub-link 3</HeaderMenuItem> </HeaderMenu> </HeaderNavigation> <HeaderGlobalBar> <div > <ExpandableSearch size="lg" labelText="Search" closeButtonLabelText="Clear search input" id="search-expandable-1" onChange={() => {}} onKeyDown={() => {}} /> </div> <HeaderGlobalAction aria-label="Notifications" aria-expanded={this.state.isNotificationPanelExpanded} isActive={this.state.isNotificationPanelExpanded} onClick={this.toggleNotificationPanel} tooltipAlignment="end" > <Notification size={20} /> </HeaderGlobalAction> <HeaderGlobalAction aria-label={this.state.isSwitcherPanelExpanded ? 'Close switcher' : 'Open switcher'} aria-expanded={this.state.isSwitcherPanelExpanded} isActive={this.state.isSwitcherPanelExpanded} onClick={this.toggleSwitcherPanel} tooltipAlignment="end" > <SwitcherIcon size={20} /> </HeaderGlobalAction> </HeaderGlobalBar> {/* 通知面板:绑定独立状态和关闭事件 */} <HeaderPanel expanded={this.state.isNotificationPanelExpanded} onHeaderPanelFocus={this.closeNotificationPanel} onBlur={this.closeNotificationPanel} href="#notification" > <Switcher aria-label="Notifications Container" expanded={this.state.isNotificationPanelExpanded}> <SwitcherItem aria-label="Link 1" href="#"> Successfully Created a performance report </SwitcherItem> <SwitcherDivider /> <SwitcherItem href="#" aria-label="Link 2"> You've got a evaluation from PEM </SwitcherItem> </Switcher> </HeaderPanel> {/* 应用切换面板:绑定独立状态和关闭事件 */} <HeaderPanel expanded={this.state.isSwitcherPanelExpanded} onHeaderPanelFocus={this.closeSwitcherPanel} onBlur={this.closeSwitcherPanel} > <Switcher aria-label="Switcher Container" expanded={this.state.isSwitcherPanelExpanded}> <SwitcherItem aria-label="Link 1" href="#"> Link 1 </SwitcherItem> <SwitcherDivider /> <SwitcherItem href="#" aria-label="Link 2"> Link 2 </SwitcherItem> <SwitcherItem href="#" aria-label="Link 3"> Link 3 </SwitcherItem> <SwitcherItem href="#" aria-label="Link 4"> Link 4 </SwitcherItem> <SwitcherItem href="#" aria-label="Link 5"> Link 5 </SwitcherItem> <SwitcherDivider /> <SwitcherItem href="#" aria-label="Link 6"> Link 6 </SwitcherItem> </Switcher> </HeaderPanel> <ErrorBoundary> <SideNav aria-label="Side navigation" expanded={isSideNavExpanded}> <SideNavItems> <SideNavMenuItem element={Link} to='/' isActive={this.state.activeItem === '/'} onClick={() => { this.setState({ activeItem: '/' }) }}> Overview </SideNavMenuItem> <SideNavMenu renderIcon={Fade} title="Inventory" defaultExpanded> <SideNavMenuItem element={Link} to='/inventory/items' isActive={this.state.activeItem === '/inventory/items'} onClick={() => { this.setState({ activeItem: '/inventory/items' }) }}> Items </SideNavMenuItem> </SideNavMenu> <SideNavMenu renderIcon={Fade} title="Management"> <SideNavMenuItem href="#"> Link </SideNavMenuItem> <SideNavMenuItem href="#"> Link </SideNavMenuItem> <SideNavMenuItem href="#"> Link </SideNavMenuItem> </SideNavMenu> <SideNavMenu renderIcon={Fade} title="Docs"> <SideNavMenuItem href="#"> Link </SideNavMenuItem> <SideNavMenuItem href="#"> Link </SideNavMenuItem> </SideNavMenu> </SideNavItems> </SideNav> </ErrorBoundary> </Header> </div> )} /> </Theme> <Content className='content'> <Routes> <Route path="/" element={<LandingPage />} /> <Route path="/contributing" element={<Contributing />} /> <Route path="*" element={<NotFound />} /> </Routes> </Content> </BrowserRouter> ); } }
核心修改点
- 状态分离:新增
isNotificationPanelExpanded和isSwitcherPanelExpanded两个独立状态,分别控制对应Panel的显示/隐藏 - 互斥展开逻辑:点击一个图标时,自动关闭另一个Panel,避免重叠
- 独立绑定状态:每个HeaderPanel的
expanded属性绑定各自的状态变量,不再复用侧边栏状态 - 完善关闭交互:添加
onBlur和onHeaderPanelFocus事件,实现点击外部或焦点离开时自动关闭面板 - 无障碍优化:图标按钮的
aria-expanded和isActive属性同步对应Panel的状态,符合无障碍规范
内容的提问来源于stack exchange,提问作者user1231111
相关产品推荐
相关产品推荐

