diff --git a/.changeset/iconbutton-default-tooltip.md b/.changeset/iconbutton-default-tooltip.md deleted file mode 100644 index 7fb2b407016..00000000000 --- a/.changeset/iconbutton-default-tooltip.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@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 deleted file mode 100644 index acaea20e976..00000000000 --- a/.changeset/improved-tooltip.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@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 0eceefea19b..2af6b14024b 100644 --- a/docs/content/IconButton.mdx +++ b/docs/content/IconButton.mdx @@ -35,16 +35,6 @@ 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 ` - -``` - -### With direction - -Set direction of tooltip with `direction`. The tooltip is responsive and will automatically adjust direction to avoid cutting off. - -```jsx live - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -## 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 ceb0be69e41..cb5ec85d317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "35.2.1", "license": "MIT", "dependencies": { - "@primer/behaviors": "^1.1.3", + "@primer/behaviors": "1.1.1", "@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.3", - "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.3.tgz", - "integrity": "sha512-WpCcjAkXG7Lv3ZbaCUgASWKHnCi/pmuSEiyTmHHb6f5xhwk1mliixNL5ZZHtDN6RCcT3VnXUsyek4GopG2lbZQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.1.tgz", + "integrity": "sha512-wvF1PYjyxKNTr6+5w4uR5Gkz53t1fsRDgKjWxDKk7wmlh0cwiILBo4dDFjjVhWRF1mBSjaIxxJGB4WGaP7ct2Q==" }, "node_modules/@primer/octicons-react": { "version": "16.1.1", @@ -38627,9 +38627,9 @@ "dev": true }, "@primer/behaviors": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.3.tgz", - "integrity": "sha512-WpCcjAkXG7Lv3ZbaCUgASWKHnCi/pmuSEiyTmHHb6f5xhwk1mliixNL5ZZHtDN6RCcT3VnXUsyek4GopG2lbZQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@primer/behaviors/-/behaviors-1.1.1.tgz", + "integrity": "sha512-wvF1PYjyxKNTr6+5w4uR5Gkz53t1fsRDgKjWxDKk7wmlh0cwiILBo4dDFjjVhWRF1mBSjaIxxJGB4WGaP7ct2Q==" }, "@primer/octicons-react": { "version": "16.1.1", diff --git a/package.json b/package.json index 45b1490df10..c847db92cd2 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "npm": ">=7" }, "dependencies": { - "@primer/behaviors": "^1.1.3", + "@primer/behaviors": "1.1.1", "@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 0f55c557a84..800915dc3a9 100644 --- a/src/Button/Button.stories.tsx +++ b/src/Button/Button.stories.tsx @@ -1,10 +1,9 @@ -import {BellIcon, EyeClosedIcon, EyeIcon, SearchIcon, TriangleDownIcon, XIcon} from '@primer/octicons-react' +import {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', @@ -94,35 +93,6 @@ 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 e4e13a4758d..401650829fc 100644 --- a/src/Button/IconButton.tsx +++ b/src/Button/IconButton.tsx @@ -4,51 +4,22 @@ 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, - disableTooltip = false, - 'aria-label': ariaLabel, - ...rest - } = props + const {variant = 'default', size = 'medium', sx: sxProp = {}, icon: Icon, ...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 147e1f7c812..7568bd3910d 100644 --- a/src/Button/types.ts +++ b/src/Button/types.ts @@ -48,8 +48,6 @@ 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 new file mode 100644 index 00000000000..0e3baa22b2d --- /dev/null +++ b/src/Tooltip.tsx @@ -0,0 +1,263 @@ +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 deleted file mode 100644 index bd92e1317aa..00000000000 --- a/src/Tooltip/index.tsx +++ /dev/null @@ -1,180 +0,0 @@ -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 d33d63f0f0b..255f8c842f6 100644 --- a/src/_TextInputInnerAction.tsx +++ b/src/_TextInputInnerAction.tsx @@ -41,7 +41,7 @@ const invisibleButtonStyleOverrides = { const ConditionalTooltip: React.FC<{ ['aria-label']?: string - children: React.ReactElement + children: React.ReactNode }> = ({'aria-label': ariaLabel, children}) => ( <> {ariaLabel ? ( diff --git a/src/__tests__/ActionMenu.test.tsx b/src/__tests__/ActionMenu.test.tsx index 29fee6e3066..7aa30c560ec 100644 --- a/src/__tests__/ActionMenu.test.tsx +++ b/src/__tests__/ActionMenu.test.tsx @@ -3,11 +3,10 @@ import 'babel-polyfill' import {axe, toHaveNoViolations} from 'jest-axe' import React from 'react' import theme from '../theme' -import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider, IconButton} from '..' +import {ActionMenu, ActionList, BaseStyles, ThemeProvider, SSRProvider} 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 { @@ -137,32 +136,6 @@ 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 fc8d0ca69dc..7e917445f9e 100644 --- a/src/__tests__/Button.test.tsx +++ b/src/__tests__/Button.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {SSRProvider, IconButton, Button} from '../' +import {IconButton, Button} from '../Button' import {behavesAsComponent} from '../utils/testing' import {render, cleanup, fireEvent} from '@testing-library/react' import {axe, toHaveNoViolations} from 'jest-axe' @@ -91,21 +91,13 @@ 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 5a9fed253c5..1516917ca2b 100644 --- a/src/__tests__/TextInput.test.tsx +++ b/src/__tests__/TextInput.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {SSRProvider, TextInput} from '..' +import {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,13 +68,11 @@ describe('TextInput', () => { const handleAction = jest.fn() expect( render( - - Clear} - /> - + Clear} + /> ) ).toMatchSnapshot() }) @@ -83,17 +81,15 @@ describe('TextInput', () => { const handleAction = jest.fn() expect( render( - - - Clear - - } - /> - + + Clear + + } + /> ) ).toMatchSnapshot() }) @@ -102,24 +98,22 @@ 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 */} - + ) const icon = container.querySelector('svg')! diff --git a/src/__tests__/Tooltip.test.tsx b/src/__tests__/Tooltip.test.tsx index 92ecbc0f8bb..4a285eea46e 100644 --- a/src/__tests__/Tooltip.test.tsx +++ b/src/__tests__/Tooltip.test.tsx @@ -1,45 +1,52 @@ import React from 'react' -import 'babel-polyfill' -import {Tooltip, TooltipContext} from '../Tooltip' -import {SSRProvider} from '..' -import {behavesAsComponent, checkExports, checkStoriesForAxeViolations} from '../utils/testing' +import Tooltip, {TooltipProps} from '../Tooltip' +import {render, renderClasses, rendersClass, behavesAsComponent, checkExports} from '../utils/testing' import {render as HTMLRender, cleanup} from '@testing-library/react' import {axe, toHaveNoViolations} from 'jest-axe' -import '@testing-library/jest-dom' +import 'babel-polyfill' expect.extend(toHaveNoViolations) -const Fixture = () => { - return ( - - - - - - ) -} - describe('Tooltip', () => { - behavesAsComponent({ - Component: Tooltip, - options: {skipAs: true, skipSx: true}, - toRender: () => - }) + behavesAsComponent({Component: Tooltip}) - checkExports('Tooltip', {default: undefined, Tooltip, TooltipContext}) + checkExports('Tooltip', { + default: Tooltip + }) it('should have no axe violations', async () => { - const {container} = HTMLRender() + const {container} = HTMLRender() const results = await axe(container) expect(results).toHaveNoViolations() cleanup() }) - it('tooltip should not be visible by default', () => { - const component = HTMLRender() - expect(component.getByText('tooltip text')).not.toBeVisible() - 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) }) - checkStoriesForAxeViolations('Tooltip/fixtures') - checkStoriesForAxeViolations('Tooltip/examples') + 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/__tests__/Tooltip.types.test.tsx b/src/__tests__/Tooltip.types.test.tsx index 59c0e45ac54..c9280121588 100644 --- a/src/__tests__/Tooltip.types.test.tsx +++ b/src/__tests__/Tooltip.types.test.tsx @@ -1,8 +1,7 @@ import React from 'react' -import {Tooltip} from '../Tooltip' +import Tooltip from '../Tooltip' -export function shouldNotAcceptCallWithMissingProps() { - // @ts-expect-error props missing +export function shouldAcceptCallWithNoProps() { return } diff --git a/src/__tests__/__snapshots__/Button.test.tsx.snap b/src/__tests__/__snapshots__/Button.test.tsx.snap index bdeeeee95bf..f54c410ac25 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`] = ` } - @@ -1853,90 +1981,6 @@ 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; @@ -2120,6 +2164,226 @@ 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; @@ -2165,10 +2429,11 @@ exports[`TextInput renders trailingAction text button with a tooltip 1`] = ` className="c2 TextInput-action" > - diff --git a/src/__tests__/__snapshots__/Tooltip.test.tsx.snap b/src/__tests__/__snapshots__/Tooltip.test.tsx.snap index 83b5ed41551..25e5a7b14ab 100644 --- a/src/__tests__/__snapshots__/Tooltip.test.tsx.snap +++ b/src/__tests__/__snapshots__/Tooltip.test.tsx.snap @@ -2,104 +2,226 @@ exports[`Tooltip renders consistently 1`] = ` .c0 { - line-height: 1; + position: relative; } -.c0:hover [data-component=tooltip], -.c0:focus-within [data-component=tooltip] { - visibility: visible; - opacity: 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; } -.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; +.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; 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; + -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; } -.c1:before { - content: ""; - width: 0; - height: 0; - border: 5px solid transparent; - position: absolute; +.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; } -.c1[data-side=outside-top]::before { - border-top: 5px solid; - border-top-color: #24292f; +.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; +} + +.c0.tooltipped-multiline:hover::after, +.c0.tooltipped-multiline:active::after, +.c0.tooltipped-multiline:focus::after { + display: table-cell; +} + +.c0.tooltipped-s::after, +.c0.tooltipped-se::after, +.c0.tooltipped-sw::after { top: 100%; + right: 50%; + margin-top: 6px; } -.c1[data-side=outside-bottom]::before { - border-bottom: 5px solid; +.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; - top: -10px; } -.c1[data-side=outside-left]::before { - border-left: 5px solid; +.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; - top: calc(50% - 5px); +} + +.c0.tooltipped-e::after { + bottom: 50%; left: 100%; + margin-left: 6px; + -webkit-transform: translateY(50%); + -ms-transform: translateY(50%); + transform: translateY(50%); } -.c1[data-side=outside-right]::before { - border-right: 5px solid; +.c0.tooltipped-e::before { + top: 50%; + right: -7px; + bottom: 50%; + margin-top: -6px; border-right-color: #24292f; - top: calc(50% - 5px); - left: -10px; } -.c1[data-align=start][data-side=outside-top]::before, -.c1[data-align=start][data-side=outside-bottom]::before { - left: 8px; +.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=center][data-side=outside-top]::before, -.c1[data-align=center][data-side=outside-bottom]::before { - left: calc(50% - 4px); +.c0.tooltipped-align-left-2::after { + left: 0; + margin-left: 0; } -.c1[data-align=end][data-side=outside-top]::before, -.c1[data-align=end][data-side=outside-bottom]::before { - left: calc(100% - 16px); +.c0.tooltipped-align-left-2::before { + left: 10px; } - - - + className="c0 tooltipped-n" + role="tooltip" +/> `; diff --git a/src/__tests__/hooks/useAnchoredPosition.test.tsx b/src/__tests__/hooks/useAnchoredPosition.test.tsx index 4b7d38dc710..4038ca73c0e 100644 --- a/src/__tests__/hooks/useAnchoredPosition.test.tsx +++ b/src/__tests__/hooks/useAnchoredPosition.test.tsx @@ -23,7 +23,6 @@ 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 435e8e9b295..f4ce005e4f7 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 {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/stories/ActionMenu/fixtures.stories.tsx b/src/stories/ActionMenu/fixtures.stories.tsx index 0b077e45656..8e74c3677f9 100644 --- a/src/stories/ActionMenu/fixtures.stories.tsx +++ b/src/stories/ActionMenu/fixtures.stories.tsx @@ -269,7 +269,6 @@ 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 new file mode 100644 index 00000000000..59d034282f1 --- /dev/null +++ b/src/stories/Tooltip.stories.tsx @@ -0,0 +1,37 @@ +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 deleted file mode 100644 index 51217c340fd..00000000000 --- a/src/stories/Tooltip/examples.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -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 deleted file mode 100644 index 401492fb3db..00000000000 --- a/src/stories/Tooltip/fixtures.stories.tsx +++ /dev/null @@ -1,108 +0,0 @@ -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 ( - - - - - - - - - ) -} - -export const TypeLabel = () => { - return ( - - - - ) -} - -export const AcceptsSx = () => { - return ( - - - - ) -} - -export const BackwardCompatibility = () => { - return ( - - - - - - - - - - - - ) -} diff --git a/src/stories/behaviors-anchored-position.stories.tsx b/src/stories/behaviors-anchored-position.stories.tsx deleted file mode 100644 index 2917b954022..00000000000 --- a/src/stories/behaviors-anchored-position.stories.tsx +++ /dev/null @@ -1,345 +0,0 @@ -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 ( - - - - {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 - - - - - ) -}