diff --git a/packages/ui-drilldown/src/Drilldown/index.tsx b/packages/ui-drilldown/src/Drilldown/index.tsx index 2e0bd01f8b..725ea55850 100644 --- a/packages/ui-drilldown/src/Drilldown/index.tsx +++ b/packages/ui-drilldown/src/Drilldown/index.tsx @@ -887,8 +887,20 @@ class Drilldown extends Component { } handleToggle = (event: React.UIEvent | React.FocusEvent, shown: boolean) => { - const { onToggle } = this.props - + const { onToggle, trigger } = this.props + + if (trigger && shown && this.currentPage) { + const actionLabel = callRenderProp(this.currentPage.renderActionLabel) + // Use action ID if exists, otherwise first non-action option's ID + const targetId = actionLabel + ? this._headerActionId + : this.currentPage.children[0]?.props.id + setTimeout(() => { + this.setState({ + highlightedOptionId: targetId + }) + }, 10) + } this.setState({ isShowingPopover: shown }) if (typeof onToggle === 'function') { @@ -1481,7 +1493,7 @@ class Drilldown extends Component { this.handleToggle(event, false) }} onShowContent={(event) => this.handleToggle(event, true)} - mountNode={mountNode} + mountNode={mountNode || this.ref} placement={placement} withArrow={withArrow} positionTarget={positionTarget} @@ -1495,6 +1507,17 @@ class Drilldown extends Component { onMouseOver={onMouseOver} offsetX={offsetX} offsetY={offsetY} + defaultFocusElement={() => { + if (!this.currentPage) return null + const actionLabel = callRenderProp(this.currentPage.renderActionLabel) + // Use action ID if exists, otherwise first non-action option's ID + const targetId = actionLabel + ? this._headerActionId + : this.currentPage.children[0]?.props.id + + if (!targetId) return null + return this._popover?._contentElement?.querySelector(`#${targetId}`) + }} elementRef={(element) => { // setting ref for "Popover" version, the popover root // (if there is no trigger, we set it in handleDrilldownRef) diff --git a/packages/ui-drilldown/src/Drilldown/props.ts b/packages/ui-drilldown/src/Drilldown/props.ts index 2931127ca3..0dbf36f924 100644 --- a/packages/ui-drilldown/src/Drilldown/props.ts +++ b/packages/ui-drilldown/src/Drilldown/props.ts @@ -207,7 +207,7 @@ type DrilldownOwnProps = { /** * If a trigger is supplied, an element or a function returning an element - * to use as the mount node for the `` (defaults to `document.body`) + * to use as the mount node for the `` (defaults to the component itself) */ mountNode?: PositionMountNode diff --git a/packages/ui-top-nav-bar/src/TopNavBar/README.md b/packages/ui-top-nav-bar/src/TopNavBar/README.md index b1f6aa0842..465783aa49 100644 --- a/packages/ui-top-nav-bar/src/TopNavBar/README.md +++ b/packages/ui-top-nav-bar/src/TopNavBar/README.md @@ -648,12 +648,10 @@ class PlaygroundExample extends React.Component { {...item.submenu && { renderSubmenu: this.generateSubmenu(item), - 'aria-label': `Open for ${item.label} menu` }} {...item.customPopoverConfig && { customPopoverConfig: item.customPopoverConfig, - 'aria-label': `Open for ${item.label} menu` }} {...!item.submenu && !item.customPopoverConfig ? { @@ -720,7 +718,7 @@ class PlaygroundExample extends React.Component { } - aria-label="Open for info menu" + aria-label="info menu" renderSubmenu={this.generateSubmenu({ id: 'Info', submenu: ['Contact', 'Map', 'Career'].map(item => ({ diff --git a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/DesktopLayout/styles.ts b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/DesktopLayout/styles.ts index fe9c0d50a6..71c20bf117 100644 --- a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/DesktopLayout/styles.ts +++ b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/DesktopLayout/styles.ts @@ -66,10 +66,9 @@ const generateStyle = ( alignItems: 'stretch', justifyContent: 'space-between', height: componentTheme.desktopHeight, - position: 'relative', zIndex: componentTheme.desktopZIndex, maxWidth: '100%', - overflow: 'hidden', + overflow: 'visible', paddingInline: componentTheme.desktopInlinePadding, paddingBlock: 0, ...(hasBrandBlock && { @@ -113,7 +112,6 @@ const generateStyle = ( marginInline: componentTheme.desktopUserContainerInlineMargin, ...(hasUserSeparator && { - position: 'relative', paddingInlineStart: componentTheme.desktopUserSeparatorGap, '&::before': { diff --git a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/SmallViewportLayout/index.tsx b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/SmallViewportLayout/index.tsx index 06ac0b5edf..96c811f36c 100644 --- a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/SmallViewportLayout/index.tsx +++ b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/SmallViewportLayout/index.tsx @@ -104,6 +104,7 @@ class TopNavBarSmallViewportLayout extends Component< private readonly _inPlaceDialogId: string private readonly _inPlaceDialogCloseButtonId: string private readonly _separatorId: string + private _drilldownRef: Drilldown | null = null private _raf: RequestAnimationFrameType[] = [] @@ -286,7 +287,44 @@ class TopNavBarSmallViewportLayout extends Component< onDropdownMenuToggle(!isDropdownMenuOpen) } - this.setState({ isDropdownMenuOpen: !isDropdownMenuOpen }) + this.setState( + { isDropdownMenuOpen: !this.state.isDropdownMenuOpen }, + () => { + if (this.state.isDropdownMenuOpen) { + this.focusFirstAvailableItem() + } + } + ) + } + + focusFirstAvailableItem() { + const userChildren = Children.toArray( + this.props.renderUser?.props.children + ) as ItemChild[] + + // If option is a User, it preceeds other item, so focus that first + const targetId = userChildren[0] + ? userChildren[0].props.id + : this.mappedMenuItemsOptions[0].optionData.id + + setTimeout(() => { + const container = document.getElementById(this._trayContainerId) + const firstOption = container?.querySelector( + `[id="${targetId}"]` + ) as HTMLSpanElement + firstOption?.focus() + if (this._drilldownRef) { + const drilldownRef = this._drilldownRef + setTimeout(() => { + if (drilldownRef) { + // highlight the option using Drilldown's handleOptionHighlight function + drilldownRef.handleOptionHighlight({} as React.SyntheticEvent, { + id: targetId + }) + } + }, 10) + } + }, 10) } renderOptionContent: RenderOptionContent = (children, itemProps) => { @@ -402,6 +440,9 @@ class TopNavBarSmallViewportLayout extends Component< return (
{menuTrigger} + {!this.hasBreadcrumbBlock && !this.props.trayMountNode && ( +
+ )} {this.hasBrandBlock(renderBrand) && !alternativeTitle && (
{renderBrand}
@@ -442,6 +483,9 @@ class TopNavBarSmallViewportLayout extends Component< return ( { + this._drilldownRef = el + }} id={this._drilldownId} rootPageId={this._menuId} label={dropdownMenuLabel} @@ -576,13 +620,7 @@ class TopNavBarSmallViewportLayout extends Component< } render() { - const { - trayMountNode, - navLabel, - renderActionItems, - renderBreadcrumb, - styles - } = this.props + const { navLabel, renderActionItems, renderBreadcrumb, styles } = this.props return ( ) diff --git a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarMenuItems/styles.ts b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarMenuItems/styles.ts index 043882d90b..64c3cba1f8 100644 --- a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarMenuItems/styles.ts +++ b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarMenuItems/styles.ts @@ -51,7 +51,8 @@ const generateStyle = ( flexDirection: 'row', alignItems: 'stretch', // padding to prevent focus ring getting cropped by `overflow: hidden` - padding: '0 0.125rem' + padding: '0 0.125rem', + overflow: 'visible' }, submenuOption: { label: 'topNavBarMenuItems__submenuOption',