diff --git a/packages/ui-buttons/src/BaseButton/index.tsx b/packages/ui-buttons/src/BaseButton/index.tsx index 0b4049e99d..28dc8e8e39 100644 --- a/packages/ui-buttons/src/BaseButton/index.tsx +++ b/packages/ui-buttons/src/BaseButton/index.tsx @@ -251,6 +251,7 @@ class BaseButton extends Component { tabIndex, styles, makeStyles, + withFocusOutline, ...props } = this.props @@ -307,6 +308,7 @@ class BaseButton extends Component { focusRingBorderRadius={String( (styles?.content as { borderRadius?: string | number })?.borderRadius )} + withFocusOutline={withFocusOutline} > {this.renderChildren()} diff --git a/packages/ui-buttons/src/BaseButton/props.ts b/packages/ui-buttons/src/BaseButton/props.ts index b3977d24c8..2efc0cfdae 100644 --- a/packages/ui-buttons/src/BaseButton/props.ts +++ b/packages/ui-buttons/src/BaseButton/props.ts @@ -159,9 +159,16 @@ type BaseButtonOwnProps = { /** * Specifies the tabindex of the `Button`. - * */ tabIndex?: number + + /** + * Manually control if the `Button` should display a focus outline. + * + * When left `undefined` (which is the default) the focus outline will display + * if this component is focusable and receives focus. + */ + withFocusOutline?: boolean } type BaseButtonStyleProps = { @@ -218,7 +225,8 @@ const propTypes: PropValidators = { onClick: PropTypes.func, onKeyDown: PropTypes.func, renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - tabIndex: PropTypes.number + tabIndex: PropTypes.number, + withFocusOutline: PropTypes.bool } const allowedProps: AllowedPropKeys = [ diff --git a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/index.tsx b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/index.tsx index 141e710208..585e018f67 100644 --- a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/index.tsx +++ b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/index.tsx @@ -128,7 +128,8 @@ class TopNavBarItem extends Component { this.state = { isSubmenuOpen: false, - isPopoverOpen: false + isPopoverOpen: false, + isFocused: false } } @@ -347,7 +348,8 @@ class TopNavBarItem extends Component { renderSubmenu, status: statusOriginal, renderAvatar, - renderIcon + renderIcon, + withFocusOutline } = this.props let href = hrefOriginal @@ -423,18 +425,28 @@ class TopNavBarItem extends Component { onClick, onMouseOver, onMouseOut, - onFocus, - onBlur, + onFocus: createChainedFunction(onFocus, this.onFocus), + onBlur: createChainedFunction(onBlur, this.onBlur), onKeyDown: createChainedFunction(onKeyDown, this.handleKeyDown), onKeyUp, renderIcon, themeOverride: this.buttonThemeOverride, elementRef: (e) => { this.handleItemRef(e as HTMLButtonElement | HTMLLinkElement) - } + }, + withFocusOutline: + withFocusOutline || this.hasOpenPopover || this.state.isFocused } } + onFocus = () => { + this.setState({ isFocused: true }) + } + + onBlur = () => { + this.setState({ isFocused: false }) + } + handleKeyDown: TopNavBarItemProps['onKeyDown'] = (e) => { if (e.key === 'ArrowDown') { if ( diff --git a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/props.ts b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/props.ts index 0c269b08d0..4e96c91b47 100644 --- a/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/props.ts +++ b/packages/ui-top-nav-bar/src/TopNavBar/TopNavBarItem/props.ts @@ -254,6 +254,14 @@ type TopNavBarItemOwnProps = { * Should close the container menu component, if clicked on the option marked with this prop */ shouldCloseOnClick?: ShouldCloseOnClick + + /** + * Manually control if this component should display a focus outline. + * + * When left `undefined` (which is the default) the focus outline will display + * if this component is focusable and receives focus or has an open popover. + */ + withFocusOutline?: boolean } type PropKeys = keyof TopNavBarItemOwnProps @@ -281,6 +289,7 @@ type TopNavBarItemStyle = ComponentStyle< type TopNavBarItemState = { isSubmenuOpen: boolean isPopoverOpen: boolean + isFocused: boolean } type TopNavBarItemStyleProps = { @@ -316,7 +325,8 @@ const propTypes: PropValidators = { onKeyUp: PropTypes.func, elementRef: PropTypes.func, itemRef: PropTypes.func, - shouldCloseOnClick: PropTypes.oneOf(['auto', 'always', 'never']) + shouldCloseOnClick: PropTypes.oneOf(['auto', 'always', 'never']), + withFocusOutline: PropTypes.bool } const allowedProps: AllowedPropKeys = [ @@ -343,7 +353,8 @@ const allowedProps: AllowedPropKeys = [ 'onKeyUp', 'elementRef', 'itemRef', - 'shouldCloseOnClick' + 'shouldCloseOnClick', + 'withFocusOutline' ] export type { 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..6c422ee858 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 @@ -362,7 +362,8 @@ class TopNavBarSmallViewportLayout extends Component< tooltip: dropdownMenuToggleButtonTooltip, themeOverride: { itemSpacing: '0.375rem' }, 'aria-haspopup': 'menu', - 'aria-expanded': isDropdownMenuOpen + 'aria-expanded': isDropdownMenuOpen, + withFocusOutline: isDropdownMenuOpen ? true : undefined } const alternativeTitleIconProps = { diff --git a/packages/ui-view/src/View/props.ts b/packages/ui-view/src/View/props.ts index eae53f6f04..a5e7509504 100644 --- a/packages/ui-view/src/View/props.ts +++ b/packages/ui-view/src/View/props.ts @@ -135,7 +135,8 @@ type ViewOwnProps = { */ insetBlockEnd?: string /** - * Manually control if the `View` should display a focus outline.
+ * Manually control if the `View` should display a focus outline. + * * When left `undefined` (which is the default) the focus outline will display * automatically if the `View` is focusable and receives focus. */