diff --git a/packages/components/src/components/link/_link.scss b/packages/components/src/components/link/_link.scss index b2a49d1ed9a3..9fecc51ac6fa 100644 --- a/packages/components/src/components/link/_link.scss +++ b/packages/components/src/components/link/_link.scss @@ -27,7 +27,7 @@ color: $link-01; text-decoration: none; outline: none; - transition: $duration--fast-01 motion(standard, productive); + transition: color $duration--fast-01 motion(standard, productive); &:hover { color: $hover-primary-text; diff --git a/packages/components/src/globals/js/settings.js b/packages/components/src/globals/js/settings.js index 2c1b461825d1..edcc1869328b 100644 --- a/packages/components/src/globals/js/settings.js +++ b/packages/components/src/globals/js/settings.js @@ -21,9 +21,13 @@ * // @todo given that the default value is so long, is it appropriate to put in the JSDoc? * @property {string} [selectorTabbable] * A selector selecting tabbable/focusable nodes. - * By default selectorTabbable refereneces links, areas, inputs, buttons, selects, textareas, + * By default selectorTabbable references links, areas, inputs, buttons, selects, textareas, * iframes, objects, embeds, or elements explicitly using tabindex or contenteditable attributes * as long as the element is not `disabled` or the `tabindex="-1"`. + * @property {string} [selectorFocusable] + * CSS selector that selects major nodes that are click focusable + * This property is identical to selectorTabbable with the exception of + * the `:not([tabindex='-1'])` pseudo class */ const settings = { prefix: 'bx', diff --git a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap index 875544dbc4dc..9a7d45484c44 100644 --- a/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap +++ b/packages/react/__tests__/__snapshots__/PublicAPI-test.js.snap @@ -3830,9 +3830,7 @@ Map { "onMouseUp": Object { "type": "func", }, - "primaryFocus": Object { - "type": "bool", - }, + "primaryFocus": [Function], "requireTitle": Object { "type": "bool", }, diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap index 45fca543a53e..8ca6f9b0ee41 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap @@ -2397,6 +2397,7 @@ exports[`DataTable should render 1`] = ` "render": [Function], } } + selectorPrimaryFocus="[data-overflow-menu-primary-focus]" tabIndex={0} title="Settings" > @@ -3373,6 +3374,7 @@ exports[`DataTable sticky header should render 1`] = ` "render": [Function], } } + selectorPrimaryFocus="[data-overflow-menu-primary-focus]" tabIndex={0} title="Settings" > diff --git a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap index a5203e15e385..4dc3eeeb425a 100644 --- a/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap +++ b/packages/react/src/components/DataTable/__tests__/__snapshots__/TableToolbarMenu-test.js.snap @@ -45,6 +45,7 @@ exports[`DataTable.TableToolbarMenu should render 1`] = ` "render": [Function], } } + selectorPrimaryFocus="[data-overflow-menu-primary-focus]" tabIndex={0} title="Add" > diff --git a/packages/react/src/components/DataTable/stories/with-overflow-menu.js b/packages/react/src/components/DataTable/stories/with-overflow-menu.js index a92f7547f085..c28837decf9a 100644 --- a/packages/react/src/components/DataTable/stories/with-overflow-menu.js +++ b/packages/react/src/components/DataTable/stories/with-overflow-menu.js @@ -55,7 +55,7 @@ export default props => ( ))} - Action 1 + Action 1 Action 2 Action 3 diff --git a/packages/react/src/components/OverflowMenu/OverflowMenu-story.js b/packages/react/src/components/OverflowMenu/OverflowMenu-story.js index b783038e54b0..4c2c7e9a7732 100644 --- a/packages/react/src/components/OverflowMenu/OverflowMenu-story.js +++ b/packages/react/src/components/OverflowMenu/OverflowMenu-story.js @@ -26,6 +26,10 @@ const props = { iconDescription: text('Icon description (iconDescription)', ''), flipped: boolean('Flipped (flipped)', false), light: boolean('Light (light)', false), + selectorPrimaryFocus: text( + 'Primary focus element selector (selectorPrimaryFocus)', + '' + ), onClick: action('onClick'), onFocus: action('onFocus'), onKeyDown: action('onKeyDown'), @@ -51,11 +55,7 @@ storiesOf('OverflowMenu', module) 'basic', withReadme(OverflowREADME, () => ( - +
Menu
, }}> - + { - const { current: triggerEl } = this._triggerRef; - if (triggerEl) { - const primaryFocusPropEl = triggerEl.querySelector( - '[data-floating-menu-primary-focus]' - ); - if (primaryFocusPropEl) { - return primaryFocusPropEl; - } - } - const firstItem = this.overflowMenuItem0; - if ( - firstItem && - firstItem.overflowMenuItem && - firstItem.overflowMenuItem.current - ) { - return firstItem.overflowMenuItem.current; - } - }; - componentDidUpdate(_, prevState) { const { onClose } = this.props; if (!this.state.open && prevState.isOpen) { @@ -393,10 +380,6 @@ class OverflowMenu extends Component { _handlePlace = menuBody => { if (menuBody) { this._menuBody = menuBody; - const primaryFocus = - menuBody.querySelector('[data-floating-menu-primary-focus]') || - menuBody; - primaryFocus.focus(); const hasFocusin = 'onfocusin' in window; const focusinEventName = hasFocusin ? 'focusin' : 'focus'; this._hFocusIn = on( @@ -446,6 +429,7 @@ class OverflowMenu extends Component { iconClass, onClick, // eslint-disable-line onOpen, // eslint-disable-line + selectorPrimaryFocus = '[data-floating-menu-primary-focus]', // eslint-disable-line renderIcon: IconElement, innerRef: ref, menuOptionsClass, @@ -509,7 +493,8 @@ class OverflowMenu extends Component { menuRef={this._bindMenuBody} flipped={this.props.flipped} target={this._getTarget} - onPlace={this._handlePlace}> + onPlace={this._handlePlace} + selectorPrimaryFocus={this.props.selectorPrimaryFocus}> {React.cloneElement(menuBody, { 'data-floating-menu-direction': direction, })} diff --git a/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js b/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js index 3cd85525c71d..bba575ce3149 100644 --- a/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js +++ b/packages/react/src/components/OverflowMenuItem/OverflowMenuItem.js @@ -11,6 +11,7 @@ import classNames from 'classnames'; import warning from 'warning'; import { settings } from 'carbon-components'; import { match, keys } from '../../internal/keyboard'; +import deprecate from '../../prop-types/deprecate.js'; const { prefix } = settings; @@ -72,7 +73,13 @@ export default class OverflowMenuItem extends React.Component { /** * `true` if this menu item should get focus when the menu gets open. */ - primaryFocus: PropTypes.bool, + primaryFocus: deprecate( + PropTypes.bool, + 'The `primaryFocus` prop has been deprecated as it is no longer used. ' + + 'Feel free to remove this prop from . This prop will ' + + 'be removed in the next major release of `carbon-components-react`. ' + + 'Opt for `selectorPrimaryFocus` in `` instead' + ), /** * `true` if this menu item has long text and requires a browser tooltip @@ -149,9 +156,6 @@ export default class OverflowMenuItem extends React.Component { }, wrapperClassName ); - const primaryFocusProp = primaryFocus - ? { 'data-floating-menu-primary-focus': true } - : {}; const TagToUse = href ? 'a' : 'button'; const OverflowMenuItemContent = (() => { if (typeof itemText !== 'string') { @@ -167,7 +171,7 @@ export default class OverflowMenuItem extends React.Component {
  • )', 0), + selectorPrimaryFocus: text( + 'Primary focus element selector (selectorPrimaryFocus)', + '' + ), }), withoutIcon: () => ({ showIcon: false, direction: select('Tooltip direction (direction)', directions, 'bottom'), triggerText: text('Trigger text (triggerText)', 'Tooltip label'), tabIndex: number('Tab index (tabIndex in )', 0), + selectorPrimaryFocus: text( + 'Primary focus element selector (selectorPrimaryFocus)', + '' + ), }), customIcon: () => ({ showIcon: true, direction: select('Tooltip direction (direction)', directions, 'bottom'), triggerText: text('Trigger text (triggerText)', 'Tooltip label'), tabIndex: number('Tab index (tabIndex in )', 0), + selectorPrimaryFocus: text( + 'Primary focus element selector (selectorPrimaryFocus)', + '' + ), renderIcon: React.forwardRef((props, ref) => (
    @@ -52,6 +64,10 @@ const props = { direction: select('Tooltip direction (direction)', directions, 'bottom'), iconDescription: 'Helpful Information', tabIndex: number('Tab index (tabIndex in )', 0), + selectorPrimaryFocus: text( + 'Primary focus element selector (selectorPrimaryFocus)', + '' + ), renderIcon: OverflowMenuVertical16, }), }; diff --git a/packages/react/src/components/Tooltip/Tooltip.js b/packages/react/src/components/Tooltip/Tooltip.js index e8bbd79ce353..272185364421 100644 --- a/packages/react/src/components/Tooltip/Tooltip.js +++ b/packages/react/src/components/Tooltip/Tooltip.js @@ -132,6 +132,12 @@ class Tooltip extends Component { */ direction: PropTypes.oneOf(['bottom', 'top', 'left', 'right']), + /** + * Specify a CSS selector that matches the DOM element that should + * be focused when the Tooltip opens + */ + selectorPrimaryFocus: PropTypes.string, + /** * The adjustment of the tooltip position. */ @@ -201,6 +207,7 @@ class Tooltip extends Component { showIcon: true, triggerText: null, menuOffset: getMenuOffset, + selectorPrimaryFocus: '[data-tooltip-primary-focus]', }; /** @@ -381,6 +388,7 @@ class Tooltip extends Component { menuOffset, tabIndex = 0, innerRef: ref, + selectorPrimaryFocus, // eslint-disable-line ...other } = this.props; @@ -447,6 +455,7 @@ class Tooltip extends Component { {open && ( { + const primaryFocusNode = menuBody.querySelector( + this.props.selectorPrimaryFocus || null + ); + const tabbableNode = menuBody.querySelector(selectorTabbable); + const focusableNode = menuBody.querySelector(selectorFocusable); + const focusTarget = + primaryFocusNode || // User defined focusable node + tabbableNode || // First sequentially focusable node + focusableNode || // First programmatic focusable node + menuBody; + focusTarget.focus(); + if (focusTarget === menuBody && __DEV__) { + warning( + focusableNode === null, + 'Floating Menus must have at least a programmatically focusable child. ' + + 'This can be accomplished by adding tabIndex="-1" to the content element.' + ); + } + }; componentDidUpdate(prevProps) { this._updateMenuSize(prevProps); const { onPlace } = this.props; - if ( - this._placeInProgress && - this.state.floatingPosition && - typeof onPlace === 'function' - ) { - onPlace(this._menuBody); - this._placeInProgress = false; + if (this._placeInProgress && this.state.floatingPosition) { + if (this._menuBody && !this._menuBody.contains(document.activeElement)) { + this._focusMenuContent(this._menuBody); + } + if (typeof onPlace === 'function') { + onPlace(this._menuBody); + this._placeInProgress = false; + } } } diff --git a/packages/react/src/internal/keyboard/navigation.js b/packages/react/src/internal/keyboard/navigation.js index cdd85de640f4..30536e8f9f0f 100644 --- a/packages/react/src/internal/keyboard/navigation.js +++ b/packages/react/src/internal/keyboard/navigation.js @@ -52,7 +52,7 @@ export const DOCUMENT_POSITION_BROAD_FOLLOWING = Node.DOCUMENT_POSITION_FOLLOWING | Node.DOCUMENT_POSITION_CONTAINED_BY; /** - * CSS selector that selects major nodes that is sequential-focusable. + * CSS selector that selects major nodes that are sequential-focusable. */ export const selectorTabbable = ` a[href], area[href], input:not([disabled]):not([tabindex='-1']), @@ -60,3 +60,13 @@ export const selectorTabbable = ` textarea:not([disabled]):not([tabindex='-1']), iframe, object, embed, *[tabindex]:not([tabindex='-1']), *[contenteditable=true] `; + +/** + * CSS selector that selects major nodes that are click focusable + */ +export const selectorFocusable = ` + a[href], area[href], input:not([disabled]), + button:not([disabled]),select:not([disabled]), + textarea:not([disabled]), + iframe, object, embed, *[tabindex], *[contenteditable=true] +`;