Skip to content
This repository was archived by the owner on Mar 4, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react'
import { Button, Tooltip } from '@stardust-ui/react'

const TooltipExample = () => (
<Tooltip trigger={<Button icon="expand" />} content="Hello from tooltip!" />
<Tooltip trigger={<Button content="Click me!" />} content="Hello from tooltip!" />
)

export default TooltipExample
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Button, Tooltip } from '@stardust-ui/react'

const TooltipExample = () => (
<Tooltip content="Hello from tooltip!">
<Button icon="expand" />
<Button>Click me!</Button>
</Tooltip>
)

Expand Down
25 changes: 3 additions & 22 deletions packages/react/src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AutoControlledComponent,
doesNodeContainClick,
applyAccessibilityKeyHandlers,
getOrGenerateIdFromShorthand,
} from '../../lib'
import { dialogBehavior } from '../../lib/accessibility'
import { FocusTrapZoneProps } from '../../lib/accessibility/FocusZone'
Expand All @@ -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<string, any>).id
}

return currentValue || _.uniqueId(`dialog-${slotName}-`)
}

export interface DialogSlotClassNames {
header: string
content: string
Expand Down Expand Up @@ -174,8 +155,8 @@ class Dialog extends AutoControlledComponent<WithAsProp<DialogProps>, DialogStat
state: DialogState,
): Partial<DialogState> {
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),
}
}

Expand Down
22 changes: 21 additions & 1 deletion packages/react/src/components/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
commonPropTypes,
isFromKeyboard,
setWhatInputSource,
getOrGenerateIdFromShorthand,
} from '../../lib'
import { ShorthandValue, Props } from '../../types'
import {
Expand All @@ -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'

Expand All @@ -36,6 +38,7 @@ export interface TooltipSlotClassNames {

export interface TooltipState {
open: boolean
contentId: string
}

export interface TooltipProps
Expand Down Expand Up @@ -116,16 +119,33 @@ export default class Tooltip extends AutoControlledComponent<TooltipProps, Toolt
position: 'above',
mouseLeaveDelay: 500,
pointing: true,
accessibility: tooltipBehavior,
}

static autoControlledProps = ['open']

pointerTargetRef = React.createRef<HTMLElement>()
triggerRef = React.createRef<HTMLElement>()
contentRef = React.createRef<HTMLElement>()

closeTimeoutId

actionHandlers = {
close: e => {
this.setTooltipOpen(false, e)
e.stopPropagation()
e.preventDefault()
},
}

static getAutoControlledStateFromProps(
props: TooltipProps,
state: TooltipState,
): Partial<TooltipState> {
return {
contentId: getOrGenerateIdFromShorthand('tooltip-content-', props.content, state.contentId),
}
}

renderComponent({
classes,
rtl,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TooltipBehaviorProps> = 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
}
1 change: 1 addition & 0 deletions packages/react/src/lib/accessibility/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
25 changes: 25 additions & 0 deletions packages/react/src/lib/getOrGenerateIdFromShorthand.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>).id
}

return currentValue || _.uniqueId(prefix)
}

export default getOrGenerateIdFromShorthand
1 change: 1 addition & 0 deletions packages/react/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions packages/react/test/specs/behaviors/behavior-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
toolbarItemBehavior,
toolbarRadioGroupBehavior,
toolbarRadioGroupItemBehavior,
tooltipBehavior,
} from 'src/lib/accessibility'
import { TestHelper } from './testHelper'
import definitions from './testDefinitions'
Expand Down Expand Up @@ -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)
29 changes: 26 additions & 3 deletions packages/react/test/specs/components/Tooltip/Tooltip-test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Tooltip defaultOpen trigger={<Button />} 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(
<Tooltip defaultOpen trigger={<Button />} 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(<Tooltip trigger={<button />} content="Hi" onOpenChange={onOpenChange} />)
mountWithProvider(<Tooltip trigger={<Button />} content="Hi" onOpenChange={onOpenChange} />)
.find('button')
.simulate('mouseEnter')

Expand All @@ -25,7 +48,7 @@ describe('Tooltip', () => {
const onOpenChange = jest.fn()

mountWithProvider(
<Tooltip open={false} trigger={<button />} content="Hi" onOpenChange={onOpenChange} />,
<Tooltip open={false} trigger={<Button />} content="Hi" onOpenChange={onOpenChange} />,
)
.find('button')
.simulate('mouseEnter')
Expand Down