UNPKG

@atlaskit/navigation-system

Version:

The latest navigation system for Atlassian apps.

127 lines (122 loc) • 4.87 kB
import { useCallback, useContext } from 'react'; import { flushSync } from 'react-dom'; import { bind } from 'bind-event-listener'; import { fg } from '@atlaskit/platform-feature-flags/fg'; import { SetSideNavVisibilityState } from './set-side-nav-visibility-state'; import { useSideNavRef } from './use-side-nav-ref'; /** * Moves focus to the first focusable item in the side nav, or the side nav element itself as a fallback. */ function focusFirstNavItem(sideNavElement) { var _firstNavItem$checkVi, _firstNavItem$checkVi2; /** * Try to get the first focusable item in the side nav. * The selector is not very broad, but should be appropriate for items from this package. */ var firstNavItem = sideNavElement.querySelector('a, button'); var isFirstNavItemVisible = firstNavItem !== null && ((_firstNavItem$checkVi = (_firstNavItem$checkVi2 = firstNavItem.checkVisibility) === null || _firstNavItem$checkVi2 === void 0 ? void 0 : _firstNavItem$checkVi2.call(firstNavItem)) !== null && _firstNavItem$checkVi !== void 0 ? _firstNavItem$checkVi : false); var itemToFocus = isFirstNavItemVisible ? firstNavItem : sideNavElement; if (itemToFocus === sideNavElement) { /** * Elements without an explicit `tabindex` attribute are not guaranteed to be focusable: * https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex * * Our slots are not interactive, so this is required. * * In the future we may want to check if there is an existing `tabindex` attribute, * as custom skip linked elements might already have one. */ sideNavElement.setAttribute('tabindex', '-1'); /** * Cleanup the `tabindex` attribute we set when the slot or custom target loses focus. * * This is preferable to always having `tabindex="-1"` because always applying the tab index can: * * - mess with click events * - potentially cause a focus ring to be always visible */ bind(sideNavElement, { type: 'blur', listener: function listener() { sideNavElement.removeAttribute('tabindex'); }, options: { once: true } }); } /** * Not using `focusVisible` option because we don't want clicks on the toggle button to show a focus ring. */ itemToFocus.focus(); } var triggersWithFocusOnExpand = new Set(['toggle-button', 'skip-link']); /** * __useExpandSideNav__ * * Returns a function that will expand the side nav when called. * * It will switch the appropriate internal desktop or mobile side nav visibility state based on the current screen size. * * If you need a function to toggle the side nav, use `useToggleSideNav` instead. */ export function useExpandSideNav() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref$trigger = _ref.trigger, trigger = _ref$trigger === void 0 ? 'programmatic' : _ref$trigger; var setSideNavState = useContext(SetSideNavVisibilityState); var sideNavRef = useSideNavRef(); var expandSideNav = useCallback(function () { var _window$matchMedia = window.matchMedia('(min-width: 64rem)'), isDesktop = _window$matchMedia.matches; var runUpdate = function runUpdate() { if (isDesktop) { setSideNavState(function (currentState) { // No-op if the side nav state has not been initialised yet // e.g. if the SideNav has not been mounted yet if (!currentState) { return null; } // Skip the re-render if it's a no-op change if (currentState.desktop === 'expanded' && currentState.flyout === 'closed') { return currentState; } return { mobile: currentState.mobile, desktop: 'expanded', flyout: 'closed', lastTrigger: trigger }; }); } else { setSideNavState(function (currentState) { // No-op if the side nav state has not been initialised yet // e.g. if the SideNav has not been mounted yet if (!currentState) { return null; } // Skip the re-render if it's a no-op change if (currentState.mobile === 'expanded' && currentState.flyout === 'closed') { return currentState; } return { desktop: currentState.desktop, mobile: 'expanded', flyout: 'closed', lastTrigger: trigger }; }); } }; if (triggersWithFocusOnExpand.has(trigger) && fg('platform_dst_nav4_skip_link_a11y_1')) { flushSync(runUpdate); var sideNavElement = sideNavRef.current; if (sideNavElement) { focusFirstNavItem(sideNavElement); } } else { runUpdate(); } }, [setSideNavState, sideNavRef, trigger]); return expandSideNav; }