diff --git a/.changeset/flat-drinks-retire.md b/.changeset/flat-drinks-retire.md deleted file mode 100644 index 74e8856b1c9..00000000000 --- a/.changeset/flat-drinks-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': minor ---- - -Tooltip: Address accessibility remediations and refactor styles towards more static styling diff --git a/docs/content/Tooltip.mdx b/docs/content/Tooltip.mdx index 90dab885ac8..c50776cae4c 100644 --- a/docs/content/Tooltip.mdx +++ b/docs/content/Tooltip.mdx @@ -4,7 +4,7 @@ title: Tooltip status: Alpha --- -import data from '../../src/Tooltip/Tooltip.docs.json' +import data from '../../src/Tooltip.docs.json' The Tooltip component adds a tooltip to add context to interactive elements on the page. @@ -20,85 +20,10 @@ A tooltip may only be used on an element that is interactive such as a button or ## Examples -### Default (As a label type) - -```jsx live - - - -``` - -### As a description type - -```jsx live - - - -``` - -### With direction - ```jsx live - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### With wrap - -```jsx live - - - -``` - -### With no delay - -```jsx live - - - -``` - -### With align - -```jsx live - - - - - - + + + ``` diff --git a/generated/components.json b/generated/components.json index 5f410ab0fe2..cd2307492bd 100644 --- a/generated/components.json +++ b/generated/components.json @@ -229,6 +229,44 @@ ], "subcomponents": [] }, + "tooltip": { + "id": "tooltip", + "name": "Tooltip", + "status": "alpha", + "a11yReviewed": false, + "stories": [], + "props": [ + { + "name": "align", + "type": "'left' | 'right'" + }, + { + "name": "direction", + "type": "'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw'", + "description": "Sets where the tooltip renders in relation to the target." + }, + { + "name": "noDelay", + "type": "boolean", + "description": "When set to `true`, tooltip appears without any delay." + }, + { + "name": "aria-label", + "type": "string", + "description": "Text used in `aria-label` (for accessibility)" + }, + { + "name": "wrap", + "type": "boolean", + "description": "Use `true` to allow text within tooltip to wrap." + }, + { + "name": "sx", + "type": "SystemStyleObject" + } + ], + "subcomponents": [] + }, "truncate": { "id": "truncate", "name": "Truncate", @@ -4644,61 +4682,6 @@ } ] }, - "tooltip": { - "id": "tooltip", - "name": "Tooltip", - "status": "alpha", - "a11yReviewed": false, - "stories": [ - { - "id": "components-tooltip--default", - "code": "() => (\n \n \n \n \n \n)" - } - ], - "props": [ - { - "name": "align", - "type": "'left' | 'right'" - }, - { - "name": "aria-label", - "type": "string", - "description": "Should be utilised for label type tooltips and it is going to be used to label the tooltip trigger." - }, - { - "name": "direction", - "type": "'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw'", - "defaultValue": "n", - "description": "Sets where the tooltip renders in relation to the target." - }, - { - "name": "noDelay", - "type": "boolean", - "description": "When set to `true`, tooltip appears without any delay." - }, - { - "name": "text", - "type": "string", - "description": "Should be utilised for description type tooltips and it is going to be used to describe the tooltip trigger." - }, - { - "name": "type", - "type": "'label' | 'description'", - "defaultValue": "label", - "description": "The type of tooltip. `label` is used for labelling the element that triggers tooltip. `description` is used for describing or adding a suplementary information to the element that triggers the tooltip." - }, - { - "name": "wrap", - "type": "boolean", - "description": "Use `true` to allow text within tooltip to wrap." - }, - { - "name": "sx", - "type": "SystemStyleObject" - } - ], - "subcomponents": [] - }, "tree_view": { "id": "tree_view", "name": "TreeView", diff --git a/src/Tooltip/Tooltip.docs.json b/src/Tooltip.docs.json similarity index 54% rename from src/Tooltip/Tooltip.docs.json rename to src/Tooltip.docs.json index d964fadd4ae..d4b88a8c6ec 100644 --- a/src/Tooltip/Tooltip.docs.json +++ b/src/Tooltip.docs.json @@ -9,15 +9,9 @@ "name": "align", "type": "'left' | 'right'" }, - { - "name": "aria-label", - "type": "string", - "description": "Should be utilised for label type tooltips and it is going to be used to label the tooltip trigger." - }, { "name": "direction", "type": "'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw'", - "defaultValue": "n", "description": "Sets where the tooltip renders in relation to the target." }, { @@ -26,15 +20,9 @@ "description": "When set to `true`, tooltip appears without any delay." }, { - "name": "text", + "name": "aria-label", "type": "string", - "description": "Should be utilised for description type tooltips and it is going to be used to describe the tooltip trigger." - }, - { - "name": "type", - "type": "'label' | 'description'", - "defaultValue": "label", - "description": "The type of tooltip. `label` is used for labelling the element that triggers tooltip. `description` is used for describing or adding a suplementary information to the element that triggers the tooltip." + "description": "Text used in `aria-label` (for accessibility)" }, { "name": "wrap", diff --git a/src/Tooltip.tsx b/src/Tooltip.tsx new file mode 100644 index 00000000000..28fb1c8645f --- /dev/null +++ b/src/Tooltip.tsx @@ -0,0 +1,266 @@ +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, + &:focus-within { + &::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, + &.tooltipped-no-delay:focus-within { + &::before, + &::after { + animation-delay: 0s; + } + } + + &.tooltipped-multiline:hover, + &.tooltipped-multiline:active, + &.tooltipped-multiline:focus, + &.tooltipped-multiline:focus-within { + &::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/Tooltip.features.stories.tsx b/src/Tooltip/Tooltip.features.stories.tsx deleted file mode 100644 index 2a5a6ebec44..00000000000 --- a/src/Tooltip/Tooltip.features.stories.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React from 'react' -import {Meta} from '@storybook/react' -import {BaseStyles, ThemeProvider, IconButton, Button} from '..' -import {Tooltip} from '.' -import {SearchIcon} from '@primer/octicons-react' -import Box from '../Box' - -export default { - title: 'Components/Tooltip/Features', - component: Tooltip, - - decorators: [ - Story => { - return ( - - - - - - ) - }, - ], -} as Meta - -// As a label for an IconButton -export const TooltipLabelTypeTooltip = () => ( - - - - - -) - -// As a label for an IconButton -export const TooltipNativeHTMLButton = () => ( - - - - - -) - -// As a supplementary description for a button -export const TooltipDescriptionTypeTooltip = () => ( - - - - - -) - -// As a supplementary description for an IconButton -export const TooltipIconButtonWithDescription = () => ( - - - - - -) - -export const TooltipWithDirection = () => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -) - -export const TooltipNoDelay = () => ( - - - -) - -export const TooltipWithAlign = () => ( - - - - - - - - -) - -export const TooltipWithWrap = () => ( - - - - - - - - -) diff --git a/src/Tooltip/Tooltip.tsx b/src/Tooltip/Tooltip.tsx deleted file mode 100644 index 9be7dbf95f7..00000000000 --- a/src/Tooltip/Tooltip.tsx +++ /dev/null @@ -1,396 +0,0 @@ -import React, {Children, useEffect, useRef, useState} from 'react' -import Box from '../Box' -import sx, {SxProp} from '../sx' -import {useId} from '../hooks/useId' -import {isFocusable} from '@primer/behaviors/utils' -import {invariant} from '../utils/invariant' -import styled from 'styled-components' -import {get} from '../constants' -import {useOnEscapePress} from '../hooks/useOnEscapePress' - -export type TooltipProps = { - direction?: 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw' - text?: string - noDelay?: boolean - align?: 'left' | 'right' - wrap?: boolean - type?: 'label' | 'description' - 'aria-label'?: React.AriaAttributes['aria-label'] -} & SxProp - -export type TriggerPropsType = { - 'aria-describedby'?: string - 'aria-labelledby'?: string - 'aria-label'?: string - onBlur?: React.FocusEventHandler - onFocus?: React.FocusEventHandler - onMouseEnter?: React.MouseEventHandler - ref?: React.RefObject -} - -const TooltipEL = styled.div` - // tooltip element itself - position: absolute; - z-index: 1000000; - 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: normal; - background: ${get('colors.neutral.emphasisPlus')}; - border-radius: ${get('radii.1')}; - width: max-content; - opacity: 0; - max-width: 250px; - @media (forced-colors: active) { - outline: 1px solid transparent; - } - - /* tooltip element should be rendered visually hidden when it is not opened. */ - &:not([data-state='open']) { - /* Visually hidden styles */ - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; - } - - // the caret - &::before { - position: absolute; - z-index: 1000001; - color: ${get('colors.neutral.emphasisPlus')}; - content: ''; - border: 6px solid transparent; - opacity: 0; - } - - // This is needed to keep the tooltip open when the user leaves the trigger element to hover tooltip - &::after { - position: absolute; - display: block; - right: 0; - left: 0; - height: 8px; - content: ''; - } - - // delay animation for tooltip - @keyframes tooltip-appear { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - /* South, East, Southeast, Southwest before */ - - &[data-direction='n']::before, - &[data-direction='ne']::before, - &[data-direction='nw']::before { - top: 100%; - border-top-color: ${get('colors.neutral.emphasisPlus')}; - } - - &[data-direction='s']::before, - &[data-direction='se']::before, - &[data-direction='sw']::before { - bottom: 100%; - border-bottom-color: ${get('colors.neutral.emphasisPlus')}; - } - - &[data-direction='n']:before, - &[data-direction='s']:before { - right: 50%; - margin-right: -6px; - } - - &[data-direction='ne']::before, - &[data-direction='se']::before { - left: 0; - margin-left: 6px; - } - - &[data-direction='sw']::before, - &[data-direction='nw']::before { - right: 0; - margin-right: 6px; - } - - /* South, East, Southeast, Southwest after */ - - &[data-direction='n']::after, - &[data-direction='ne']::after, - &[data-direction='nw']::after { - top: 100%; - } - - &[data-direction='s']::after, - &[data-direction='se']::after, - &[data-direction='sw']::after { - bottom: 100%; - } - - /* West before and after */ - - &[data-direction='w']::before { - top: 50%; - bottom: 50%; - left: 100%; - margin-top: -6px; - border-left-color: ${get('colors.neutral.emphasisPlus')}; - } - - &[data-direction='w']::after { - position: absolute; - display: block; - height: 100%; - width: 8px; - content: ''; - bottom: 0; - left: 100%; - } - - /* East before and after */ - - &[data-direction='e']::after { - position: absolute; - display: block; - height: 100%; - width: 8px; - content: ''; - bottom: 0; - right: 100%; - margin-left: -8px; - } - - &[data-direction='e']::before { - top: 50%; - bottom: 50%; - right: 100%; - margin-top: -6px; - border-right-color: ${get('colors.neutral.emphasisPlus')}; - } - - /* Animation styles */ - - &[data-state='open'], - &[data-state='open']::before { - animation-name: tooltip-appear; - animation-duration: 0.1s; - animation-fill-mode: forwards; - animation-timing-function: ease-in; - animation-delay: 0.4s; - } - - /* Position of the tooltip element when it is opened. */ - - &[data-state='open'] { - &[data-no-delay='true'], - &[data-no-delay='true']::before { - animation-delay: 0s; - } - &[data-direction='s'], - &[data-direction='se'], - &[data-direction='sw'] { - top: 100%; - right: 50%; - margin-top: 6px; - } - - &[data-direction='n'], - &[data-direction='ne'], - &[data-direction='nw'] { - bottom: 100%; - margin-bottom: 6px; - right: 50%; - } - - &[data-direction='n'], - &[data-direction='s'] { - transform: translateX(50%); - } - - &[data-direction='se'] { - right: auto; - left: 50%; - margin-left: -${get('space.3')}; - } - &[data-direction='ne'] { - right: auto; - left: 50%; - margin-left: -${get('space.3')}; - } - - &[data-direction='sw'] { - margin-right: -${get('space.3')}; - } - - &[data-direction='e'] { - bottom: 50%; - left: 100%; - margin-left: 6px; - transform: translateY(50%); - } - - &[data-direction='w'] { - bottom: 50%; - right: 100%; - margin-right: 6px; - transform: translateY(50%); - } - - /* Align and wrap styles */ - - &[data-align='left'] { - right: 100%; - margin-left: 0; - } - &[data-align='left']::before { - right: 40px; - } - &[data-align='right'] { - right: 0; - margin-right: 0; - } - &[data-align='right']::before { - right: 72px; - } - - &[data-wrap='true'] { - display: table-cell; - width: max-content; - max-width: 250px; - word-wrap: break-word; - white-space: pre-line; - border-collapse: separate; - } - - &[data-wrap='true'][data-direction='n'], - &[data-wrap='true'][data-direction='s'] { - transform: translateX(-50%); - right: auto; - left: 50%; - } - - &[data-wrap='true'][data-direction='w'], - &[data-wrap='true'][data-direction='e'] { - right: 100%; - } - } - - ${sx}; -` - -export const Tooltip: React.FC> = ({ - direction = 'n', - // used for description type - text, - // used for label type - 'aria-label': label, - align, - wrap, - noDelay, - type = 'label', - children, - ...rest -}) => { - const id = useId() - const triggerRef = useRef(null) - const child = Children.only(children) - const [open, setOpen] = useState(false) - - // we need this check for every render - if (__DEV__) { - // Practically, this is not a conditional hook, it is a compile time check - // eslint-disable-next-line react-hooks/rules-of-hooks - useEffect(() => { - if (triggerRef.current) { - // Has trigger element or any of its children interactive elements? - const isTriggerInteractive = isFocusable(triggerRef.current) - const triggerChildren = triggerRef.current.childNodes - const hasInteractiveChild = Array.from(triggerChildren).some(child => { - return child instanceof HTMLElement && isFocusable(child) - }) - invariant( - isTriggerInteractive || hasInteractiveChild, - 'The `Tooltip` component expects a single React element that contains interactive content. Consider using a ` - -) - -describe('Tooltip', () => { - checkExports('Tooltip', { - default: undefined, - Tooltip, - }) - - checkStoriesForAxeViolations('Tooltip.features', '../Tooltip/') - - it('renders `data-direction="n"` by default', () => { - const {getByText} = HTMLRender() - expect(getByText('label type tooltip')).toHaveAttribute('data-direction', 'n') - }) - it('renders `data-direction` attribute with the correct value when the `direction` prop is specified', () => { - const {getByText} = HTMLRender() - expect(getByText('label type tooltip')).toHaveAttribute('data-direction', 's') - }) - - it('renders `data-no-delay` attribute with the correct value when the `noDelay` prop is specified', () => { - const {getByText} = HTMLRender() - expect(getByText('label type tooltip')).toHaveAttribute('data-no-delay', 'true') - }) - - it('renders `data-wrap` attribute with the correct value when the `wrap` prop is specified', () => { - const {getByText} = HTMLRender() - expect(getByText('label type tooltip')).toHaveAttribute('data-wrap', 'true') - }) - it('renders `data-align` attribute with the correct value when the `align` prop is specified', () => { - const {getByText} = HTMLRender() - expect(getByText('label type tooltip')).toHaveAttribute('data-align', 'right') - }) - it('should label the trigger element by its tooltip when the tooltip type is label', () => { - const {getByRole, getByText} = HTMLRender() - const triggerEL = getByRole('button') - const tooltipEl = getByText('label type tooltip') - expect(triggerEL).toHaveAttribute('aria-labelledby', tooltipEl.id) - }) - it('should render aria-hidden on the tooltip element when the tooltip is label type', () => { - const {getByText} = HTMLRender() - expect(getByText('label type tooltip')).toHaveAttribute('aria-hidden', 'true') - }) - it('should describe the trigger element by its tooltip when the tooltip type is describe', () => { - const {getByRole, getByText} = HTMLRender( - , - ) - const triggerEL = getByRole('button') - const tooltipEl = getByText('This is description for the trigger element') - expect(triggerEL).toHaveAttribute('aria-describedby', tooltipEl.id) - }) - it('should render the tooltip element with role="tooltip" when the tooltip type is describe', () => { - const {getByText} = HTMLRender( - , - ) - expect(getByText('This is description for the trigger element')).toHaveAttribute('role', 'tooltip') - }) - it('should display the tooltip when the trigger element is hovered', async () => { - const {getByRole, getByText} = HTMLRender() - const triggerEL = getByRole('button') - const user = userEvent.setup() - await user.hover(triggerEL) - expect(getByText('label type tooltip')).toHaveAttribute('data-state', 'open') - }) - it('should display the tooltip when the trigger element is focused', () => { - const {getByRole, getByText} = HTMLRender() - const triggerEL = getByRole('button') - act(() => { - triggerEL.focus() - }) - expect(getByText('label type tooltip')).toHaveAttribute('data-state', 'open') - }) - it('should hide the tooltip when the trigger element is blurred', async () => { - const {getByRole, getByText} = HTMLRender() - const triggerEL = getByRole('button') - const user = userEvent.setup() - act(() => { - triggerEL.focus() - }) - await user.keyboard('{TAB}') - expect(getByText('label type tooltip')).not.toHaveAttribute('data-state', 'open') - }) - it('should hide the tooltip when the ESC key is pressed', async () => { - const {getByRole, getByText} = HTMLRender() - const triggerEL = getByRole('button') - const user = userEvent.setup() - act(() => { - triggerEL.focus() - }) - await user.keyboard('{Escape}') - expect(getByText('label type tooltip')).not.toHaveAttribute('data-state', 'open') - }) -}) diff --git a/src/Tooltip/index.ts b/src/Tooltip/index.ts deleted file mode 100644 index ba15f407377..00000000000 --- a/src/Tooltip/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Tooltip' diff --git a/src/_TextInputInnerAction.tsx b/src/_TextInputInnerAction.tsx index dbbe086d3ac..492ceb8a96a 100644 --- a/src/_TextInputInnerAction.tsx +++ b/src/_TextInputInnerAction.tsx @@ -2,7 +2,7 @@ import React, {forwardRef} from 'react' import {IconProps} from '@primer/octicons-react' import Box from './Box' import {Button, IconButton, ButtonProps} from './Button' -import {Tooltip} from './Tooltip' +import Tooltip from './Tooltip' import {BetterSystemStyleObject, merge, SxProp} from './sx' type TextInputActionProps = Omit< diff --git a/src/__tests__/TextInput.test.tsx b/src/__tests__/TextInput.test.tsx index 6ab50f64269..2257963b8fa 100644 --- a/src/__tests__/TextInput.test.tsx +++ b/src/__tests__/TextInput.test.tsx @@ -76,6 +76,36 @@ describe('TextInput', () => { ).toMatchSnapshot() }) + it('renders trailingAction text button with a tooltip', () => { + const handleAction = jest.fn() + expect( + render( + + Clear + + } + />, + ), + ).toMatchSnapshot() + }) + + it('renders trailingAction icon button', () => { + const handleAction = jest.fn() + expect( + render( + } + />, + ), + ).toMatchSnapshot() + }) + it('focuses the text input if you do not click the input element', () => { const {container, getByLabelText} = HTMLRender( <> diff --git a/src/__tests__/Tooltip.test.tsx b/src/__tests__/Tooltip.test.tsx new file mode 100644 index 00000000000..2f96bfb1728 --- /dev/null +++ b/src/__tests__/Tooltip.test.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import Tooltip, {TooltipProps} from '../Tooltip' +import {render, renderClasses, rendersClass, behavesAsComponent, checkExports} from '../utils/testing' +import {render as HTMLRender} from '@testing-library/react' +import {axe, toHaveNoViolations} from 'jest-axe' + +expect.extend(toHaveNoViolations) + +describe('Tooltip', () => { + behavesAsComponent({Component: Tooltip}) + + checkExports('Tooltip', { + default: Tooltip, + }) + + it('should have no axe violations', async () => { + const {container} = HTMLRender() + const results = await axe(container) + expect(results).toHaveNoViolations() + }) + + 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('respects the "wrap" prop', () => { + expect(rendersClass(, 'tooltipped-multiline')).toBe(true) + }) +}) diff --git a/src/Tooltip/__tests__/Tooltip.types.test.tsx b/src/__tests__/Tooltip.types.test.tsx similarity index 88% rename from src/Tooltip/__tests__/Tooltip.types.test.tsx rename to src/__tests__/Tooltip.types.test.tsx index 65d27666df2..c9280121588 100644 --- a/src/Tooltip/__tests__/Tooltip.types.test.tsx +++ b/src/__tests__/Tooltip.types.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {Tooltip} from '..' +import Tooltip from '../Tooltip' export function shouldAcceptCallWithNoProps() { return diff --git a/src/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/__tests__/__snapshots__/TextInput.test.tsx.snap index 9848aabca2e..085b35d792a 100644 --- a/src/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -1250,6 +1250,638 @@ exports[`TextInput renders small 1`] = ` `; +exports[`TextInput renders trailingAction icon button 1`] = ` +.c2 { + margin-left: 4px; + margin-right: 4px; + line-height: 0; +} + +.c4 { + border-radius: 6px; + border: 1px solid; + border-color: transparent; + font-family: inherit; + font-weight: 500; + font-size: 14px; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-text-decoration: none; + text-decoration: none; + text-align: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + height: 32px; + padding: 0 12px; + gap: 8px; + min-width: -webkit-max-content; + min-width: -moz-max-content; + min-width: max-content; + -webkit-transition: 80ms cubic-bezier(0.65,0,0.35,1); + transition: 80ms cubic-bezier(0.65,0,0.35,1); + -webkit-transition-property: color,fill,background-color,border-color; + transition-property: color,fill,background-color,border-color; + color: #0969da; + background-color: transparent; + box-shadow: none; +} + +.c4:focus:not(:disabled) { + box-shadow: none; + outline: 2px solid #0969da; + outline-offset: -2px; +} + +.c4:focus:not(:disabled):not(:focus-visible) { + outline: solid 1px transparent; +} + +.c4:focus-visible:not(:disabled) { + box-shadow: none; + outline: 2px solid #0969da; + outline-offset: -2px; +} + +.c4[href] { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c4[href]:hover { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c4:hover { + -webkit-transition-duration: 80ms; + transition-duration: 80ms; +} + +.c4:active { + -webkit-transition: none; + transition: none; +} + +.c4:disabled { + cursor: not-allowed; + box-shadow: none; + color: #8c959f; +} + +.c4:disabled [data-component=ButtonCounter] { + color: inherit; +} + +.c4 [data-component=ButtonCounter] { + font-size: 14px; +} + +.c4[data-component=IconButton] { + display: inline-grid; + padding: unset; + place-content: center; + width: 32px; + min-width: unset; +} + +.c4[data-size="small"] { + padding: 0 8px; + height: 28px; + gap: 4px; + font-size: 12px; +} + +.c4[data-size="small"] [data-component="text"] { + line-height: calc(20 / 12); +} + +.c4[data-size="small"] [data-component=ButtonCounter] { + font-size: 12px; +} + +.c4[data-size="small"] [data-component="buttonContent"] > :not(:last-child) { + margin-right: 4px; +} + +.c4[data-size="small"][data-component=IconButton] { + width: 28px; + padding: unset; +} + +.c4[data-size="large"] { + padding: 0 16px; + height: 40px; + gap: 8px; +} + +.c4[data-size="large"] [data-component="buttonContent"] > :not(:last-child) { + margin-right: 8px; +} + +.c4[data-size="large"][data-component=IconButton] { + width: 40px; + padding: unset; +} + +.c4[data-block="block"] { + width: 100%; +} + +.c4 [data-component="leadingVisual"] { + grid-area: leadingVisual; + color: #656d76; +} + +.c4 [data-component="text"] { + grid-area: text; + line-height: calc(20/14); + white-space: nowrap; +} + +.c4 [data-component="trailingVisual"] { + grid-area: trailingVisual; +} + +.c4 [data-component="trailingAction"] { + margin-right: -4px; + color: #656d76; +} + +.c4 [data-component="buttonContent"] { + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + display: grid; + grid-template-areas: "leadingVisual text trailingVisual"; + grid-template-columns: min-content minmax(0,auto) min-content; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; +} + +.c4 [data-component="buttonContent"] > :not(:last-child) { + margin-right: 8px; +} + +.c4:hover:not([disabled]) { + background-color: #f3f4f6; +} + +.c4:active:not([disabled]) { + background-color: hsla(220,14%,94%,1); +} + +.c4[aria-expanded=true] { + background-color: hsla(220,14%,94%,1); +} + +.c4[data-component="IconButton"][data-no-visuals] { + color: #656d76; +} + +.c4[data-no-visuals] { + color: #0969da; +} + +.c4:has([data-component="ButtonCounter"]) { + color: #0969da; +} + +.c4[data-size="small"][data-no-visuals="true"] { + padding-top: 2px; + padding-right: 4px; + padding-bottom: 2px; + padding-left: 4px; + position: relative; +} + +.c4[data-size="small"][data-no-visuals="true"][data-component="IconButton"] { + width: var(--inner-action-size); + height: var(--inner-action-size); +} + +.c0 { + font-size: 14px; + line-height: 20px; + color: #1F2328; + vertical-align: middle; + background-color: #ffffff; + border: 1px solid #d0d7de; + border-radius: 6px; + outline: none; + box-shadow: inset 0 1px 0 rgba(208,215,222,0.2); + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + min-height: 32px; + background-repeat: no-repeat; + background-position: right 8px center; + padding-left: 0; + padding-right: 0; +} + +.c0 input, +.c0 textarea { + cursor: text; +} + +.c0 select { + cursor: pointer; +} + +.c0::-webkit-input-placeholder { + color: #6e7781; +} + +.c0::-moz-placeholder { + color: #6e7781; +} + +.c0:-ms-input-placeholder { + color: #6e7781; +} + +.c0::placeholder { + color: #6e7781; +} + +.c0 > textarea { + padding: 12px; +} + +.c0 > :not(:last-child) { + margin-right: 8px; +} + +.c0 .TextInput-icon, +.c0 .TextInput-action { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + color: #656d76; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c0 > input, +.c0 > select { + padding-left: 12px; + padding-right: 0; +} + +.c1 { + border: 0; + font-size: inherit; + font-family: inherit; + background-color: transparent; + -webkit-appearance: none; + color: inherit; + width: 100%; +} + +.c1:focus { + outline: 0; +} + +.c3 { + position: relative; +} + +.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","Noto Sans",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:focus-within::before, +.c3:hover::after, +.c3:active::after, +.c3:focus::after, +.c3:focus-within::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:focus-within::before, +.c3.tooltipped-no-delay:hover::after, +.c3.tooltipped-no-delay:active::after, +.c3.tooltipped-no-delay:focus::after, +.c3.tooltipped-no-delay:focus-within::after { + -webkit-animation-delay: 0s; + animation-delay: 0s; +} + +.c3.tooltipped-multiline:hover::after, +.c3.tooltipped-multiline:active::after, +.c3.tooltipped-multiline:focus::after, +.c3.tooltipped-multiline:focus-within::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; + } +} + +@media (pointer:coarse) { + .c4[data-size="small"][data-no-visuals="true"]:after { + content: ""; + position: absolute; + left: 0; + right: 0; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + top: 50%; + min-height: 44px; + } +} + +@media (min-width:768px) { + .c0 { + font-size: 14px; + } +} + + + + + + + + + +`; + exports[`TextInput renders trailingAction text button 1`] = ` .c2 { margin-left: 4px; @@ -1257,14 +1889,408 @@ exports[`TextInput renders trailingAction text button 1`] = ` line-height: 0; } -.c4 { +.c4 { + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c3 { + border-radius: 6px; + border: 1px solid; + border-color: transparent; + font-family: inherit; + font-weight: 500; + font-size: 14px; + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-text-decoration: none; + text-decoration: none; + text-align: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + height: 32px; + padding: 0 12px; + gap: 8px; + min-width: -webkit-max-content; + min-width: -moz-max-content; + min-width: max-content; + -webkit-transition: 80ms cubic-bezier(0.65,0,0.35,1); + transition: 80ms cubic-bezier(0.65,0,0.35,1); + -webkit-transition-property: color,fill,background-color,border-color; + transition-property: color,fill,background-color,border-color; + color: #0969da; + background-color: transparent; + box-shadow: none; +} + +.c3:focus:not(:disabled) { + box-shadow: none; + outline: 2px solid #0969da; + outline-offset: -2px; +} + +.c3:focus:not(:disabled):not(:focus-visible) { + outline: solid 1px transparent; +} + +.c3:focus-visible:not(:disabled) { + box-shadow: none; + outline: 2px solid #0969da; + outline-offset: -2px; +} + +.c3[href] { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c3[href]:hover { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c3:hover { + -webkit-transition-duration: 80ms; + transition-duration: 80ms; +} + +.c3:active { + -webkit-transition: none; + transition: none; +} + +.c3:disabled { + cursor: not-allowed; + box-shadow: none; + color: #8c959f; +} + +.c3:disabled [data-component=ButtonCounter] { + color: inherit; +} + +.c3 [data-component=ButtonCounter] { + font-size: 14px; +} + +.c3[data-component=IconButton] { + display: inline-grid; + padding: unset; + place-content: center; + width: 32px; + min-width: unset; +} + +.c3[data-size="small"] { + padding: 0 8px; + height: 28px; + gap: 4px; + font-size: 12px; +} + +.c3[data-size="small"] [data-component="text"] { + line-height: calc(20 / 12); +} + +.c3[data-size="small"] [data-component=ButtonCounter] { + font-size: 12px; +} + +.c3[data-size="small"] [data-component="buttonContent"] > :not(:last-child) { + margin-right: 4px; +} + +.c3[data-size="small"][data-component=IconButton] { + width: 28px; + padding: unset; +} + +.c3[data-size="large"] { + padding: 0 16px; + height: 40px; + gap: 8px; +} + +.c3[data-size="large"] [data-component="buttonContent"] > :not(:last-child) { + margin-right: 8px; +} + +.c3[data-size="large"][data-component=IconButton] { + width: 40px; + padding: unset; +} + +.c3[data-block="block"] { + width: 100%; +} + +.c3 [data-component="leadingVisual"] { + grid-area: leadingVisual; + color: #656d76; +} + +.c3 [data-component="text"] { + grid-area: text; + line-height: calc(20/14); + white-space: nowrap; +} + +.c3 [data-component="trailingVisual"] { + grid-area: trailingVisual; +} + +.c3 [data-component="trailingAction"] { + margin-right: -4px; + color: #656d76; +} + +.c3 [data-component="buttonContent"] { + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + display: grid; + grid-template-areas: "leadingVisual text trailingVisual"; + grid-template-columns: min-content minmax(0,auto) min-content; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; +} + +.c3 [data-component="buttonContent"] > :not(:last-child) { + margin-right: 8px; +} + +.c3:hover:not([disabled]) { + background-color: #f3f4f6; +} + +.c3:active:not([disabled]) { + background-color: hsla(220,14%,94%,1); +} + +.c3[aria-expanded=true] { + background-color: hsla(220,14%,94%,1); +} + +.c3[data-component="IconButton"][data-no-visuals] { + color: #656d76; +} + +.c3[data-no-visuals] { + color: #0969da; +} + +.c3:has([data-component="ButtonCounter"]) { + color: #0969da; +} + +.c3[data-no-visuals="true"] { + padding-top: 2px; + padding-right: 4px; + padding-bottom: 2px; + padding-left: 4px; + position: relative; +} + +.c3[data-no-visuals="true"][data-component="IconButton"] { + width: var(--inner-action-size); + height: var(--inner-action-size); +} + +.c0 { + font-size: 14px; + line-height: 20px; + color: #1F2328; + vertical-align: middle; + background-color: #ffffff; + border: 1px solid #d0d7de; + border-radius: 6px; + outline: none; + box-shadow: inset 0 1px 0 rgba(208,215,222,0.2); + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + min-height: 32px; + background-repeat: no-repeat; + background-position: right 8px center; + padding-left: 0; + padding-right: 0; +} + +.c0 input, +.c0 textarea { + cursor: text; +} + +.c0 select { + cursor: pointer; +} + +.c0::-webkit-input-placeholder { + color: #6e7781; +} + +.c0::-moz-placeholder { + color: #6e7781; +} + +.c0:-ms-input-placeholder { + color: #6e7781; +} + +.c0::placeholder { + color: #6e7781; +} + +.c0 > textarea { + padding: 12px; +} + +.c0 > :not(:last-child) { + margin-right: 8px; +} + +.c0 .TextInput-icon, +.c0 .TextInput-action { + -webkit-align-self: center; + -ms-flex-item-align: center; + align-self: center; + color: #656d76; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c0 > input, +.c0 > select { + padding-left: 12px; + padding-right: 0; +} + +.c1 { + border: 0; + font-size: inherit; + font-family: inherit; + background-color: transparent; + -webkit-appearance: none; + color: inherit; + width: 100%; +} + +.c1:focus { + outline: 0; +} + +@media (forced-colors:active) { + .c3:focus { + outline: solid 1px transparent; + } +} + +@media (pointer:coarse) { + .c3[data-no-visuals="true"]:after { + content: ""; + position: absolute; + left: 0; + right: 0; + -webkit-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + top: 50%; + min-height: 44px; + } +} + +@media (min-width:768px) { + .c0 { + font-size: 14px; + } +} + + + + + + + +`; + +exports[`TextInput renders trailingAction text button with a tooltip 1`] = ` +.c2 { + margin-left: 4px; + margin-right: 4px; + line-height: 0; +} + +.c5 { -webkit-box-pack: center; -webkit-justify-content: center; -ms-flex-pack: center; justify-content: center; } -.c3 { +.c4 { border-radius: 6px; border: 1px solid; border-color: transparent; @@ -1309,59 +2335,59 @@ exports[`TextInput renders trailingAction text button 1`] = ` box-shadow: none; } -.c3:focus:not(:disabled) { +.c4:focus:not(:disabled) { box-shadow: none; outline: 2px solid #0969da; outline-offset: -2px; } -.c3:focus:not(:disabled):not(:focus-visible) { +.c4:focus:not(:disabled):not(:focus-visible) { outline: solid 1px transparent; } -.c3:focus-visible:not(:disabled) { +.c4:focus-visible:not(:disabled) { box-shadow: none; outline: 2px solid #0969da; outline-offset: -2px; } -.c3[href] { +.c4[href] { display: -webkit-inline-box; display: -webkit-inline-flex; display: -ms-inline-flexbox; display: inline-flex; } -.c3[href]:hover { +.c4[href]:hover { -webkit-text-decoration: none; text-decoration: none; } -.c3:hover { +.c4:hover { -webkit-transition-duration: 80ms; transition-duration: 80ms; } -.c3:active { +.c4:active { -webkit-transition: none; transition: none; } -.c3:disabled { +.c4:disabled { cursor: not-allowed; box-shadow: none; color: #8c959f; } -.c3:disabled [data-component=ButtonCounter] { +.c4:disabled [data-component=ButtonCounter] { color: inherit; } -.c3 [data-component=ButtonCounter] { +.c4 [data-component=ButtonCounter] { font-size: 14px; } -.c3[data-component=IconButton] { +.c4[data-component=IconButton] { display: inline-grid; padding: unset; place-content: center; @@ -1369,70 +2395,70 @@ exports[`TextInput renders trailingAction text button 1`] = ` min-width: unset; } -.c3[data-size="small"] { +.c4[data-size="small"] { padding: 0 8px; height: 28px; gap: 4px; font-size: 12px; } -.c3[data-size="small"] [data-component="text"] { +.c4[data-size="small"] [data-component="text"] { line-height: calc(20 / 12); } -.c3[data-size="small"] [data-component=ButtonCounter] { +.c4[data-size="small"] [data-component=ButtonCounter] { font-size: 12px; } -.c3[data-size="small"] [data-component="buttonContent"] > :not(:last-child) { +.c4[data-size="small"] [data-component="buttonContent"] > :not(:last-child) { margin-right: 4px; } -.c3[data-size="small"][data-component=IconButton] { +.c4[data-size="small"][data-component=IconButton] { width: 28px; padding: unset; } -.c3[data-size="large"] { +.c4[data-size="large"] { padding: 0 16px; height: 40px; gap: 8px; } -.c3[data-size="large"] [data-component="buttonContent"] > :not(:last-child) { +.c4[data-size="large"] [data-component="buttonContent"] > :not(:last-child) { margin-right: 8px; } -.c3[data-size="large"][data-component=IconButton] { +.c4[data-size="large"][data-component=IconButton] { width: 40px; padding: unset; } -.c3[data-block="block"] { +.c4[data-block="block"] { width: 100%; } -.c3 [data-component="leadingVisual"] { +.c4 [data-component="leadingVisual"] { grid-area: leadingVisual; color: #656d76; } -.c3 [data-component="text"] { +.c4 [data-component="text"] { grid-area: text; line-height: calc(20/14); white-space: nowrap; } -.c3 [data-component="trailingVisual"] { +.c4 [data-component="trailingVisual"] { grid-area: trailingVisual; } -.c3 [data-component="trailingAction"] { +.c4 [data-component="trailingAction"] { margin-right: -4px; color: #656d76; } -.c3 [data-component="buttonContent"] { +.c4 [data-component="buttonContent"] { -webkit-flex: 1 0 auto; -ms-flex: 1 0 auto; flex: 1 0 auto; @@ -1448,35 +2474,35 @@ exports[`TextInput renders trailingAction text button 1`] = ` align-content: center; } -.c3 [data-component="buttonContent"] > :not(:last-child) { +.c4 [data-component="buttonContent"] > :not(:last-child) { margin-right: 8px; } -.c3:hover:not([disabled]) { +.c4:hover:not([disabled]) { background-color: #f3f4f6; } -.c3:active:not([disabled]) { +.c4:active:not([disabled]) { background-color: hsla(220,14%,94%,1); } -.c3[aria-expanded=true] { +.c4[aria-expanded=true] { background-color: hsla(220,14%,94%,1); } -.c3[data-component="IconButton"][data-no-visuals] { +.c4[data-component="IconButton"][data-no-visuals] { color: #656d76; } -.c3[data-no-visuals] { +.c4[data-no-visuals] { color: #0969da; } -.c3:has([data-component="ButtonCounter"]) { +.c4:has([data-component="ButtonCounter"]) { color: #0969da; } -.c3[data-no-visuals="true"] { +.c4[data-no-visuals="true"] { padding-top: 2px; padding-right: 4px; padding-bottom: 2px; @@ -1484,7 +2510,7 @@ exports[`TextInput renders trailingAction text button 1`] = ` position: relative; } -.c3[data-no-visuals="true"][data-component="IconButton"] { +.c4[data-no-visuals="true"][data-component="IconButton"] { width: var(--inner-action-size); height: var(--inner-action-size); } @@ -1578,14 +2604,239 @@ exports[`TextInput renders trailingAction text button 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","Noto Sans",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:focus-within::before, +.c3:hover::after, +.c3:active::after, +.c3:focus::after, +.c3:focus-within::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:focus-within::before, +.c3.tooltipped-no-delay:hover::after, +.c3.tooltipped-no-delay:active::after, +.c3.tooltipped-no-delay:focus::after, +.c3.tooltipped-no-delay:focus-within::after { + -webkit-animation-delay: 0s; + animation-delay: 0s; +} + +.c3.tooltipped-multiline:hover::after, +.c3.tooltipped-multiline:active::after, +.c3.tooltipped-multiline:focus::after, +.c3.tooltipped-multiline:focus-within::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) { - .c3:focus { + .c4:focus { outline: solid 1px transparent; } } @media (pointer:coarse) { - .c3[data-no-visuals="true"]:after { + .c4[data-no-visuals="true"]:after { content: ""; position: absolute; left: 0; @@ -1622,24 +2873,30 @@ exports[`TextInput renders trailingAction text button 1`] = ` - + + `; diff --git a/src/__tests__/__snapshots__/Tooltip.test.tsx.snap b/src/__tests__/__snapshots__/Tooltip.test.tsx.snap new file mode 100644 index 00000000000..d461c34404c --- /dev/null +++ b/src/__tests__/__snapshots__/Tooltip.test.tsx.snap @@ -0,0 +1,232 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Tooltip renders consistently 1`] = ` +.c0 { + position: relative; +} + +.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::after { + position: absolute; + z-index: 1000000; + display: none; + padding: 0.5em 0.75em; + font: normal normal 11px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",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; +} + +.c0:hover::before, +.c0:active::before, +.c0:focus::before, +.c0:focus-within::before, +.c0:hover::after, +.c0:active::after, +.c0:focus::after, +.c0:focus-within::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:focus-within::before, +.c0.tooltipped-no-delay:hover::after, +.c0.tooltipped-no-delay:active::after, +.c0.tooltipped-no-delay:focus::after, +.c0.tooltipped-no-delay:focus-within::after { + -webkit-animation-delay: 0s; + animation-delay: 0s; +} + +.c0.tooltipped-multiline:hover::after, +.c0.tooltipped-multiline:active::after, +.c0.tooltipped-multiline:focus::after, +.c0.tooltipped-multiline:focus-within::after { + display: table-cell; +} + +.c0.tooltipped-s::after, +.c0.tooltipped-se::after, +.c0.tooltipped-sw::after { + 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; + border-bottom-color: #24292f; +} + +.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; + border-left-color: #24292f; +} + +.c0.tooltipped-e::after { + bottom: 50%; + 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; + border-right-color: #24292f; +} + +.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; +} + +.c0.tooltipped-align-left-2::after { + left: 0; + margin-left: 0; +} + +.c0.tooltipped-align-left-2::before { + left: 10px; +} + + +`; diff --git a/src/index.ts b/src/index.ts index cd1d3b4de92..335f542f443 100644 --- a/src/index.ts +++ b/src/index.ts @@ -168,7 +168,7 @@ export type { } from './Timeline' export {default as Token, IssueLabelToken, AvatarToken} from './Token' export type {TokenProps} from './Token' -export {Tooltip} from './Tooltip' +export {default as Tooltip} from './Tooltip' export type {TooltipProps} from './Tooltip' export {default as Truncate} from './Truncate' export type {TruncateProps} from './Truncate' diff --git a/src/Tooltip/Tooltip.stories.tsx b/src/stories/Tooltip.stories.tsx similarity index 54% rename from src/Tooltip/Tooltip.stories.tsx rename to src/stories/Tooltip.stories.tsx index e51629f8e05..316b0f469b3 100644 --- a/src/Tooltip/Tooltip.stories.tsx +++ b/src/stories/Tooltip.stories.tsx @@ -1,11 +1,12 @@ import React from 'react' import {Meta} from '@storybook/react' -import {BaseStyles, ThemeProvider, Button} from '..' -import {Tooltip} from '../Tooltip' +import {BaseStyles, ThemeProvider, IconButton} from '..' import Box from '../Box' +import Tooltip from '../Tooltip' +import {SearchIcon} from '@primer/octicons-react' export default { - title: 'Components/Tooltip', + title: 'Components/Tooltip/Default', component: Tooltip, decorators: [ @@ -21,11 +22,10 @@ export default { ], } as Meta -// As a label for an IconButton -export const Default = () => ( +export const IconButtonTooltip = () => ( - - + + )