diff --git a/CHANGELOG.md b/CHANGELOG.md index 6353ab4f96..de2e231581 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### BREAKING CHANGES +- Add `trapFocus` and `autoFocus` props to `Popup` and remove `popupFocusTrapBehavior` and `popupAutoFocusBehavior` @sophieH29 ([#1565](https://github.com/stardust-ui/react/pull/1565)) + ## [v0.34.1](https://github.com/stardust-ui/react/tree/v0.34.1) (2019-07-11) [Compare changes](https://github.com/stardust-ui/react/compare/v0.34.0...v0.34.1) diff --git a/docs/src/examples/components/Popup/Types/PopupFocusTrapExample.shorthand.tsx b/docs/src/examples/components/Popup/Types/PopupFocusTrapExample.shorthand.tsx index ee753335d3..4c9bbb11f2 100644 --- a/docs/src/examples/components/Popup/Types/PopupFocusTrapExample.shorthand.tsx +++ b/docs/src/examples/components/Popup/Types/PopupFocusTrapExample.shorthand.tsx @@ -1,11 +1,11 @@ import * as React from 'react' -import { Button, Flex, Input, Header, Popup, popupFocusTrapBehavior } from '@stardust-ui/react' +import { Button, Flex, Input, Header, Popup } from '@stardust-ui/react' const PopupFocusTrapExample = () => ( } content={{ content: ( diff --git a/docs/src/examples/components/Popup/Usage/PopupExampleOnWithFocusTrap.shorthand.tsx b/docs/src/examples/components/Popup/Usage/PopupExampleOnWithFocusTrap.shorthand.tsx index 4809367757..7b54c1b720 100644 --- a/docs/src/examples/components/Popup/Usage/PopupExampleOnWithFocusTrap.shorthand.tsx +++ b/docs/src/examples/components/Popup/Usage/PopupExampleOnWithFocusTrap.shorthand.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Button, Flex, Popup, popupFocusTrapBehavior } from '@stardust-ui/react' +import { Button, Flex, Popup } from '@stardust-ui/react' const contentWithButtons = { content: ( @@ -15,19 +15,19 @@ const PopupExampleOnWithFocusTrap = () => ( } content={contentWithButtons} - accessibility={popupFocusTrapBehavior} + trapFocus on="click" /> } content={contentWithButtons} - accessibility={popupFocusTrapBehavior} + trapFocus on="hover" /> } content={contentWithButtons} - accessibility={popupFocusTrapBehavior} + trapFocus on="focus" /> diff --git a/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx b/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx index 7727851529..2b657c4fce 100644 --- a/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx +++ b/docs/src/examples/components/Portal/Types/PortalExampleFocusTrapped.shorthand.tsx @@ -33,7 +33,7 @@ class PortalExampleFocusTrapped extends React.Component { elementToFocusOnDismiss: null, // Indicates whether to force focus inside a Portal, if the 'focus' event was invoked at any place. // 'false' by default. - forceFocusInsideTrap: false, + forceFocusInsideTrapOnOutsideFocus: false, // Ignore focusing element which activated Portal after it was closed. // 'false' by default. ignoreExternalFocusing: false, diff --git a/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx index bf1100042d..b74370afec 100644 --- a/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx +++ b/docs/src/prototypes/chatMessages/ChatMessageWithPopover/ReactionPopup.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import keyboardKey from 'keyboard-key' -import { Popup, Menu, Reaction, ReactionProps, popupAutoFocusBehavior } from '@stardust-ui/react' +import { Popup, Menu, Reaction, ReactionProps } from '@stardust-ui/react' const getAriaLabel = ({ content: numberOfPersons, icon: emojiType }: ReactionProps) => { if (numberOfPersons === 1) { @@ -27,6 +27,7 @@ class ReactionPopup extends React.Component { render() { return ( { on="hover" open={this.state.open} onOpenChange={this.handleOpenChange} - accessibility={popupAutoFocusBehavior} /> ) } diff --git a/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx b/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx index 40661e5778..2cd1613e5b 100644 --- a/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx +++ b/docs/src/prototypes/chatPane/services/messageFactoryMock.tsx @@ -3,7 +3,6 @@ import { Popup, Button, Menu, - popupFocusTrapBehavior, AvatarProps, ChatMessageProps, DividerProps, @@ -127,7 +126,7 @@ function createMessageContentWithAttachments(content: string, messageId: string) const actionPopup = ( (

- FocusTrapZone is used to trap the focus in any html element. Pressing TAB key will circle - focus within the inner focusable elements of the FocusTrapZone. For example, when Popup opens, - we want the focus go inside Popup and trap there.{' '} + FocusTrapZone is used to grab and trap the focus inside an HTML element. Currently can be used + only in {code('Popup')} and {code('Dialog')} components. Pressing TAB key will circle focus + within the inner focusable elements of the FocusTrapZone. For example, when Popup opens, we + want the focus to go inside Popup and trap there.{' '} {link('Read more about FocusTrapZone.', '/focus-trap-zone')}

- AutoFocusZone is used to focus inner element on mount. For example, when we want to focus - inner element in Popup when it mounts, but still without focus trap.{' '} - {link('Read more about AutoFocusZone.', '/auto-focus-zone')} + AutoFocusZone is used to focus inner element on mount. Currently can be used in{' '} + {code('Popup')}. For example, when we want to focus inner element in Popup when it mounts, but + still without focus trap. {link('Read more about AutoFocusZone.', '/auto-focus-zone')}

diff --git a/docs/src/views/AccessibilityBehaviors.tsx b/docs/src/views/AccessibilityBehaviors.tsx index 253f1c5a3d..39146fe6e9 100644 --- a/docs/src/views/AccessibilityBehaviors.tsx +++ b/docs/src/views/AccessibilityBehaviors.tsx @@ -59,20 +59,6 @@ export default () => ( Read more about FocusZone.

Type: {code('{ mode: FocusZoneMode, props?: FocusZoneProps }')}.

-
  • - focusTrap - {code('FocusTrapZone')} grabs the focus and traps it within an HTML - element, usually a dialog or popup.{' '} - Read more about FocusTrapZone. -

    Type: {code('FocusTrapZoneProps | boolean')}.

    -
  • -
  • - autoFocus - {code('AutoFocusZone')} is used to grab focus and put it to inner - element when component mounts. For example, when it is needed to focus an inner element in - the Popup when it mounts. If true, it is enabled with default properties or can be - modified by setting object.{' '} - Read more about AutoFocusZone. -

    Type: {code('AutoFocusZoneProps | boolean')}.

    -
  • childBehaviors - {code('{ [childBehaviorSlot: string]: Accessibility }')} are used for components such as {code('Menu')} that contain children which, in turn, require their @@ -214,8 +200,6 @@ export default () => ( }, }, focusZone: {}, - focusTrap: {}, - autoFocus: {}, }) `} /> diff --git a/docs/src/views/AutoFocusZone.tsx b/docs/src/views/AutoFocusZone.tsx index 92f10019ae..5465d2a678 100644 --- a/docs/src/views/AutoFocusZone.tsx +++ b/docs/src/views/AutoFocusZone.tsx @@ -16,25 +16,24 @@ export default () => (
    Overview

    - {code('AutoFocusZone')} can wrap a component and is used to grab focus and put it to an inner - element when the component mounts. For example, when it is needed to focus an inner element in - the Popup when it mounts. + {code('AutoFocusZone')} can be integrated into {code('Popup')} component and is used to grab + focus and put it to an inner element when the component mounts. This can be achieved by + specifying {code('autoFocus')} prop on the {code('Popup')}.

    If you need both - grabbing the focus and trap the focus in the component - use{' '} - FocusTrapZone. + {code('trapFocus')} prop for {code('Popup')} + to integrate FocusTrapZone.

    Usage

    - In Stardust, {code('AutoFocusZone')} is applied through accessibility behavior, as for{' '} - {code('FocusZone')} and - {code('FocusTrapZone')}. To enable auto focus for a component, in the behavior set prop{' '} - {code('autoFocus')} to {code('true')} - with default settings or set an object with desired values for auto focus zone props.{' '} - Read more about Accessibility Behaviors. + In {code('Popup')}, set {code('autoFocus')} to {code('true')} with default settings or set an + object with desired values for auto focus zone props.

    +

    By default, focus will be set to the first tabbable element in the Popup:

    + `} />

    - {code('AutoFocusZone')}'s props which can be applied in accessibility behavior ( + {code('AutoFocusZone')}'s props which can be applied to {code('autoFocus')} prop ( {link( 'lookup for API on GitHub', 'https://github.com/stardust-ui/react/blob/master/packages/react/src/lib/accessibility/AutoFocusZone/AutoFocusZone.types.tsx', @@ -44,31 +43,15 @@ export default () => (

    Override {code('AutoFocusZone')} settings

    - To be able to add/override {code('AutoFocusZone')} props already set for a component, it is - needed to override or create a new accessibility behavior. + For example, we want to specify the focusable selector for Popup with auto focus. On Popup + mount, focus will go to the element matched to that selector. For that purpose, we can specify{' '} + {code('firstFocusableSelector')}.

    -

    - For example, we want to specify the focusable selector for Popup with auto focus, so on Popup - mount, focus will go to the element matched to that selector. For that purpose, we can to - override {code('popupAutoFocusBehavior')} and specify {code('firstFocusableSelector')} there. -

    - { - const behavior = popupFocusTrapBehavior(props) - - behavior.autoFocus.firstFocusableSelector = ".btn-submit"; - - return behavior - } - `} - /> - And then use this new behavior by Popup component: ( - + )`} />

    Read more about:

    diff --git a/docs/src/views/FocusTrapZone.tsx b/docs/src/views/FocusTrapZone.tsx index 7a7270c67d..cac0599946 100644 --- a/docs/src/views/FocusTrapZone.tsx +++ b/docs/src/views/FocusTrapZone.tsx @@ -17,11 +17,12 @@ export default () => (
    Overview

    - {code('FocusTrapZone')} grabs the focus and traps it within an HTML element, usually a dialog - or popup. Pressing {code('TAB')} key will circle focus within the inner focusable elements of - the {code('FocusTrapZone')}. The main purpose is to block user interaction outside{' '} - {code('FocusTrapZone')} - in any way. Therefore, keyboard events are not propagated outside {code('FocusTrapZone')}. + {code('FocusTrapZone')} grabs the focus and traps it within an HTML element. Currently can be + used only in {code('Popup')} + and {code('Dialog')} components. Pressing {code('TAB')} key will circle focus within the inner + focusable elements of the {code('FocusTrapZone')}. The main purpose is to block user + interaction outside {code('FocusTrapZone')} in any way. Therefore, keyboard events are not + propagated outside {code('FocusTrapZone')}, hence {code('Popup')} or {code('Dialog')}.

    Stardust leverages Focus Trap Zone component which is based on the{' '} @@ -32,16 +33,18 @@ export default () => (

    Usage

    - Stardust applies focus trap via accessibility behavior, the same way as it's done for{' '} - FocusZone. To enable focus trap for component, it is needed, in - behavior, to set prop {code('trapFocus')} to + To apply {code('FocusTrapZone')} to {code('Popup')} set prop {code('trapFocus')} to {code('true')} with default settings or set an object with desired values for focus trap zone - props. Read more about Accessibility Behaviors.{' '} - Currently, it is used for Popup via {code('popupFocusTrapBehavior')} and Dialog via{' '} - {code('dialogBehavior')}. + props.

    + `} />

    - {code('FocusTrapZone')}'s props which can be applied in accessibility behavior ( + {code('Dialog')} component has always set {code('trapFocus')} prop to {code('true')}, but it + is also possible to override default settings by specifying object of{' '} + {code('FocusTrapZoneProps')}. +

    +

    + {code('FocusTrapZone')}'s props which can be applied to {code('trapFocus')} prop ( {link( 'lookup for API on GitHub', 'https://github.com/stardust-ui/react/blob/master/packages/react/src/lib/accessibility/FocusTrapZone/FocusTrapZone.types.tsx', @@ -50,31 +53,23 @@ export default () => (

    Override {code('FocusTrapZone')} settings
    -

    - To be able to add/override {code('FocusTrapZone')} props already set for a component, it is - needed to override or create a new accessibility behavior. -

    For example, we want to disable first focus on Popup mount, so we can control the initial - focus by ourselves. + focus by ourselves:

    { - const behavior = popupFocusTrapBehavior(props) - - behavior.trapFocus.disableFirstFocus = true; - - return behavior - } - `} + const Popup = () => ( + + )`} /> - And then use this new behavior by Popup component: +

    Same usage applies to {code('Dialog')} component:

    ( - + const Dialog = () => ( + )`} />

    Read more about:

    diff --git a/packages/react/src/components/Popup/Popup.tsx b/packages/react/src/components/Popup/Popup.tsx index 4d225f76b3..dd9f5282eb 100644 --- a/packages/react/src/components/Popup/Popup.tsx +++ b/packages/react/src/components/Popup/Popup.tsx @@ -32,12 +32,7 @@ import { } from '../../lib/positioner' import PopupContent from './PopupContent' import { popupBehavior } from '../../lib/accessibility' -import { - AutoFocusZone, - AutoFocusZoneProps, - FocusTrapZone, - FocusTrapZoneProps, -} from '../../lib/accessibility/FocusZone' +import { AutoFocusZoneProps, FocusTrapZoneProps } from '../../lib/accessibility/FocusZone' import { Accessibility } from '../../lib/accessibility/types' import { ReactAccessibilityBehavior } from '../../lib/accessibility/reactTypes' @@ -60,7 +55,7 @@ export interface PopupProps /** * Accessibility behavior if overridden by the user. * @default popupBehavior - * @available popupFocusTrapBehavior, dialogBehavior + * @available dialogBehavior * */ accessibility?: Accessibility @@ -114,6 +109,12 @@ export interface PopupProps /** Ref for Popup content DOM node. */ contentRef?: React.Ref + + /** Controls whether or not focus trap should be applied, using boolean or FocusTrapZoneProps type value. */ + trapFocus?: boolean | FocusTrapZoneProps + + /** Controls whether or not auto focus should be applied, using boolean or AutoFocusZoneProps type value. */ + autoFocus?: boolean | AutoFocusZoneProps } export interface PopupState { @@ -122,6 +123,13 @@ export interface PopupState { /** * A Popup displays additional information on top of a page. + * @accessibility + * Do set `trapFocus` if the focus needs to be trapped inside of the Popup. + * Don't use `trapFocus` for `inline` popup, as it leads to broken behavior for screen reader users. + * Beware of using `autoFocus` as it just grabs focus and do not traps it. User is able to tab out from popup, + * so consider to use `inline` prop to save a correct tab order. + * If Popup's content is lazy loaded and focus needs to be trapped inside - make sure to use state change to trigger componentDidUpdate, + * so the focus can be set correctly to the first tabbable element inside Popup or manually set focus to the element inside once content is loaded. */ export default class Popup extends AutoControlledComponent { static displayName = 'Popup' @@ -164,6 +172,8 @@ export default class Popup extends AutoControlledComponent { - const { content: propsContent, renderContent, contentRef, mountDocument, pointing } = this.props + const { + content: propsContent, + renderContent, + contentRef, + mountDocument, + pointing, + trapFocus, + autoFocus, + } = this.props const content = renderContent ? renderContent(scheduleUpdate) : propsContent const documentRef = toRefObject(mountDocument) - const popupWrapperAttributes = { - ...(rtl && { dir: 'rtl' }), - ...accessibility.attributes.popup, - ...accessibility.keyHandlers.popup, - className: popupPositionClasses, - ...this.getContentProps(), - } - - const focusTrapProps = { - ...(typeof accessibility.focusTrap === 'boolean' ? {} : accessibility.focusTrap), - ...popupWrapperAttributes, - } as FocusTrapZoneProps - - const autoFocusProps = { - ...(typeof accessibility.autoFocus === 'boolean' ? {} : accessibility.autoFocus), - ...popupWrapperAttributes, - } as AutoFocusZoneProps - - /** - * if there is no focus trap or auto focus wrapper, we should apply - * HTML attributes and positioning to popup content directly - */ - const popupContentAttributes = - accessibility.focusTrap || accessibility.autoFocus ? {} : popupWrapperAttributes - const popupContent = Popup.Content.create(content, { defaultProps: { - ...popupContentAttributes, + ...(rtl && { dir: 'rtl' }), + ...accessibility.attributes.popup, + ...accessibility.keyHandlers.popup, + className: popupPositionClasses, + ...this.getContentProps(), placement, pointing, pointerRef: this.pointerTargetRef, - unstable_wrapped: accessibility.focusTrap || accessibility.autoFocus, + trapFocus, + autoFocus, }, overrideProps: this.getContentProps, }) @@ -461,13 +476,7 @@ export default class Popup extends AutoControlledComponent - {accessibility.focusTrap ? ( - {popupContent} - ) : accessibility.autoFocus ? ( - {popupContent} - ) : ( - popupContent - )} + {popupContent} - /** - * @deprecated - * Indicates that PopupContent is wrapped with FocusZone. Do not use it, it used only for internal implementation and - * will be removed in future releases. - */ - unstable_wrapped?: boolean + /** Controls whether or not focus trap should be applied, using boolean or FocusTrapZoneProps type value. */ + trapFocus?: boolean | FocusTrapZoneProps + + /** Controls whether or not auto focus should be applied, using boolean or AutoFocusZoneProps type value. */ + autoFocus?: boolean | AutoFocusZoneProps } class PopupContent extends UIComponent> { @@ -75,7 +80,8 @@ class PopupContent extends UIComponent> { onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, pointerRef: customPropTypes.ref, - unstable_wrapped: PropTypes.bool, + trapFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), + autoFocus: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), } static defaultProps = { @@ -97,23 +103,24 @@ class PopupContent extends UIComponent> { unhandledProps, styles, }: RenderResultConfig): React.ReactNode { - const { children, content, pointing, pointerRef } = this.props - - return ( - + const { children, content, pointing, pointerRef, trapFocus, autoFocus } = this.props + + const popupContentProps: PopupContentProps = { + className: classes.root, + ...rtlTextContainer.getAttributes({ forElements: [children, content] }), + ...accessibility.attributes.root, + ...unhandledProps, + onMouseEnter: this.handleMouseEnter, + onMouseLeave: this.handleMouseLeave, + } + + const popupContent = ( + <> {pointing && ( {Box.create({}, { defaultProps: { styles: styles.pointer } })} )} - {Box.create( {}, { @@ -123,8 +130,30 @@ class PopupContent extends UIComponent> { }, }, )} - + ) + + if (trapFocus) { + const focusTrapZoneProps = { + ...popupContentProps, + ...((_.keys(trapFocus).length && trapFocus) as FocusTrapZoneProps), + as: ElementType, + } + + return {popupContent} + } + + if (autoFocus) { + const autoFocusZoneProps = { + ...popupContentProps, + ...((_.keys(autoFocus).length && autoFocus) as AutoFocusZoneProps), + as: ElementType, + } + + return {popupContent} + } + + return {popupContent} } } diff --git a/packages/react/src/components/Toolbar/ToolbarItem.tsx b/packages/react/src/components/Toolbar/ToolbarItem.tsx index 9f5b46bddc..a0e5632986 100644 --- a/packages/react/src/components/Toolbar/ToolbarItem.tsx +++ b/packages/react/src/components/Toolbar/ToolbarItem.tsx @@ -28,7 +28,7 @@ import { } from '../../types' import { Popper } from '../../lib/positioner' import { Accessibility } from '../../lib/accessibility/types' -import { toolbarItemBehavior, popupFocusTrapBehavior } from '../../lib/accessibility' +import { toolbarItemBehavior } from '../../lib/accessibility' import ToolbarMenu from './ToolbarMenu' import Icon from '../Icon/Icon' @@ -96,7 +96,7 @@ export interface ToolbarItemProps /** * Attaches a `Popup` component to the ToolbarItem. * Accepts all props as a `Popup`, except `trigger` and `children`. - * Sets `accessibility` to `popupFocusTrapBehavior` by default. + * Traps focus by default. * @see PopupProps */ popup?: Omit | string @@ -227,7 +227,7 @@ class ToolbarItem extends UIComponent, ToolbarItemS if (popup) { return Popup.create(popup, { defaultProps: { - accessibility: popupFocusTrapBehavior, + trapFocus: true, }, overrideProps: { trigger: renderedItem, diff --git a/packages/react/src/lib/accessibility/Behaviors/Dialog/dialogBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Dialog/dialogBehavior.ts index 4e4364907f..3d62a97a96 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Dialog/dialogBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Dialog/dialogBehavior.ts @@ -1,6 +1,5 @@ import { Accessibility, AccessibilityAttributes } from '../../types' -import popupFocusTrapBehavior from '../Popup/popupFocusTrapBehavior' -import { PopupBehaviorProps } from '../Popup/popupBehavior' +import popupBehavior, { PopupBehaviorProps } from '../Popup/popupBehavior' /** * @description @@ -13,10 +12,9 @@ import { PopupBehaviorProps } from '../Popup/popupBehavior' * Adds attribute 'role=dialog' to 'popup' slot. * Adds attribute 'aria-labelledby' based on the property 'aria-labelledby' to 'popup' slot. * Adds attribute 'aria-describedby' based on the property 'aria-describedby' to 'popup' slot. - * Traps focus inside component. */ const dialogBehavior: Accessibility = props => { - const behaviorData = popupFocusTrapBehavior(props) + const behaviorData = popupBehavior(props) const defaultAriaLabelledBy = getDefaultAriaLabelledBy(props) const defaultAriaDescribedBy = getDefaultAriaDescribedBy(props) @@ -24,6 +22,7 @@ const dialogBehavior: Accessibility = props => { behaviorData.attributes.popup = { ...behaviorData.attributes.popup, role: 'dialog', + 'aria-modal': true, 'aria-labelledby': defaultAriaLabelledBy || props['aria-labelledby'], 'aria-describedby': defaultAriaDescribedBy || props['aria-describedby'], } diff --git a/packages/react/src/lib/accessibility/Behaviors/Popup/popupAutoFocusBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Popup/popupAutoFocusBehavior.ts deleted file mode 100644 index aaa7dd6b9c..0000000000 --- a/packages/react/src/lib/accessibility/Behaviors/Popup/popupAutoFocusBehavior.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Accessibility } from '../../types' -import popupBehavior, { PopupBehaviorProps } from './popupBehavior' - -/** - * @description - * Adds tabIndex='0' to 'trigger' slot, if it is not tabbable element and no tabIndex attribute provided. - * - * @specification - * Adds attribute 'aria-disabled=true' to 'trigger' slot if 'disabled' property is true. Does not set the attribute otherwise. - * Automatically focus the first focusable element inside component. - */ -const popupAutoFocusBehavior: Accessibility = props => ({ - ...popupBehavior(props), - autoFocus: true, -}) - -export default popupAutoFocusBehavior diff --git a/packages/react/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts index 20006950e0..08f279ae52 100644 --- a/packages/react/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts +++ b/packages/react/src/lib/accessibility/Behaviors/Popup/popupBehavior.ts @@ -9,7 +9,8 @@ import { PopupEvents, PopupEventsArray } from '../../../../components/Popup/Popu * * @specification * Adds attribute 'aria-disabled=true' to 'trigger' slot if 'disabled' property is true. Does not set the attribute otherwise. - * Adds attribute 'role=complementary' to 'popup' slot. + * Adds attribute 'role=dialog' to 'popup' slot if 'trapFocus' property is true. Sets the attribute to 'complementary' otherwise. + * Adds attribute 'aria-modal=true' to 'popup' slot if 'trapFocus' property is true. Does not set the attribute otherwise. */ const popupBehavior: Accessibility = props => { const onAsArray = _.isArray(props.on) ? props.on : [props.on] @@ -20,7 +21,8 @@ const popupBehavior: Accessibility = props => { 'aria-disabled': props.disabled, }, popup: { - role: 'complementary', + role: props.trapFocus ? 'dialog' : 'complementary', + 'aria-modal': props.trapFocus ? true : undefined, }, }, keyActions: { @@ -84,6 +86,8 @@ const getAriaAttributeFromProps = ( export default popupBehavior export type PopupBehaviorProps = { + /** Indicates if focus should be trapped inside popup's container. */ + trapFocus?: boolean | object /** Events triggering the popup. */ on?: PopupEvents | PopupEventsArray /** Indicates if popup's trigger is disabled. */ diff --git a/packages/react/src/lib/accessibility/Behaviors/Popup/popupFocusTrapBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Popup/popupFocusTrapBehavior.ts deleted file mode 100644 index 3f7a78cb3d..0000000000 --- a/packages/react/src/lib/accessibility/Behaviors/Popup/popupFocusTrapBehavior.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Accessibility } from '../../types' -import popupBehavior, { PopupBehaviorProps } from './popupBehavior' - -/** - * @description - * Adds tabIndex='0' to 'trigger' slot, if it is not tabbable element and no tabIndex attribute provided. - * - * @specification - * Adds attribute 'aria-disabled=true' to 'trigger' slot if 'disabled' property is true. Does not set the attribute otherwise. - * Adds attribute 'aria-modal=true' to 'popup' slot. - * Adds attribute 'role=dialog' to 'popup' slot. - * Traps focus inside component. - */ -const popupFocusTrapBehavior: Accessibility = props => { - const behaviorData = popupBehavior(props) - behaviorData.attributes.popup = { - ...behaviorData.attributes.popup, - 'aria-modal': true, - role: 'dialog', - } - behaviorData.focusTrap = true - - return behaviorData -} - -export default popupFocusTrapBehavior diff --git a/packages/react/src/lib/accessibility/index.ts b/packages/react/src/lib/accessibility/index.ts index 3e5d6b26de..0480f347e3 100644 --- a/packages/react/src/lib/accessibility/index.ts +++ b/packages/react/src/lib/accessibility/index.ts @@ -33,8 +33,6 @@ export { export { default as radioGroupBehavior } from './Behaviors/Radio/radioGroupBehavior' export { default as radioGroupItemBehavior } from './Behaviors/Radio/radioGroupItemBehavior' export { default as popupBehavior } from './Behaviors/Popup/popupBehavior' -export { default as popupFocusTrapBehavior } from './Behaviors/Popup/popupFocusTrapBehavior' -export { default as popupAutoFocusBehavior } from './Behaviors/Popup/popupAutoFocusBehavior' export { default as chatBehavior } from './Behaviors/Chat/chatBehavior' export { default as chatMessageBehavior } from './Behaviors/Chat/chatMessageBehavior' export { default as gridBehavior } from './Behaviors/Grid/gridBehavior' diff --git a/packages/react/src/lib/accessibility/types.ts b/packages/react/src/lib/accessibility/types.ts index 317e8232be..5d6d7c6bfe 100644 --- a/packages/react/src/lib/accessibility/types.ts +++ b/packages/react/src/lib/accessibility/types.ts @@ -1,9 +1,4 @@ -import { - FocusTrapZoneProps, - FocusZoneProps, - AutoFocusZoneProps, - IS_FOCUSABLE_ATTRIBUTE, -} from './FocusZone' +import { FocusZoneProps, IS_FOCUSABLE_ATTRIBUTE } from './FocusZone' export type AriaWidgetRole = | 'button' @@ -152,17 +147,12 @@ export type FocusZoneDefinition = { props?: FocusZoneProps } -export type FocusTrapDefinition = FocusTrapZoneProps | boolean -export type AutoFocusZoneDefinition = AutoFocusZoneProps | boolean - export type KeyActions = { [partName: string]: { [actionName: string]: KeyAction } } export interface AccessibilityDefinition { attributes?: AccessibilityAttributesBySlot keyActions?: KeyActions focusZone?: FocusZoneDefinition - focusTrap?: FocusTrapDefinition - autoFocus?: AutoFocusZoneDefinition childBehaviors?: { [childBehaviorSlot: string]: Accessibility } } diff --git a/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts b/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts index 3a705b00e0..2281289e03 100644 --- a/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts +++ b/packages/react/src/themes/teams/components/Popup/popupContentStyles.ts @@ -10,12 +10,6 @@ const popupContentStyles: ComponentSlotStylesInput { - test('adds tabIndex=0 to trigger if element is not tabbable and tabIndex attribute is not provided', () => { - const expectedResult = popupAutoFocusBehavior({ trigger:
    }) - expect(expectedResult.attributes.trigger.tabIndex).toEqual(0) - }) - - test('adds tabIndex attribute with value passed as prop', () => { - const expectedResult = popupAutoFocusBehavior({ trigger:
    }) - expect(expectedResult.attributes.trigger.tabIndex).toEqual(-1) - }) - - test('does not add tabIndex if element is already tabbable', () => { - const expectedResult = popupAutoFocusBehavior({ trigger: