diff --git a/CHANGELOG.md b/CHANGELOG.md
index f350e8a8b8..8ba58659c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `alert`, `info`, `share-alt` and `microsoft-stream` icons to Teams theme @marst89 ([#1544](https://github.com/stardust-ui/react/pull/1544))
- Add `custom` `kind` for `items` in `Toolbar` component @miroslavstastny ([#1558](https://github.com/stardust-ui/react/pull/1558))
- Add `hand` icon to Teams theme @t-proko ([#1567](https://github.com/stardust-ui/react/pull/1567))
+- Add accessibility attributes and keyboard handlers for `Tooltip` @sophieH29 ([#1575](https://github.com/stardust-ui/react/pull/1575))
### Documentation
- Ensure docs content doesn't overlap with sidebar @kuzhelov ([#1568](https://github.com/stardust-ui/react/pull/1568))
diff --git a/docs/src/examples/components/Tooltip/Types/TooltipExample.shorthand.tsx b/docs/src/examples/components/Tooltip/Types/TooltipExample.shorthand.tsx
index f2df6671eb..a2d9fda391 100644
--- a/docs/src/examples/components/Tooltip/Types/TooltipExample.shorthand.tsx
+++ b/docs/src/examples/components/Tooltip/Types/TooltipExample.shorthand.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import { Button, Tooltip } from '@stardust-ui/react'
const TooltipExample = () => (
- } content="Hello from tooltip!" />
+ } content="Hello from tooltip!" />
)
export default TooltipExample
diff --git a/docs/src/examples/components/Tooltip/Types/TooltipExample.tsx b/docs/src/examples/components/Tooltip/Types/TooltipExample.tsx
index 5a1c4b42bc..6de5f89ff3 100644
--- a/docs/src/examples/components/Tooltip/Types/TooltipExample.tsx
+++ b/docs/src/examples/components/Tooltip/Types/TooltipExample.tsx
@@ -3,7 +3,7 @@ import { Button, Tooltip } from '@stardust-ui/react'
const TooltipExample = () => (
-
+
)
diff --git a/packages/react/src/components/Dialog/Dialog.tsx b/packages/react/src/components/Dialog/Dialog.tsx
index f457772a28..cea2ec0b33 100644
--- a/packages/react/src/components/Dialog/Dialog.tsx
+++ b/packages/react/src/components/Dialog/Dialog.tsx
@@ -12,6 +12,7 @@ import {
AutoControlledComponent,
doesNodeContainClick,
applyAccessibilityKeyHandlers,
+ getOrGenerateIdFromShorthand,
} from '../../lib'
import { dialogBehavior } from '../../lib/accessibility'
import { FocusTrapZoneProps } from '../../lib/accessibility/FocusZone'
@@ -23,26 +24,6 @@ import Header from '../Header/Header'
import Portal from '../Portal/Portal'
import Flex from '../Flex/Flex'
-const getOrGenerateIdFromShorthand = (
- slotName: string,
- value: ShorthandValue,
- currentValue?: string,
-): string | undefined => {
- if (_.isNil(value)) {
- return undefined
- }
-
- if (React.isValidElement(value)) {
- return (value as React.ReactElement<{ id?: string }>).props.id
- }
-
- if (_.isPlainObject(value)) {
- return (value as Record).id
- }
-
- return currentValue || _.uniqueId(`dialog-${slotName}-`)
-}
-
export interface DialogSlotClassNames {
header: string
content: string
@@ -174,8 +155,8 @@ class Dialog extends AutoControlledComponent, DialogStat
state: DialogState,
): Partial {
return {
- contentId: getOrGenerateIdFromShorthand('content', props.content, state.contentId),
- headerId: getOrGenerateIdFromShorthand('header', props.header, state.headerId),
+ contentId: getOrGenerateIdFromShorthand('dialog-content-', props.content, state.contentId),
+ headerId: getOrGenerateIdFromShorthand('dialog-header-', props.header, state.headerId),
}
}
diff --git a/packages/react/src/components/Tooltip/Tooltip.tsx b/packages/react/src/components/Tooltip/Tooltip.tsx
index 7bf45f62e5..42437b77bb 100644
--- a/packages/react/src/components/Tooltip/Tooltip.tsx
+++ b/packages/react/src/components/Tooltip/Tooltip.tsx
@@ -17,6 +17,7 @@ import {
commonPropTypes,
isFromKeyboard,
setWhatInputSource,
+ getOrGenerateIdFromShorthand,
} from '../../lib'
import { ShorthandValue, Props } from '../../types'
import {
@@ -27,6 +28,7 @@ import {
PopperChildrenProps,
} from '../../lib/positioner'
import TooltipContent from './TooltipContent'
+import { tooltipBehavior } from '../../lib/accessibility'
import { Accessibility } from '../../lib/accessibility/types'
import { ReactAccessibilityBehavior } from '../../lib/accessibility/reactTypes'
@@ -36,6 +38,7 @@ export interface TooltipSlotClassNames {
export interface TooltipState {
open: boolean
+ contentId: string
}
export interface TooltipProps
@@ -116,6 +119,7 @@ export default class Tooltip extends AutoControlledComponent()
triggerRef = React.createRef()
contentRef = React.createRef()
-
closeTimeoutId
+ actionHandlers = {
+ close: e => {
+ this.setTooltipOpen(false, e)
+ e.stopPropagation()
+ e.preventDefault()
+ },
+ }
+
+ static getAutoControlledStateFromProps(
+ props: TooltipProps,
+ state: TooltipState,
+ ): Partial {
+ return {
+ contentId: getOrGenerateIdFromShorthand('tooltip-content-', props.content, state.contentId),
+ }
+ }
+
renderComponent({
classes,
rtl,
diff --git a/packages/react/src/lib/accessibility/Behaviors/Tooltip/tooltipBehavior.ts b/packages/react/src/lib/accessibility/Behaviors/Tooltip/tooltipBehavior.ts
new file mode 100644
index 0000000000..d43138b4d7
--- /dev/null
+++ b/packages/react/src/lib/accessibility/Behaviors/Tooltip/tooltipBehavior.ts
@@ -0,0 +1,55 @@
+import { Accessibility } from '../../types'
+import * as keyboardKey from 'keyboard-key'
+
+/**
+ * @description
+ * Implements ARIA Tooltip design pattern.
+ *
+ * @specification
+ * Adds attribute 'role=tooltip' to 'tooltip' slot.
+ * Adds attribute 'aria-hidden=false' to 'tooltip' slot if 'open' property is true. Sets the attribute to 'true' otherwise.
+ * Adds attribute 'aria-describedby' based on the property 'aria-describedby' to 'trigger' slot.
+ * Triggers 'close' action with 'Escape' on 'trigger'.
+ */
+const tooltipBehavior: Accessibility = props => {
+ const defaultAriaDescribedBy = getDefaultAriaDescribedBy(props)
+
+ return {
+ attributes: {
+ trigger: {
+ 'aria-describedby': defaultAriaDescribedBy || props['aria-describedby'],
+ },
+ tooltip: {
+ role: 'tooltip',
+ id: defaultAriaDescribedBy,
+ 'aria-hidden': !props.open,
+ },
+ },
+ keyActions: {
+ trigger: {
+ close: {
+ keyCombinations: [{ keyCode: keyboardKey.Escape }],
+ },
+ },
+ },
+ }
+}
+
+export default tooltipBehavior
+
+/**
+ * Returns the element id of the tooltip, it is used when user does not provide aria-describedby as props.
+ */
+const getDefaultAriaDescribedBy = (props: TooltipBehaviorProps) => {
+ if (props['aria-describedby']) {
+ return undefined
+ }
+ return props.contentId
+}
+
+export type TooltipBehaviorProps = {
+ /** If tooltip is visible. */
+ open: boolean
+ /** Tooltip's container id. */
+ contentId: string
+}
diff --git a/packages/react/src/lib/accessibility/index.ts b/packages/react/src/lib/accessibility/index.ts
index c2970f16cf..3e5d6b26de 100644
--- a/packages/react/src/lib/accessibility/index.ts
+++ b/packages/react/src/lib/accessibility/index.ts
@@ -49,3 +49,4 @@ export { default as accordionBehavior } from './Behaviors/Accordion/accordionBeh
export { default as accordionTitleBehavior } from './Behaviors/Accordion/accordionTitleBehavior'
export { default as accordionContentBehavior } from './Behaviors/Accordion/accordionContentBehavior'
export { default as checkboxBehavior } from './Behaviors/Checkbox/checkboxBehavior'
+export { default as tooltipBehavior } from './Behaviors/Tooltip/tooltipBehavior'
diff --git a/packages/react/src/lib/getOrGenerateIdFromShorthand.ts b/packages/react/src/lib/getOrGenerateIdFromShorthand.ts
new file mode 100644
index 0000000000..875dce59b4
--- /dev/null
+++ b/packages/react/src/lib/getOrGenerateIdFromShorthand.ts
@@ -0,0 +1,25 @@
+import * as React from 'react'
+import * as _ from 'lodash'
+import { ShorthandValue } from '../types'
+
+const getOrGenerateIdFromShorthand = (
+ prefix: string,
+ value: ShorthandValue,
+ currentValue?: string,
+): string | undefined => {
+ if (_.isNil(value)) {
+ return undefined
+ }
+
+ if (React.isValidElement(value)) {
+ return (value as React.ReactElement<{ id?: string }>).props.id
+ }
+
+ if (_.isPlainObject(value)) {
+ return (value as Record).id
+ }
+
+ return currentValue || _.uniqueId(prefix)
+}
+
+export default getOrGenerateIdFromShorthand
diff --git a/packages/react/src/lib/index.ts b/packages/react/src/lib/index.ts
index 63095471cd..a5f6ef3eda 100644
--- a/packages/react/src/lib/index.ts
+++ b/packages/react/src/lib/index.ts
@@ -8,6 +8,7 @@ export { default as felaRenderer } from './felaRenderer'
export { default as toCompactArray } from './toCompactArray'
export { default as rtlTextContainer } from './rtlTextContainer'
export { default as stringLiteralsArray } from './stringLiteralsArray'
+export { default as getOrGenerateIdFromShorthand } from './getOrGenerateIdFromShorthand'
export * from './factories'
export { default as callable } from './callable'
diff --git a/packages/react/test/specs/behaviors/behavior-test.tsx b/packages/react/test/specs/behaviors/behavior-test.tsx
index ccb2d0ba3e..dd800b5f66 100644
--- a/packages/react/test/specs/behaviors/behavior-test.tsx
+++ b/packages/react/test/specs/behaviors/behavior-test.tsx
@@ -48,6 +48,7 @@ import {
toolbarItemBehavior,
toolbarRadioGroupBehavior,
toolbarRadioGroupItemBehavior,
+ tooltipBehavior,
} from 'src/lib/accessibility'
import { TestHelper } from './testHelper'
import definitions from './testDefinitions'
@@ -100,5 +101,6 @@ testHelper.addBehavior('toolbarBehavior', toolbarBehavior)
testHelper.addBehavior('toolbarItemBehavior', toolbarItemBehavior)
testHelper.addBehavior('toolbarRadioGroupBehavior', toolbarRadioGroupBehavior)
testHelper.addBehavior('toolbarRadioGroupItemBehavior', toolbarRadioGroupItemBehavior)
+testHelper.addBehavior('tooltipBehavior', tooltipBehavior)
testHelper.run(behaviorMenuItems)
diff --git a/packages/react/test/specs/components/Tooltip/Tooltip-test.tsx b/packages/react/test/specs/components/Tooltip/Tooltip-test.tsx
index 6e528389e6..d91cc65ecc 100644
--- a/packages/react/test/specs/components/Tooltip/Tooltip-test.tsx
+++ b/packages/react/test/specs/components/Tooltip/Tooltip-test.tsx
@@ -1,15 +1,38 @@
import * as React from 'react'
import Tooltip from 'src/components/Tooltip/Tooltip'
+import Button from 'src/components/Button/Button'
-import { mountWithProvider } from '../../../utils'
+import { mountWithProvider, findIntrinsicElement } from '../../../utils'
describe('Tooltip', () => {
+ describe('content', () => {
+ it('uses "id" if "content" with "id" is passed', () => {
+ const contentId = 'element-id'
+
+ const wrapper = mountWithProvider(
+ } content={{ id: contentId }} />,
+ )
+ const content = findIntrinsicElement(wrapper, `.${Tooltip.slotClassNames.content}`)
+
+ expect(content.prop('id')).toBe(contentId)
+ })
+
+ it('uses computed "id" if "content" is passed without "id"', () => {
+ const wrapper = mountWithProvider(
+ } content="Welcome" />,
+ )
+ const content = findIntrinsicElement(wrapper, `.${Tooltip.slotClassNames.content}`)
+
+ expect(content.prop('id')).toMatch(/tooltip-content-\d+/)
+ })
+ })
+
describe('onOpenChange', () => {
test('is called on hover', () => {
const onOpenChange = jest.fn()
- mountWithProvider(} content="Hi" onOpenChange={onOpenChange} />)
+ mountWithProvider(} content="Hi" onOpenChange={onOpenChange} />)
.find('button')
.simulate('mouseEnter')
@@ -25,7 +48,7 @@ describe('Tooltip', () => {
const onOpenChange = jest.fn()
mountWithProvider(
- } content="Hi" onOpenChange={onOpenChange} />,
+ } content="Hi" onOpenChange={onOpenChange} />,
)
.find('button')
.simulate('mouseEnter')