diff --git a/.changeset/iconbutton-default-tooltip.md b/.changeset/iconbutton-default-tooltip.md
new file mode 100644
index 00000000000..7fb2b407016
--- /dev/null
+++ b/.changeset/iconbutton-default-tooltip.md
@@ -0,0 +1,5 @@
+---
+'@primer/react': minor
+---
+
+[IconButton](https://primer.style/react/IconButton) now has a tooltip by default, it can be [customised by wrapping in a Tooltip](https://primer.style/react/IconButton#customize-description--tooltip-text) ([#2006](https://github.com/primer/react/pull/2006))
diff --git a/.changeset/improved-tooltip.md b/.changeset/improved-tooltip.md
new file mode 100644
index 00000000000..acaea20e976
--- /dev/null
+++ b/.changeset/improved-tooltip.md
@@ -0,0 +1,6 @@
+---
+'@primer/react': patch
+---
+
+Accessibility and position fixes (backward compatible) for Tooltip ([#2006](https://github.com/primer/react/pull/2006))
+
diff --git a/docs/content/IconButton.mdx b/docs/content/IconButton.mdx
index 2af6b14024b..0eceefea19b 100644
--- a/docs/content/IconButton.mdx
+++ b/docs/content/IconButton.mdx
@@ -35,6 +35,16 @@ A separate component called `IconButton` is used if the action shows only an ico
>
```
+### Customize description / tooltip text
+
+To add description for the button, wrap `IconButton` in a `Tooltip`. Make sure you pass `aria-label` to the button as well.
+
+```jsx live
+
+
+
+```
+
## API reference
Native `` HTML attributes are forwarded to the underlying React `button` component and are not listed below.
diff --git a/docs/content/Tooltip.md b/docs/content/Tooltip.md
deleted file mode 100644
index a8aea00bfa1..00000000000
--- a/docs/content/Tooltip.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-componentId: tooltip
-title: Tooltip
-status: Alpha
----
-
-The Tooltip component adds a tooltip to add context to elements on the page.
-
-**_⚠️ Usage warning! ⚠️_**
-
-Tooltips as a UI pattern should be our last resort for conveying information because it is hidden by default and often with zero or little visual indicator of its existence.
-
-Before adding a tooltip, please consider: Is this information essential and necessary? Can the UI be made clearer? Can the information be shown on the page by default?
-
-**Attention:** we use aria-label for tooltip contents, because it is crucial that they are accessible to screen reader users. However, aria-label replaces the text content of an element in screen readers, so only use Tooltip on elements with no existing text content, or consider using `title` for supplemental information.
-
-## Default example
-
-```jsx live
-
- Text with a tooltip
-
-```
-
-## Component props
-
-| Name | Type | Default | Description |
-| :--------- | :---------------- | :-----: | :------------------------------------------------------------------------------------------------------------------ |
-| align | String | | Can be either `left` or `right`. |
-| direction | String | | Can be one of `n`, `ne`, `e`, `se`, `s`, `sw`, `w`, `nw`. Sets where the tooltip renders in relation to the target. |
-| noDelay | Boolean | | When set to `true`, tooltip appears without any delay |
-| aria-label | String | | Text used in `aria-label` (for accessibility). |
-| wrap | Boolean | | Use `true` to allow text within tooltip to wrap. |
-| sx | SystemStyleObject | {} | Style to be applied to the component |
diff --git a/docs/content/Tooltip.mdx b/docs/content/Tooltip.mdx
new file mode 100644
index 00000000000..1f6b49209cc
--- /dev/null
+++ b/docs/content/Tooltip.mdx
@@ -0,0 +1,194 @@
+---
+componentId: tooltip
+title: Tooltip
+status: Alpha
+source: https://github.com/primer/react/tree/main/src/Tooltip
+storybook: '/react/storybook?path=/story/composite-components-tooltip'
+description: Use tooltips to add context to elements on the page.
+---
+
+Tooltip only appears on mouse hover or keyboard focus and contain a label or description text. Use tooltips sparingly and as a last resort. [Consider these alternatives](https://primer.style/design/accessibility/tooltip-alternatives).
+
+import {Tooltip, IconButton, Button} from '@primer/react'
+import {BellIcon, MentionIcon} from '@primer/octicons-react'
+import InlineCode from '@primer/gatsby-theme-doctocat/src/components/inline-code'
+
+
+
+
+
+
+
+When using a tooltip, follow the provided guidelines to avoid accessibility issues:
+
+- Tooltip text should be brief and to the point.
+- Tooltips should contain only **non-essential text**. Tooltips can easily be missed and are not accessible on touch devices so never use tooltips to convey critical information.
+
+## Examples
+
+### As a description for icon-only button
+
+If the tooltip content provides supplementary description, wrap the target in a `Tooltip`. The trigger element should also have a concise accessible label via `aria-label`.
+
+```jsx live
+
+
+
+```
+
+### As a description for a button with visible label
+
+```jsx live
+
+ Save
+
+```
+
+### With direction
+
+Set direction of tooltip with `direction`. The tooltip is responsive and will automatically adjust direction to avoid cutting off.
+
+```jsx live
+
+
+
+ North west
+
+
+ North
+
+
+ North east
+
+
+
+
+ East
+
+
+ West
+
+
+
+
+ South west
+
+
+ South
+
+
+ South east
+
+
+
+```
+
+## Props
+
+### Tooltip
+
+
+
+
+
+
+ Use text instead
+ >
+ }
+ />
+
+ Use aria-describedby or aria-labelledby
+ >
+ }
+ />
+
+
+
+ When set to true , tooltip appears without any delay
+ >
+ }
+ />
+
+ Use to allow text within tooltip to wrap. Deprecated: always set to true now.
+ >
+ }
+ />
+
+
+
+## Status
+
+
+
+## Further reading
+
+- [Tooltip alternatives](https://primer.style/design/accessibility/tooltip-alternatives)
+
+## Related components
+
+- [IconButton](/IconButton)
diff --git a/package-lock.json b/package-lock.json
index cb5ec85d317..ceb0be69e41 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,7 @@
"version": "35.2.1",
"license": "MIT",
"dependencies": {
- "@primer/behaviors": "1.1.1",
+ "@primer/behaviors": "^1.1.3",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.6.0",
"@radix-ui/react-polymorphic": "0.0.14",
@@ -5588,9 +5588,9 @@
}
},
"node_modules/@primer/behaviors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.1.tgz",
- "integrity": "sha512-wvF1PYjyxKNTr6+5w4uR5Gkz53t1fsRDgKjWxDKk7wmlh0cwiILBo4dDFjjVhWRF1mBSjaIxxJGB4WGaP7ct2Q=="
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.3.tgz",
+ "integrity": "sha512-WpCcjAkXG7Lv3ZbaCUgASWKHnCi/pmuSEiyTmHHb6f5xhwk1mliixNL5ZZHtDN6RCcT3VnXUsyek4GopG2lbZQ=="
},
"node_modules/@primer/octicons-react": {
"version": "16.1.1",
@@ -38627,9 +38627,9 @@
"dev": true
},
"@primer/behaviors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.1.tgz",
- "integrity": "sha512-wvF1PYjyxKNTr6+5w4uR5Gkz53t1fsRDgKjWxDKk7wmlh0cwiILBo4dDFjjVhWRF1mBSjaIxxJGB4WGaP7ct2Q=="
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.3.tgz",
+ "integrity": "sha512-WpCcjAkXG7Lv3ZbaCUgASWKHnCi/pmuSEiyTmHHb6f5xhwk1mliixNL5ZZHtDN6RCcT3VnXUsyek4GopG2lbZQ=="
},
"@primer/octicons-react": {
"version": "16.1.1",
diff --git a/package.json b/package.json
index c847db92cd2..45b1490df10 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
"npm": ">=7"
},
"dependencies": {
- "@primer/behaviors": "1.1.1",
+ "@primer/behaviors": "^1.1.3",
"@primer/octicons-react": "16.1.1",
"@primer/primitives": "7.6.0",
"@radix-ui/react-polymorphic": "0.0.14",
diff --git a/src/Button/Button.stories.tsx b/src/Button/Button.stories.tsx
index 800915dc3a9..0f55c557a84 100644
--- a/src/Button/Button.stories.tsx
+++ b/src/Button/Button.stories.tsx
@@ -1,9 +1,10 @@
-import {EyeClosedIcon, EyeIcon, SearchIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react'
+import {BellIcon, EyeClosedIcon, EyeIcon, SearchIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react'
import {Meta} from '@storybook/react'
import React, {useState} from 'react'
import {Button, ButtonProps, IconButton} from '.'
import {BaseStyles, ThemeProvider} from '..'
import Box from '../Box'
+import {Tooltip} from '../Tooltip'
export default {
title: 'Composite components/Button',
@@ -93,6 +94,35 @@ export const iconButton = ({...args}: ButtonProps) => {
)
}
+export const iconButtonWithTooltip = ({...args}: ButtonProps) => {
+ return (
+ <>
+
+ Default tooltip
+
+
+
+ Custom tooltip text
+
+
+
+
+
+ Custom tooltip direction
+
+
+
+
+
+ Disable tooltip
+
+
+
+
+ >
+ )
+}
+
export const WatchCounterButton = ({...args}: ButtonProps) => {
const [count, setCount] = useState(0)
return (
diff --git a/src/Button/IconButton.tsx b/src/Button/IconButton.tsx
index 401650829fc..e4e13a4758d 100644
--- a/src/Button/IconButton.tsx
+++ b/src/Button/IconButton.tsx
@@ -4,22 +4,51 @@ import {useTheme} from '../ThemeProvider'
import Box from '../Box'
import {IconButtonProps, StyledButton} from './types'
import {getBaseStyles, getSizeStyles, getVariantStyles} from './styles'
+import {Tooltip, TooltipContext} from '../Tooltip'
const IconButton = forwardRef((props, forwardedRef): JSX.Element => {
- const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, ...rest} = props
+ const {
+ variant = 'default',
+ size = 'medium',
+ sx: sxProp = {},
+ icon: Icon,
+ disableTooltip = false,
+ 'aria-label': ariaLabel,
+ ...rest
+ } = props
const {theme} = useTheme()
+
const sxStyles = merge.all([
getBaseStyles(theme),
getSizeStyles(size, variant, true),
getVariantStyles(variant, theme),
sxProp as SxProp
])
+
+ // If button is already wrapped in a Tooltip,
+ // do not add another.
+ const {tooltipId} = React.useContext(TooltipContext)
+
+ if (tooltipId || disableTooltip) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ // use Tooltip with type=label and skip aria-label on button
+ // because the aria-labelledby is provided by the tooltip
return (
-
-
-
-
-
+
+
+
+
+
+
+
)
})
diff --git a/src/Button/types.ts b/src/Button/types.ts
index 7568bd3910d..147e1f7c812 100644
--- a/src/Button/types.ts
+++ b/src/Button/types.ts
@@ -48,6 +48,8 @@ export type ButtonProps = {
export type IconButtonProps = ButtonA11yProps & {
icon: React.FunctionComponent
+ 'aria-label': string
+ disableTooltip?: boolean
} & ButtonBaseProps
// adopted from React.AnchorHTMLAttributes
diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx
deleted file mode 100644
index 0e3baa22b2d..00000000000
--- a/src/Tooltip.tsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import classnames from 'classnames'
-import React from 'react'
-import styled from 'styled-components'
-import {get} from './constants'
-import sx, {SxProp} from './sx'
-import {ComponentProps} from './utils/types'
-
-const TooltipBase = styled.span`
- position: relative;
-
- &::before {
- position: absolute;
- z-index: 1000001;
- display: none;
- width: 0px;
- height: 0px;
- color: ${get('colors.neutral.emphasisPlus')};
- pointer-events: none;
- content: '';
- border: 6px solid transparent;
- opacity: 0;
- }
-
- &::after {
- position: absolute;
- z-index: 1000000;
- display: none;
- padding: 0.5em 0.75em;
- font: normal normal 11px/1.5 ${get('fonts.normal')};
- -webkit-font-smoothing: subpixel-antialiased;
- color: ${get('colors.fg.onEmphasis')};
- text-align: center;
- text-decoration: none;
- text-shadow: none;
- text-transform: none;
- letter-spacing: normal;
- word-wrap: break-word;
- white-space: pre;
- pointer-events: none;
- content: attr(aria-label);
- background: ${get('colors.neutral.emphasisPlus')};
- border-radius: ${get('radii.1')};
- opacity: 0;
- }
-
- // delay animation for tooltip
- @keyframes tooltip-appear {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
- }
-
- &:hover,
- &:active,
- &:focus {
- &::before,
- &::after {
- display: inline-block;
- text-decoration: none;
- animation-name: tooltip-appear;
- animation-duration: 0.1s;
- animation-fill-mode: forwards;
- animation-timing-function: ease-in;
- animation-delay: 0.4s;
- }
- }
-
- &.tooltipped-no-delay:hover,
- &.tooltipped-no-delay:active,
- &.tooltipped-no-delay:focus {
- &::before,
- &::after {
- animation-delay: 0s;
- }
- }
-
- &.tooltipped-multiline:hover,
- &.tooltipped-multiline:active,
- &.tooltipped-multiline:focus {
- &::after {
- display: table-cell;
- }
- }
-
- // Tooltipped south
- &.tooltipped-s,
- &.tooltipped-se,
- &.tooltipped-sw {
- &::after {
- top: 100%;
- right: 50%;
- margin-top: 6px;
- }
-
- &::before {
- top: auto;
- right: 50%;
- bottom: -7px;
- margin-right: -6px;
- border-bottom-color: ${get('colors.neutral.emphasisPlus')};
- }
- }
-
- &.tooltipped-se {
- &::after {
- right: auto;
- left: 50%;
- margin-left: -${get('space.3')};
- }
- }
-
- &.tooltipped-sw::after {
- margin-right: -${get('space.3')};
- }
-
- // Tooltips above the object
- &.tooltipped-n,
- &.tooltipped-ne,
- &.tooltipped-nw {
- &::after {
- right: 50%;
- bottom: 100%;
- margin-bottom: 6px;
- }
-
- &::before {
- top: -7px;
- right: 50%;
- bottom: auto;
- margin-right: -6px;
- border-top-color: ${get('colors.neutral.emphasisPlus')};
- }
- }
-
- &.tooltipped-ne {
- &::after {
- right: auto;
- left: 50%;
- margin-left: -${get('space.3')};
- }
- }
-
- &.tooltipped-nw::after {
- margin-right: -${get('space.3')};
- }
-
- // Move the tooltip body to the center of the object.
- &.tooltipped-s::after,
- &.tooltipped-n::after {
- transform: translateX(50%);
- }
-
- // Tooltipped to the left
- &.tooltipped-w {
- &::after {
- right: 100%;
- bottom: 50%;
- margin-right: 6px;
- transform: translateY(50%);
- }
-
- &::before {
- top: 50%;
- bottom: 50%;
- left: -7px;
- margin-top: -6px;
- border-left-color: ${get('colors.neutral.emphasisPlus')};
- }
- }
-
- // tooltipped to the right
- &.tooltipped-e {
- &::after {
- bottom: 50%;
- left: 100%;
- margin-left: 6px;
- transform: translateY(50%);
- }
-
- &::before {
- top: 50%;
- right: -7px;
- bottom: 50%;
- margin-top: -6px;
- border-right-color: ${get('colors.neutral.emphasisPlus')};
- }
- }
-
- &.tooltipped-multiline {
- &::after {
- width: max-content;
- max-width: 250px;
- word-wrap: break-word;
- white-space: pre-line;
- border-collapse: separate;
- }
-
- &.tooltipped-s::after,
- &.tooltipped-n::after {
- right: auto;
- left: 50%;
- transform: translateX(-50%);
- }
-
- &.tooltipped-w::after,
- &.tooltipped-e::after {
- right: 100%;
- }
- }
-
- &.tooltipped-align-right-2::after {
- right: 0;
- margin-right: 0;
- }
-
- &.tooltipped-align-right-2::before {
- right: 15px;
- }
-
- &.tooltipped-align-left-2::after {
- left: 0;
- margin-left: 0;
- }
-
- &.tooltipped-align-left-2::before {
- left: 10px;
- }
-
- ${sx};
-`
-
-export type TooltipProps = {
- direction?: 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw'
- text?: string
- noDelay?: boolean
- align?: 'left' | 'right'
- wrap?: boolean
-} & ComponentProps
-
-function Tooltip({direction = 'n', children, className, text, noDelay, align, wrap, ...rest}: TooltipProps) {
- const classes = classnames(
- className,
- `tooltipped-${direction}`,
- align && `tooltipped-align-${align}-2`,
- noDelay && 'tooltipped-no-delay',
- wrap && 'tooltipped-multiline'
- )
- return (
-
- {children}
-
- )
-}
-
-Tooltip.alignments = ['left', 'right']
-
-Tooltip.directions = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']
-
-export default Tooltip
diff --git a/src/Tooltip/index.tsx b/src/Tooltip/index.tsx
new file mode 100644
index 00000000000..bd92e1317aa
--- /dev/null
+++ b/src/Tooltip/index.tsx
@@ -0,0 +1,180 @@
+import React from 'react'
+import {useSSRSafeId} from '@react-aria/ssr'
+import type {AnchorPosition, AnchorSide, AnchorAlignment} from '@primer/behaviors'
+import Box from '../Box'
+import {useAnchoredPosition, useProvidedRefOrCreate} from '../hooks'
+import {SxProp, merge, BetterSystemStyleObject} from '../sx'
+
+type TooltipDirection = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w'
+type TooltipAlign = 'left' | 'right'
+
+export type TooltipProps = {
+ /** The text content of the tooltip. This should be brief and no longer than a sentence.
+ * Marked as optional to support backward compatibility with aria-label. */
+ text?: string
+ /** @deprecated Use `text` instead */
+ 'aria-label'?: string
+ /** Direction relative to target */
+ direction?: TooltipDirection
+ /** @deprecated Use `direction` instead. Alignment relative to target. */
+ align?: TooltipAlign
+ /** Use aria-describedby or aria-labelledby */
+ type?: 'description' | 'label'
+ /** Tooltip target */
+ children: React.ReactElement & {ref?: React.RefObject}
+ /** When set to true, tooltip appears without any delay */
+ noDelay?: boolean
+ /** @deprecated Always set to true now. */
+ wrap?: boolean
+} & SxProp
+
+// map tooltip direction to anchoredPosition props
+const directionToPosition: Record = {
+ nw: {side: 'outside-top', align: 'start'},
+ n: {side: 'outside-top', align: 'center'},
+ ne: {side: 'outside-top', align: 'end'},
+ e: {side: 'outside-right', align: 'center'},
+ se: {side: 'outside-bottom', align: 'end'},
+ s: {side: 'outside-bottom', align: 'center'},
+ sw: {side: 'outside-bottom', align: 'start'},
+ w: {side: 'outside-left', align: 'center'}
+}
+
+// map align to AnchorAlignment
+const alignToAnchorAlignment: Record = {left: 'start', right: 'end'}
+
+export const TooltipContext = React.createContext<{tooltipId?: string}>({})
+
+export const Tooltip: React.FC = ({
+ text,
+ children,
+ direction = 'n',
+ align,
+ type = 'description',
+ noDelay = false,
+ sx = {},
+ ...props
+}) => {
+ const tooltipId = useSSRSafeId()
+
+ const childRef = children.ref
+ const anchorElementRef = useProvidedRefOrCreate(childRef)
+ const tooltipRef = React.useRef(null)
+
+ const child = React.cloneElement(children, {
+ ref: anchorElementRef,
+ [type === 'description' ? 'aria-describedby' : 'aria-labelledby']: tooltipId
+ })
+
+ const {position} = useAnchoredPosition({
+ side: directionToPosition[direction].side,
+ // support both algin and direction for backward compatibility
+ align: align ? alignToAnchorAlignment[align] : directionToPosition[direction].align,
+ floatingElementRef: tooltipRef,
+ anchorElementRef
+ })
+
+ const tooltipText = text || props['aria-label']
+
+ return (
+
+ {child}
+
+
+ )
+}
+
+const FloatingTooltip = React.forwardRef<
+ HTMLDivElement,
+ Pick & {id: string; position?: AnchorPosition}
+>(({id, text, noDelay, position, sx = {}}, ref) => {
+ const styles: BetterSystemStyleObject = {
+ visibility: 'hidden',
+ opacity: 0,
+ transition: 'opacity 100ms ease-in',
+ transitionDelay: noDelay ? '0ms' : '400ms',
+
+ backgroundColor: 'neutral.emphasisPlus',
+ color: 'fg.onEmphasis',
+ borderRadius: 1,
+ fontSize: 0,
+ paddingY: 1,
+ paddingX: 2,
+ width: 'fit-content',
+ maxWidth: '250px',
+ textAlign: 'center',
+ position: 'absolute',
+ zIndex: 2,
+ top: position?.top,
+ left: position?.left,
+
+ ':before': {
+ content: '""',
+ width: 0,
+ height: 0,
+ border: '5px solid transparent',
+ position: 'absolute'
+ },
+
+ '&[data-side=outside-top]::before': {
+ borderTop: '5px solid',
+ borderTopColor: 'neutral.emphasisPlus',
+ top: '100%'
+ },
+ '&[data-side=outside-bottom]::before': {
+ borderBottom: '5px solid',
+ borderBottomColor: 'neutral.emphasisPlus',
+ top: '-10px'
+ },
+ '&[data-side=outside-left]::before': {
+ borderLeft: '5px solid',
+ borderLeftColor: 'neutral.emphasisPlus',
+ top: 'calc(50% - 5px)',
+ left: '100%'
+ },
+ '&[data-side=outside-right]::before': {
+ borderRight: '5px solid',
+ borderRightColor: 'neutral.emphasisPlus',
+ top: 'calc(50% - 5px)',
+ left: '-10px'
+ },
+
+ '&[data-align=start][data-side=outside-top]::before, &[data-align=start][data-side=outside-bottom]::before': {
+ left: '8px'
+ },
+ '&[data-align=center][data-side=outside-top]::before, &[data-align=center][data-side=outside-bottom]::before': {
+ left: 'calc(50% - 4px)'
+ },
+ '&[data-align=end][data-side=outside-top]::before, &[data-align=end][data-side=outside-bottom]::before': {
+ left: 'calc(100% - 16px)'
+ }
+ }
+
+ return (
+ (styles, sx)}
+ >
+ {text}
+
+ )
+})
diff --git a/src/_TextInputInnerAction.tsx b/src/_TextInputInnerAction.tsx
index 255f8c842f6..d33d63f0f0b 100644
--- a/src/_TextInputInnerAction.tsx
+++ b/src/_TextInputInnerAction.tsx
@@ -41,7 +41,7 @@ const invisibleButtonStyleOverrides = {
const ConditionalTooltip: React.FC<{
['aria-label']?: string
- children: React.ReactNode
+ children: React.ReactElement
}> = ({'aria-label': ariaLabel, children}) => (
<>
{ariaLabel ? (
diff --git a/src/__tests__/ActionMenu.test.tsx b/src/__tests__/ActionMenu.test.tsx
index 7aa30c560ec..29fee6e3066 100644
--- a/src/__tests__/ActionMenu.test.tsx
+++ b/src/__tests__/ActionMenu.test.tsx
@@ -3,10 +3,11 @@ import 'babel-polyfill'
import {axe, toHaveNoViolations} from 'jest-axe'
import React from 'react'
import theme from '../theme'
-import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider} from '..'
+import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider, IconButton} from '..'
import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
import {SingleSelection, MixedSelection} from '../stories/ActionMenu/examples.stories'
import '@testing-library/jest-dom'
+import {TriangleDownIcon} from '@primer/octicons-react'
expect.extend(toHaveNoViolations)
function Example(): JSX.Element {
@@ -136,6 +137,32 @@ describe('ActionMenu', () => {
cleanup()
})
+ it('should open Menu on MenuAnchor click with IconButton', async () => {
+ const component = HTMLRender(
+
+
+
+
+
+
+
+
+
+ New file
+ Copy link
+
+
+
+
+
+
+ )
+ const button = component.getByLabelText('Toggle Menu')
+ fireEvent.click(button)
+ expect(component.getByRole('menu')).toBeInTheDocument()
+ cleanup()
+ })
+
it('should have no axe violations', async () => {
const {container} = HTMLRender( )
const results = await axe(container)
diff --git a/src/__tests__/Button.test.tsx b/src/__tests__/Button.test.tsx
index 7e917445f9e..fc8d0ca69dc 100644
--- a/src/__tests__/Button.test.tsx
+++ b/src/__tests__/Button.test.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {IconButton, Button} from '../Button'
+import {SSRProvider, IconButton, Button} from '../'
import {behavesAsComponent} from '../utils/testing'
import {render, cleanup, fireEvent} from '@testing-library/react'
import {axe, toHaveNoViolations} from 'jest-axe'
@@ -91,13 +91,21 @@ describe('Button', () => {
})
it('styles icon only button to make it a square', () => {
- const container = render( )
+ const container = render(
+
+
+
+ )
const IconOnlyButton = container.getByRole('button')
expect(IconOnlyButton).toHaveStyleRule('padding-right', '8px')
expect(IconOnlyButton).toMatchSnapshot()
})
it('makes sure icon button has an aria-label', () => {
- const container = render( )
+ const container = render(
+
+
+
+ )
const IconOnlyButton = container.getByLabelText('Search button')
expect(IconOnlyButton).toBeTruthy()
})
diff --git a/src/__tests__/TextInput.test.tsx b/src/__tests__/TextInput.test.tsx
index 1516917ca2b..5a9fed253c5 100644
--- a/src/__tests__/TextInput.test.tsx
+++ b/src/__tests__/TextInput.test.tsx
@@ -1,5 +1,5 @@
import React from 'react'
-import {TextInput} from '..'
+import {SSRProvider, TextInput} from '..'
import {render, mount, behavesAsComponent, checkExports} from '../utils/testing'
import {render as HTMLRender, cleanup, fireEvent} from '@testing-library/react'
import {axe, toHaveNoViolations} from 'jest-axe'
@@ -68,11 +68,13 @@ describe('TextInput', () => {
const handleAction = jest.fn()
expect(
render(
- Clear}
- />
+
+ Clear}
+ />
+
)
).toMatchSnapshot()
})
@@ -81,15 +83,17 @@ describe('TextInput', () => {
const handleAction = jest.fn()
expect(
render(
-
- Clear
-
- }
- />
+
+
+ Clear
+
+ }
+ />
+
)
).toMatchSnapshot()
})
@@ -98,22 +102,24 @@ describe('TextInput', () => {
const handleAction = jest.fn()
expect(
render(
- }
- />
+
+ }
+ />
+
)
).toMatchSnapshot()
})
it('focuses the text input if you do not click the input element', () => {
const {container, getByLabelText} = HTMLRender(
- <>
+
{/* eslint-disable-next-line jsx-a11y/label-has-for */}
Search
- >
+
)
const icon = container.querySelector('svg')!
diff --git a/src/__tests__/Tooltip.test.tsx b/src/__tests__/Tooltip.test.tsx
index 4a285eea46e..92ecbc0f8bb 100644
--- a/src/__tests__/Tooltip.test.tsx
+++ b/src/__tests__/Tooltip.test.tsx
@@ -1,52 +1,45 @@
import React from 'react'
-import Tooltip, {TooltipProps} from '../Tooltip'
-import {render, renderClasses, rendersClass, behavesAsComponent, checkExports} from '../utils/testing'
+import 'babel-polyfill'
+import {Tooltip, TooltipContext} from '../Tooltip'
+import {SSRProvider} from '..'
+import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing'
import {render as HTMLRender, cleanup} from '@testing-library/react'
import {axe, toHaveNoViolations} from 'jest-axe'
-import 'babel-polyfill'
+import '@testing-library/jest-dom'
expect.extend(toHaveNoViolations)
-describe('Tooltip', () => {
- behavesAsComponent({Component: Tooltip})
+const Fixture = () => {
+ return (
+
+
+ Save
+
+
+ )
+}
- checkExports('Tooltip', {
- default: Tooltip
+describe('Tooltip', () => {
+ behavesAsComponent({
+ Component: Tooltip,
+ options: {skipAs: true, skipSx: true},
+ toRender: () =>
})
+ checkExports('Tooltip', {default: undefined, Tooltip, TooltipContext})
+
it('should have no axe violations', async () => {
- const {container} = HTMLRender( )
+ const {container} = HTMLRender( )
const results = await axe(container)
expect(results).toHaveNoViolations()
cleanup()
})
- it('renders a with the "tooltipped" class', () => {
- expect(render( ).type).toEqual('span')
- expect(renderClasses( )).toContain('tooltipped-n')
- })
-
- it('respects the "align" prop', () => {
- expect(rendersClass( , 'tooltipped-align-left-2')).toBe(true)
- expect(rendersClass( , 'tooltipped-align-right-2')).toBe(true)
- })
-
- it('respects the "direction" prop', () => {
- for (const direction of Tooltip.directions) {
- expect(
- rendersClass( , `tooltipped-${direction}`)
- ).toBe(true)
- }
- })
-
- it('respects the "noDelay" prop', () => {
- expect(rendersClass( , 'tooltipped-no-delay')).toBe(true)
- })
-
- it('respects the "text" prop', () => {
- expect(render( ).props['aria-label']).toEqual('hi')
+ it('tooltip should not be visible by default', () => {
+ const component = HTMLRender( )
+ expect(component.getByText('tooltip text')).not.toBeVisible()
+ cleanup()
})
- it('respects the "wrap" prop', () => {
- expect(rendersClass( , 'tooltipped-multiline')).toBe(true)
- })
+ checkStoriesForAxeViolations('Tooltip/fixtures')
+ checkStoriesForAxeViolations('Tooltip/examples')
})
diff --git a/src/__tests__/Tooltip.types.test.tsx b/src/__tests__/Tooltip.types.test.tsx
index c9280121588..59c0e45ac54 100644
--- a/src/__tests__/Tooltip.types.test.tsx
+++ b/src/__tests__/Tooltip.types.test.tsx
@@ -1,7 +1,8 @@
import React from 'react'
-import Tooltip from '../Tooltip'
+import {Tooltip} from '../Tooltip'
-export function shouldAcceptCallWithNoProps() {
+export function shouldNotAcceptCallWithMissingProps() {
+ // @ts-expect-error props missing
return
}
diff --git a/src/__tests__/__snapshots__/Button.test.tsx.snap b/src/__tests__/__snapshots__/Button.test.tsx.snap
index f54c410ac25..bdeeeee95bf 100644
--- a/src/__tests__/__snapshots__/Button.test.tsx.snap
+++ b/src/__tests__/__snapshots__/Button.test.tsx.snap
@@ -324,7 +324,7 @@ exports[`Button styles icon only button to make it a square 1`] = `
}
+
+ iconLabel
+
@@ -1981,6 +1853,90 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
margin: 4px;
}
+.c3 {
+ line-height: 1;
+}
+
+.c3:hover [data-component=tooltip],
+.c3:focus-within [data-component=tooltip] {
+ visibility: visible;
+ opacity: 1;
+}
+
+.c5 {
+ visibility: hidden;
+ opacity: 0;
+ -webkit-transition: opacity 100ms ease-in;
+ transition: opacity 100ms ease-in;
+ -webkit-transition-delay: 400ms;
+ transition-delay: 400ms;
+ background-color: #24292f;
+ color: #ffffff;
+ border-radius: 3px;
+ font-size: 12px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ max-width: 250px;
+ text-align: center;
+ position: absolute;
+ z-index: 2;
+ display: inline-block;
+}
+
+.c5:before {
+ content: "";
+ width: 0;
+ height: 0;
+ border: 5px solid transparent;
+ position: absolute;
+}
+
+.c5[data-side=outside-top]::before {
+ border-top: 5px solid;
+ border-top-color: #24292f;
+ top: 100%;
+}
+
+.c5[data-side=outside-bottom]::before {
+ border-bottom: 5px solid;
+ border-bottom-color: #24292f;
+ top: -10px;
+}
+
+.c5[data-side=outside-left]::before {
+ border-left: 5px solid;
+ border-left-color: #24292f;
+ top: calc(50% - 5px);
+ left: 100%;
+}
+
+.c5[data-side=outside-right]::before {
+ border-right: 5px solid;
+ border-right-color: #24292f;
+ top: calc(50% - 5px);
+ left: -10px;
+}
+
+.c5[data-align=start][data-side=outside-top]::before,
+.c5[data-align=start][data-side=outside-bottom]::before {
+ left: 8px;
+}
+
+.c5[data-align=center][data-side=outside-top]::before,
+.c5[data-align=center][data-side=outside-bottom]::before {
+ left: calc(50% - 4px);
+}
+
+.c5[data-align=end][data-side=outside-top]::before,
+.c5[data-align=end][data-side=outside-bottom]::before {
+ left: calc(100% - 16px);
+}
+
.c4 {
border-radius: 6px;
border: 0;
@@ -2164,226 +2120,6 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
outline: 0;
}
-.c3 {
- position: relative;
- display: inline-block;
-}
-
-.c3::before {
- position: absolute;
- z-index: 1000001;
- display: none;
- width: 0px;
- height: 0px;
- color: #24292f;
- pointer-events: none;
- content: '';
- border: 6px solid transparent;
- opacity: 0;
-}
-
-.c3::after {
- position: absolute;
- z-index: 1000000;
- display: none;
- padding: 0.5em 0.75em;
- font: normal normal 11px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
- -webkit-font-smoothing: subpixel-antialiased;
- color: #ffffff;
- text-align: center;
- -webkit-text-decoration: none;
- text-decoration: none;
- text-shadow: none;
- text-transform: none;
- -webkit-letter-spacing: normal;
- -moz-letter-spacing: normal;
- -ms-letter-spacing: normal;
- letter-spacing: normal;
- word-wrap: break-word;
- white-space: pre;
- pointer-events: none;
- content: attr(aria-label);
- background: #24292f;
- border-radius: 3px;
- opacity: 0;
-}
-
-.c3:hover::before,
-.c3:active::before,
-.c3:focus::before,
-.c3:hover::after,
-.c3:active::after,
-.c3:focus::after {
- display: inline-block;
- -webkit-text-decoration: none;
- text-decoration: none;
- -webkit-animation-name: tooltip-appear;
- animation-name: tooltip-appear;
- -webkit-animation-duration: 0.1s;
- animation-duration: 0.1s;
- -webkit-animation-fill-mode: forwards;
- animation-fill-mode: forwards;
- -webkit-animation-timing-function: ease-in;
- animation-timing-function: ease-in;
- -webkit-animation-delay: 0.4s;
- animation-delay: 0.4s;
-}
-
-.c3.tooltipped-no-delay:hover::before,
-.c3.tooltipped-no-delay:active::before,
-.c3.tooltipped-no-delay:focus::before,
-.c3.tooltipped-no-delay:hover::after,
-.c3.tooltipped-no-delay:active::after,
-.c3.tooltipped-no-delay:focus::after {
- -webkit-animation-delay: 0s;
- animation-delay: 0s;
-}
-
-.c3.tooltipped-multiline:hover::after,
-.c3.tooltipped-multiline:active::after,
-.c3.tooltipped-multiline:focus::after {
- display: table-cell;
-}
-
-.c3.tooltipped-s::after,
-.c3.tooltipped-se::after,
-.c3.tooltipped-sw::after {
- top: 100%;
- right: 50%;
- margin-top: 6px;
-}
-
-.c3.tooltipped-s::before,
-.c3.tooltipped-se::before,
-.c3.tooltipped-sw::before {
- top: auto;
- right: 50%;
- bottom: -7px;
- margin-right: -6px;
- border-bottom-color: #24292f;
-}
-
-.c3.tooltipped-se::after {
- right: auto;
- left: 50%;
- margin-left: -16px;
-}
-
-.c3.tooltipped-sw::after {
- margin-right: -16px;
-}
-
-.c3.tooltipped-n::after,
-.c3.tooltipped-ne::after,
-.c3.tooltipped-nw::after {
- right: 50%;
- bottom: 100%;
- margin-bottom: 6px;
-}
-
-.c3.tooltipped-n::before,
-.c3.tooltipped-ne::before,
-.c3.tooltipped-nw::before {
- top: -7px;
- right: 50%;
- bottom: auto;
- margin-right: -6px;
- border-top-color: #24292f;
-}
-
-.c3.tooltipped-ne::after {
- right: auto;
- left: 50%;
- margin-left: -16px;
-}
-
-.c3.tooltipped-nw::after {
- margin-right: -16px;
-}
-
-.c3.tooltipped-s::after,
-.c3.tooltipped-n::after {
- -webkit-transform: translateX(50%);
- -ms-transform: translateX(50%);
- transform: translateX(50%);
-}
-
-.c3.tooltipped-w::after {
- right: 100%;
- bottom: 50%;
- margin-right: 6px;
- -webkit-transform: translateY(50%);
- -ms-transform: translateY(50%);
- transform: translateY(50%);
-}
-
-.c3.tooltipped-w::before {
- top: 50%;
- bottom: 50%;
- left: -7px;
- margin-top: -6px;
- border-left-color: #24292f;
-}
-
-.c3.tooltipped-e::after {
- bottom: 50%;
- left: 100%;
- margin-left: 6px;
- -webkit-transform: translateY(50%);
- -ms-transform: translateY(50%);
- transform: translateY(50%);
-}
-
-.c3.tooltipped-e::before {
- top: 50%;
- right: -7px;
- bottom: 50%;
- margin-top: -6px;
- border-right-color: #24292f;
-}
-
-.c3.tooltipped-multiline::after {
- width: -webkit-max-content;
- width: -moz-max-content;
- width: max-content;
- max-width: 250px;
- word-wrap: break-word;
- white-space: pre-line;
- border-collapse: separate;
-}
-
-.c3.tooltipped-multiline.tooltipped-s::after,
-.c3.tooltipped-multiline.tooltipped-n::after {
- right: auto;
- left: 50%;
- -webkit-transform: translateX(-50%);
- -ms-transform: translateX(-50%);
- transform: translateX(-50%);
-}
-
-.c3.tooltipped-multiline.tooltipped-w::after,
-.c3.tooltipped-multiline.tooltipped-e::after {
- right: 100%;
-}
-
-.c3.tooltipped-align-right-2::after {
- right: 0;
- margin-right: 0;
-}
-
-.c3.tooltipped-align-right-2::before {
- right: 15px;
-}
-
-.c3.tooltipped-align-left-2::after {
- left: 0;
- margin-left: 0;
-}
-
-.c3.tooltipped-align-left-2::before {
- left: 10px;
-}
-
@media (forced-colors:active) {
.c4:focus {
outline: solid 1px transparent;
@@ -2429,11 +2165,10 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = `
className="c2 TextInput-action"
>
+
+ Clear input
+
diff --git a/src/__tests__/__snapshots__/Tooltip.test.tsx.snap b/src/__tests__/__snapshots__/Tooltip.test.tsx.snap
index 25e5a7b14ab..83b5ed41551 100644
--- a/src/__tests__/__snapshots__/Tooltip.test.tsx.snap
+++ b/src/__tests__/__snapshots__/Tooltip.test.tsx.snap
@@ -2,226 +2,104 @@
exports[`Tooltip renders consistently 1`] = `
.c0 {
- position: relative;
+ line-height: 1;
}
-.c0::before {
- position: absolute;
- z-index: 1000001;
- display: none;
- width: 0px;
- height: 0px;
- color: #24292f;
- pointer-events: none;
- content: '';
- border: 6px solid transparent;
- opacity: 0;
+.c0:hover [data-component=tooltip],
+.c0:focus-within [data-component=tooltip] {
+ visibility: visible;
+ opacity: 1;
}
-.c0::after {
- position: absolute;
- z-index: 1000000;
- display: none;
- padding: 0.5em 0.75em;
- font: normal normal 11px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";
- -webkit-font-smoothing: subpixel-antialiased;
+.c1 {
+ visibility: hidden;
+ opacity: 0;
+ -webkit-transition: opacity 100ms ease-in;
+ transition: opacity 100ms ease-in;
+ -webkit-transition-delay: 400ms;
+ transition-delay: 400ms;
+ background-color: #24292f;
color: #ffffff;
- text-align: center;
- -webkit-text-decoration: none;
- text-decoration: none;
- text-shadow: none;
- text-transform: none;
- -webkit-letter-spacing: normal;
- -moz-letter-spacing: normal;
- -ms-letter-spacing: normal;
- letter-spacing: normal;
- word-wrap: break-word;
- white-space: pre;
- pointer-events: none;
- content: attr(aria-label);
- background: #24292f;
border-radius: 3px;
- opacity: 0;
-}
-
-.c0:hover::before,
-.c0:active::before,
-.c0:focus::before,
-.c0:hover::after,
-.c0:active::after,
-.c0:focus::after {
- display: inline-block;
- -webkit-text-decoration: none;
- text-decoration: none;
- -webkit-animation-name: tooltip-appear;
- animation-name: tooltip-appear;
- -webkit-animation-duration: 0.1s;
- animation-duration: 0.1s;
- -webkit-animation-fill-mode: forwards;
- animation-fill-mode: forwards;
- -webkit-animation-timing-function: ease-in;
- animation-timing-function: ease-in;
- -webkit-animation-delay: 0.4s;
- animation-delay: 0.4s;
-}
-
-.c0.tooltipped-no-delay:hover::before,
-.c0.tooltipped-no-delay:active::before,
-.c0.tooltipped-no-delay:focus::before,
-.c0.tooltipped-no-delay:hover::after,
-.c0.tooltipped-no-delay:active::after,
-.c0.tooltipped-no-delay:focus::after {
- -webkit-animation-delay: 0s;
- animation-delay: 0s;
+ font-size: 12px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ padding-left: 8px;
+ padding-right: 8px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ max-width: 250px;
+ text-align: center;
+ position: absolute;
+ z-index: 2;
}
-.c0.tooltipped-multiline:hover::after,
-.c0.tooltipped-multiline:active::after,
-.c0.tooltipped-multiline:focus::after {
- display: table-cell;
+.c1:before {
+ content: "";
+ width: 0;
+ height: 0;
+ border: 5px solid transparent;
+ position: absolute;
}
-.c0.tooltipped-s::after,
-.c0.tooltipped-se::after,
-.c0.tooltipped-sw::after {
+.c1[data-side=outside-top]::before {
+ border-top: 5px solid;
+ border-top-color: #24292f;
top: 100%;
- right: 50%;
- margin-top: 6px;
}
-.c0.tooltipped-s::before,
-.c0.tooltipped-se::before,
-.c0.tooltipped-sw::before {
- top: auto;
- right: 50%;
- bottom: -7px;
- margin-right: -6px;
+.c1[data-side=outside-bottom]::before {
+ border-bottom: 5px solid;
border-bottom-color: #24292f;
+ top: -10px;
}
-.c0.tooltipped-se::after {
- right: auto;
- left: 50%;
- margin-left: -16px;
-}
-
-.c0.tooltipped-sw::after {
- margin-right: -16px;
-}
-
-.c0.tooltipped-n::after,
-.c0.tooltipped-ne::after,
-.c0.tooltipped-nw::after {
- right: 50%;
- bottom: 100%;
- margin-bottom: 6px;
-}
-
-.c0.tooltipped-n::before,
-.c0.tooltipped-ne::before,
-.c0.tooltipped-nw::before {
- top: -7px;
- right: 50%;
- bottom: auto;
- margin-right: -6px;
- border-top-color: #24292f;
-}
-
-.c0.tooltipped-ne::after {
- right: auto;
- left: 50%;
- margin-left: -16px;
-}
-
-.c0.tooltipped-nw::after {
- margin-right: -16px;
-}
-
-.c0.tooltipped-s::after,
-.c0.tooltipped-n::after {
- -webkit-transform: translateX(50%);
- -ms-transform: translateX(50%);
- transform: translateX(50%);
-}
-
-.c0.tooltipped-w::after {
- right: 100%;
- bottom: 50%;
- margin-right: 6px;
- -webkit-transform: translateY(50%);
- -ms-transform: translateY(50%);
- transform: translateY(50%);
-}
-
-.c0.tooltipped-w::before {
- top: 50%;
- bottom: 50%;
- left: -7px;
- margin-top: -6px;
+.c1[data-side=outside-left]::before {
+ border-left: 5px solid;
border-left-color: #24292f;
-}
-
-.c0.tooltipped-e::after {
- bottom: 50%;
+ top: calc(50% - 5px);
left: 100%;
- margin-left: 6px;
- -webkit-transform: translateY(50%);
- -ms-transform: translateY(50%);
- transform: translateY(50%);
}
-.c0.tooltipped-e::before {
- top: 50%;
- right: -7px;
- bottom: 50%;
- margin-top: -6px;
+.c1[data-side=outside-right]::before {
+ border-right: 5px solid;
border-right-color: #24292f;
+ top: calc(50% - 5px);
+ left: -10px;
}
-.c0.tooltipped-multiline::after {
- width: -webkit-max-content;
- width: -moz-max-content;
- width: max-content;
- max-width: 250px;
- word-wrap: break-word;
- white-space: pre-line;
- border-collapse: separate;
-}
-
-.c0.tooltipped-multiline.tooltipped-s::after,
-.c0.tooltipped-multiline.tooltipped-n::after {
- right: auto;
- left: 50%;
- -webkit-transform: translateX(-50%);
- -ms-transform: translateX(-50%);
- transform: translateX(-50%);
-}
-
-.c0.tooltipped-multiline.tooltipped-w::after,
-.c0.tooltipped-multiline.tooltipped-e::after {
- right: 100%;
-}
-
-.c0.tooltipped-align-right-2::after {
- right: 0;
- margin-right: 0;
-}
-
-.c0.tooltipped-align-right-2::before {
- right: 15px;
+.c1[data-align=start][data-side=outside-top]::before,
+.c1[data-align=start][data-side=outside-bottom]::before {
+ left: 8px;
}
-.c0.tooltipped-align-left-2::after {
- left: 0;
- margin-left: 0;
+.c1[data-align=center][data-side=outside-top]::before,
+.c1[data-align=center][data-side=outside-bottom]::before {
+ left: calc(50% - 4px);
}
-.c0.tooltipped-align-left-2::before {
- left: 10px;
+.c1[data-align=end][data-side=outside-top]::before,
+.c1[data-align=end][data-side=outside-bottom]::before {
+ left: calc(100% - 16px);
}
+ className="c0"
+>
+
+ Save
+
+
+ tooltip text
+
+
`;
diff --git a/src/__tests__/hooks/useAnchoredPosition.test.tsx b/src/__tests__/hooks/useAnchoredPosition.test.tsx
index 4038ca73c0e..4b7d38dc710 100644
--- a/src/__tests__/hooks/useAnchoredPosition.test.tsx
+++ b/src/__tests__/hooks/useAnchoredPosition.test.tsx
@@ -23,6 +23,7 @@ it('should should return a position', () => {
expect(cb).toHaveBeenCalledTimes(2)
expect(cb.mock.calls[1][0]['position']).toMatchInlineSnapshot(`
Object {
+ "anchorAlign": "start",
"anchorSide": "outside-bottom",
"left": 0,
"top": 4,
diff --git a/src/index.ts b/src/index.ts
index f4ce005e4f7..435e8e9b295 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -143,7 +143,7 @@ export type {
} from './Timeline'
export {default as Token, IssueLabelToken, AvatarToken} from './Token'
export type {TokenProps} from './Token'
-export {default as Tooltip} from './Tooltip'
+export {Tooltip} from './Tooltip'
export type {TooltipProps} from './Tooltip'
export {default as Truncate} from './Truncate'
export type {TruncateProps} from './Truncate'
diff --git a/src/stories/ActionMenu/fixtures.stories.tsx b/src/stories/ActionMenu/fixtures.stories.tsx
index 8e74c3677f9..0b077e45656 100644
--- a/src/stories/ActionMenu/fixtures.stories.tsx
+++ b/src/stories/ActionMenu/fixtures.stories.tsx
@@ -269,6 +269,7 @@ export function MemexTableMenu(): JSX.Element {
width: 200,
display: 'flex',
justifyContent: 'space-between',
+ alignItems: 'center',
p: 2,
border: '1px solid',
borderColor: 'border.default'
diff --git a/src/stories/Tooltip.stories.tsx b/src/stories/Tooltip.stories.tsx
deleted file mode 100644
index 59d034282f1..00000000000
--- a/src/stories/Tooltip.stories.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import {Meta} from '@storybook/react'
-import {BaseStyles, ThemeProvider, IconButton} from '..'
-import Box from '../Box'
-import Tooltip from '../Tooltip'
-import {SearchIcon} from '@primer/octicons-react'
-
-export default {
- title: 'Tooltip/Default',
- component: Tooltip,
-
- decorators: [
- Story => {
- return (
-
-
-
-
-
- )
- }
- ]
-} as Meta
-
-export const TextTooltip = () => (
-
- Text with a tooltip
-
-)
-
-export const IconButtonTooltip = () => (
-
-
-
-
-
-)
diff --git a/src/stories/Tooltip/examples.stories.tsx b/src/stories/Tooltip/examples.stories.tsx
new file mode 100644
index 00000000000..51217c340fd
--- /dev/null
+++ b/src/stories/Tooltip/examples.stories.tsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import {Meta} from '@storybook/react'
+import {CodeIcon, CrossReferenceIcon, ImageIcon, MentionIcon} from '@primer/octicons-react'
+import {IconButton, Box, IssueLabelToken, Tooltip} from '../..'
+
+export default {title: 'Composite components/Tooltip/examples', component: Tooltip} as Meta
+
+const IssueLabel = React.forwardRef(({text, fillColor}, ref) => {
+ return (
+
+ )
+})
+
+export const TokenWithTooltip = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const ButtonWithTooltip = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/stories/Tooltip/fixtures.stories.tsx b/src/stories/Tooltip/fixtures.stories.tsx
new file mode 100644
index 00000000000..401492fb3db
--- /dev/null
+++ b/src/stories/Tooltip/fixtures.stories.tsx
@@ -0,0 +1,108 @@
+import React from 'react'
+import {Meta} from '@storybook/react'
+import {MentionIcon} from '@primer/octicons-react'
+import {Box, Button, IssueLabelToken, Tooltip} from '../..'
+
+export default {title: 'Composite components/Tooltip/fixtures', component: Tooltip} as Meta
+
+const IssueLabel = React.forwardRef(({text, fillColor}, ref) => {
+ return (
+
+ )
+})
+
+export const Direction = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export const Delay = () => {
+ return (
+
+
+ default, with delay
+
+
+ no delay
+
+
+ )
+}
+
+export const TypeLabel = () => {
+ return (
+
+
+
+
+
+ )
+}
+
+export const AcceptsSx = () => {
+ return (
+
+ Tooltip accepts sx prop
+
+ )
+}
+
+export const BackwardCompatibility = () => {
+ return (
+
+
+ Has tooltip with aria-label
+
+
+ Has tooltip with align:right
+
+
+ Has tooltip with wrap
+
+
+ )
+}
diff --git a/src/stories/behaviors-anchored-position.stories.tsx b/src/stories/behaviors-anchored-position.stories.tsx
new file mode 100644
index 00000000000..2917b954022
--- /dev/null
+++ b/src/stories/behaviors-anchored-position.stories.tsx
@@ -0,0 +1,345 @@
+import React from 'react'
+import {Meta} from '@storybook/react'
+import {SmileyIcon, KebabHorizontalIcon, TriangleDownIcon} from '@primer/octicons-react'
+import {
+ BaseStyles,
+ Box,
+ ThemeProvider,
+ Text,
+ IconButton,
+ PageLayout,
+ Heading,
+ ActionMenu,
+ ActionList,
+ Avatar,
+ Label,
+ LabelProps
+} from '..'
+import {useAnchoredPosition} from '../hooks'
+import type {AnchorAlignment} from '@primer/behaviors'
+
+export default {
+ title: 'Behaviors/anchoredPosition',
+ decorators: [
+ // Note: For some reason, if you use ,
+ // the component gets unmounted from the root every time a control changes!
+ Story => {
+ return (
+
+ {Story()}
+
+ )
+ }
+ ]
+} as Meta
+
+type TooltipProps = {
+ children: string
+ position?: {left: number; top: number; anchorAlign: AnchorAlignment}
+ defaultVisible?: boolean
+}
+const Tooltip = React.forwardRef(({defaultVisible, position, children}, ref) => {
+ return (
+
+
+ {children}
+
+ )
+})
+
+const LabelWithTooltip: React.FC = ({
+ description,
+ defaultVisible = false,
+ ...props
+}) => {
+ const labelRef = React.useRef(null)
+ const tooltipRef = React.useRef(null)
+
+ const {position} = useAnchoredPosition({
+ side: 'outside-bottom',
+ align: 'center',
+ anchorElementRef: labelRef,
+ floatingElementRef: tooltipRef
+ })
+
+ return (
+
+
+ {props.children}
+
+
+ {description}
+
+
+ )
+}
+
+export const Tooltips = () => {
+ const [optionsOpen, setOptionsOpen] = React.useState(false)
+ const optionsButtonRef = React.useRef(null)
+ const optionsTooltipRef = React.useRef(null)
+ const {position: optionsTooltipPosition} = useAnchoredPosition({
+ side: 'outside-bottom',
+ align: 'start',
+ anchorElementRef: optionsButtonRef,
+ floatingElementRef: optionsTooltipRef
+ })
+
+ const [reactionsOpen, setReactionsOpen] = React.useState(false)
+ const reactionButtonRef = React.useRef(null)
+ const reactionTooltipRef = React.useRef(null)
+ const {position: reactionTooltipPosition} = useAnchoredPosition({
+ side: 'outside-bottom',
+ align: 'start',
+ anchorElementRef: reactionButtonRef,
+ floatingElementRef: reactionTooltipRef
+ })
+
+ return (
+ <>
+
+
+
+
+ Input validation styles #1831
+
+
+
+
+
+
+
+
+ setReactionsOpen(!reactionsOpen)}
+ />
+
+ Add reaction
+
+
+
+
+
+ {['👍', '👎', '😄', '🎉', '😕', '❤️', '🚀', '👀'].map(emoji => (
+ {emoji}
+ ))}
+
+
+
+
+
+ setOptionsOpen(!optionsOpen)}
+ />
+
+ Show options
+
+
+
+
+
+ Copy link
+ Quote reply
+
+ Edit
+
+
+
+
+
+
+ colebemis {' '}
+ added {' '}
+
+ bug
+
+
+ collab
+
+
+ blocked
+
+
+ dependencies
+
+
+ dependencies
+
+
+
+
+
+
+ Assignees
+ No one – assign yourself
+
+
+
+ Labels
+ None yet
+
+
+
+
+ >
+ )
+}
+
+export function MemexTableMenu(): JSX.Element {
+ return (
+ <>
+
+
+ Primer teams backlog
+
+
+
+
+
+ Title
+ Assignees
+ Status
+ Labels
+ Repository
+
+
+ >
+ )
+}
+
+const TableHeader: React.FC<{style?: Record}> = props => {
+ return (
+
+ {props.children}
+
+
+
+
+
+
+
+ Sort ascending (123...)
+ Sort descending (123...)
+
+ Filter by values
+ Group by values
+
+ Delete file
+
+
+
+
+ )
+}