Skip to content

Commit 6d7d3fa

Browse files
committed
fix(ui-top-nav-bar,ui-popover,ui-drilldown): fix Drilldown's and TopNavBar's keyboard navigation issues
INSTUI-4494
1 parent 983e580 commit 6d7d3fa

File tree

6 files changed

+77
-23
lines changed

6 files changed

+77
-23
lines changed

packages/ui-drilldown/src/Drilldown/index.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -887,8 +887,20 @@ class Drilldown extends Component<DrilldownProps, DrilldownState> {
887887
}
888888

889889
handleToggle = (event: React.UIEvent | React.FocusEvent, shown: boolean) => {
890-
const { onToggle } = this.props
891-
890+
const { onToggle, trigger } = this.props
891+
892+
if (trigger && shown && this.currentPage) {
893+
const actionLabel = callRenderProp(this.currentPage.renderActionLabel)
894+
// Use action ID if exists, otherwise first non-action option's ID
895+
const targetId = actionLabel
896+
? this._headerActionId
897+
: this.currentPage.children[0]?.props.id
898+
setTimeout(() => {
899+
this.setState({
900+
highlightedOptionId: targetId
901+
})
902+
}, 10)
903+
}
892904
this.setState({ isShowingPopover: shown })
893905

894906
if (typeof onToggle === 'function') {
@@ -1481,7 +1493,7 @@ class Drilldown extends Component<DrilldownProps, DrilldownState> {
14811493
this.handleToggle(event, false)
14821494
}}
14831495
onShowContent={(event) => this.handleToggle(event, true)}
1484-
mountNode={mountNode}
1496+
mountNode={mountNode || this.ref}
14851497
placement={placement}
14861498
withArrow={withArrow}
14871499
positionTarget={positionTarget}
@@ -1495,6 +1507,17 @@ class Drilldown extends Component<DrilldownProps, DrilldownState> {
14951507
onMouseOver={onMouseOver}
14961508
offsetX={offsetX}
14971509
offsetY={offsetY}
1510+
defaultFocusElement={() => {
1511+
if (!this.currentPage) return null
1512+
const actionLabel = callRenderProp(this.currentPage.renderActionLabel)
1513+
// Use action ID if exists, otherwise first non-action option's ID
1514+
const targetId = actionLabel
1515+
? this._headerActionId
1516+
: this.currentPage.children[0]?.props.id
1517+
1518+
if (!targetId) return null
1519+
return this._popover?._contentElement?.querySelector(`#${targetId}`)
1520+
}}
14981521
elementRef={(element) => {
14991522
// setting ref for "Popover" version, the popover root
15001523
// (if there is no trigger, we set it in handleDrilldownRef)

packages/ui-drilldown/src/Drilldown/props.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ type DrilldownOwnProps = {
207207

208208
/**
209209
* If a trigger is supplied, an element or a function returning an element
210-
* to use as the mount node for the `<Drilldown />` (defaults to `document.body`)
210+
* to use as the mount node for the `<Drilldown />` (defaults to the component itself)
211211
*/
212212
mountNode?: PositionMountNode
213213

packages/ui-top-nav-bar/src/TopNavBar/README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,12 +648,10 @@ class PlaygroundExample extends React.Component {
648648

649649
{...item.submenu && {
650650
renderSubmenu: this.generateSubmenu(item),
651-
'aria-label': `Open for ${item.label} menu`
652651
}}
653652

654653
{...item.customPopoverConfig && {
655654
customPopoverConfig: item.customPopoverConfig,
656-
'aria-label': `Open for ${item.label} menu`
657655
}}
658656

659657
{...!item.submenu && !item.customPopoverConfig ? {
@@ -720,7 +718,7 @@ class PlaygroundExample extends React.Component {
720718
<TopNavBar.Item
721719
id="Info"
722720
renderIcon={<IconQuestionLine />}
723-
aria-label="Open for info menu"
721+
aria-label="info menu"
724722
renderSubmenu={this.generateSubmenu({
725723
id: 'Info',
726724
submenu: ['Contact', 'Map', 'Career'].map(item => ({

packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/DesktopLayout/styles.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ const generateStyle = (
6666
alignItems: 'stretch',
6767
justifyContent: 'space-between',
6868
height: componentTheme.desktopHeight,
69-
position: 'relative',
7069
zIndex: componentTheme.desktopZIndex,
7170
maxWidth: '100%',
72-
overflow: 'hidden',
71+
overflow: 'visible',
7372
paddingInline: componentTheme.desktopInlinePadding,
7473
paddingBlock: 0,
7574
...(hasBrandBlock && {
@@ -113,7 +112,6 @@ const generateStyle = (
113112
marginInline: componentTheme.desktopUserContainerInlineMargin,
114113

115114
...(hasUserSeparator && {
116-
position: 'relative',
117115
paddingInlineStart: componentTheme.desktopUserSeparatorGap,
118116

119117
'&::before': {

packages/ui-top-nav-bar/src/TopNavBar/TopNavBarLayout/SmallViewportLayout/index.tsx

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class TopNavBarSmallViewportLayout extends Component<
104104
private readonly _inPlaceDialogId: string
105105
private readonly _inPlaceDialogCloseButtonId: string
106106
private readonly _separatorId: string
107+
private _drilldownRef: Drilldown | null = null
107108

108109
private _raf: RequestAnimationFrameType[] = []
109110

@@ -286,7 +287,44 @@ class TopNavBarSmallViewportLayout extends Component<
286287
onDropdownMenuToggle(!isDropdownMenuOpen)
287288
}
288289

289-
this.setState({ isDropdownMenuOpen: !isDropdownMenuOpen })
290+
this.setState(
291+
{ isDropdownMenuOpen: !this.state.isDropdownMenuOpen },
292+
() => {
293+
if (this.state.isDropdownMenuOpen) {
294+
this.focusFirstAvailableItem()
295+
}
296+
}
297+
)
298+
}
299+
300+
focusFirstAvailableItem() {
301+
const userChildren = Children.toArray(
302+
this.props.renderUser?.props.children
303+
) as ItemChild[]
304+
305+
// If option is a User, it preceeds other item, so focus that first
306+
const targetId = userChildren[0]
307+
? userChildren[0].props.id
308+
: this.mappedMenuItemsOptions[0].optionData.id
309+
310+
setTimeout(() => {
311+
const container = document.getElementById(this._trayContainerId)
312+
const firstOption = container?.querySelector(
313+
`[id="${targetId}"]`
314+
) as HTMLSpanElement
315+
firstOption?.focus()
316+
if (this._drilldownRef) {
317+
const drilldownRef = this._drilldownRef
318+
setTimeout(() => {
319+
if (drilldownRef) {
320+
// highlight the option using Drilldown's handleOptionHighlight function
321+
drilldownRef.handleOptionHighlight({} as React.SyntheticEvent, {
322+
id: targetId
323+
})
324+
}
325+
}, 10)
326+
}
327+
}, 10)
290328
}
291329

292330
renderOptionContent: RenderOptionContent = (children, itemProps) => {
@@ -402,6 +440,9 @@ class TopNavBarSmallViewportLayout extends Component<
402440
return (
403441
<div css={styles?.menuTriggerContainer}>
404442
{menuTrigger}
443+
{!this.hasBreadcrumbBlock && !this.props.trayMountNode && (
444+
<div css={styles?.trayContainer} id={this._trayContainerId} />
445+
)}
405446

406447
{this.hasBrandBlock(renderBrand) && !alternativeTitle && (
407448
<div css={styles?.brandContainer}>{renderBrand}</div>
@@ -442,6 +483,9 @@ class TopNavBarSmallViewportLayout extends Component<
442483

443484
return (
444485
<Drilldown
486+
ref={(el) => {
487+
this._drilldownRef = el
488+
}}
445489
id={this._drilldownId}
446490
rootPageId={this._menuId}
447491
label={dropdownMenuLabel}
@@ -576,13 +620,7 @@ class TopNavBarSmallViewportLayout extends Component<
576620
}
577621

578622
render() {
579-
const {
580-
trayMountNode,
581-
navLabel,
582-
renderActionItems,
583-
renderBreadcrumb,
584-
styles
585-
} = this.props
623+
const { navLabel, renderActionItems, renderBreadcrumb, styles } = this.props
586624

587625
return (
588626
<nav
@@ -607,10 +645,6 @@ class TopNavBarSmallViewportLayout extends Component<
607645

608646
{!this.hasBreadcrumbBlock && this.renderInPlaceDialog()}
609647

610-
{!this.hasBreadcrumbBlock && !trayMountNode && (
611-
<div css={styles?.trayContainer} id={this._trayContainerId} />
612-
)}
613-
614648
{!this.hasBreadcrumbBlock && this.renderDropdownMenuTray()}
615649
</nav>
616650
)

packages/ui-top-nav-bar/src/TopNavBar/TopNavBarMenuItems/styles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ const generateStyle = (
5151
flexDirection: 'row',
5252
alignItems: 'stretch',
5353
// padding to prevent focus ring getting cropped by `overflow: hidden`
54-
padding: '0 0.125rem'
54+
padding: '0 0.125rem',
55+
overflow: 'visible'
5556
},
5657
submenuOption: {
5758
label: 'topNavBarMenuItems__submenuOption',

0 commit comments

Comments
 (0)