diff --git a/packages/kubrick/src/Button/Button.tsx b/packages/kubrick/src/Button/Button.tsx index 82c1b72..5336411 100644 --- a/packages/kubrick/src/Button/Button.tsx +++ b/packages/kubrick/src/Button/Button.tsx @@ -72,6 +72,7 @@ export const Button = forwardRef( ], })} {...mergeProps(buttonProps, hoverProps, focusProps)} + ref={ref} role={role} > {prefix && ( diff --git a/packages/kubrick/src/IconButton/IconButton.module.scss b/packages/kubrick/src/IconButton/IconButton.module.scss index 58b4d3b..6729503 100644 --- a/packages/kubrick/src/IconButton/IconButton.module.scss +++ b/packages/kubrick/src/IconButton/IconButton.module.scss @@ -4,6 +4,7 @@ align-items: center; justify-content: center; width: max-content; + height: max-content; aspect-ratio: 1/1; padding: 6px; diff --git a/packages/kubrick/src/IconButton/IconButton.tsx b/packages/kubrick/src/IconButton/IconButton.tsx index 63c9268..fa996e4 100644 --- a/packages/kubrick/src/IconButton/IconButton.tsx +++ b/packages/kubrick/src/IconButton/IconButton.tsx @@ -79,6 +79,7 @@ export const IconButton = forwardRef( })} {...mergeProps(buttonProps, hoverProps, focusProps)} aria-label={label} + ref={ref} role={role} > {children} diff --git a/packages/kubrick/src/Tooltip/Tooltip.module.scss b/packages/kubrick/src/Tooltip/Tooltip.module.scss new file mode 100644 index 0000000..0c54fba --- /dev/null +++ b/packages/kubrick/src/Tooltip/Tooltip.module.scss @@ -0,0 +1,13 @@ +@use '../../scss/variables/colors'; + +.root { + width: max-content; + max-width: 320px; + max-height: 160px; + padding: 1rem; + overflow-y: scroll; + text-wrap: pretty; + background: colors.$white; + border: 1px solid colors.$gray-100; + box-shadow: 0 1px 1px rgba(0 0 0 / 4%); +} diff --git a/packages/kubrick/src/Tooltip/Tooltip.stories.tsx b/packages/kubrick/src/Tooltip/Tooltip.stories.tsx new file mode 100644 index 0000000..6c4cf64 --- /dev/null +++ b/packages/kubrick/src/Tooltip/Tooltip.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { Icon, wordpress } from '@wordpress/icons'; +import { IconButton } from '../IconButton'; +import { Tooltip } from './Tooltip'; + +const meta: Meta = { + argTypes: { + content: { + control: 'text', + }, + }, + args: { + content: + 'Do not put essential information in a tooltip. Tooltips have low discoverability and have usability issues on devices without hover interactions.', + }, + component: Tooltip, + decorators: [ + (Story) => { + return ( +
+ +
+ ); + }, + ], + parameters: { + controls: { + include: ['content'], + }, + }, + tags: ['autodocs'], + title: 'Components/Tooltip', +}; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ( + + + + + + ), +}; + +export default meta; diff --git a/packages/kubrick/src/Tooltip/Tooltip.tsx b/packages/kubrick/src/Tooltip/Tooltip.tsx new file mode 100644 index 0000000..38b85f7 --- /dev/null +++ b/packages/kubrick/src/Tooltip/Tooltip.tsx @@ -0,0 +1,101 @@ +import { FocusableProvider } from '@react-aria/focus'; +import { mergeProps, useObjectRef } from '@react-aria/utils'; +import { FocusableElement } from '@react-types/shared'; +import { ReactNode, forwardRef, useRef } from 'react'; +import { + AriaTooltipProps, + Overlay, + useOverlayPosition, + useTooltip, + useTooltipTrigger, +} from 'react-aria'; +import { TooltipTriggerProps, useTooltipTriggerState } from 'react-stately'; +import { GlobalProps } from '../types'; +import { useProps } from '../useProps'; +import styles from './Tooltip.module.scss'; + +export const DEFAULT_DELAY = 200; +export const DEFAULT_CLOSE_DELAY = 100; +export const DEFAULT_OFFSET = 5; + +interface TooltipProps + extends GlobalProps, + AriaTooltipProps, + TooltipTriggerProps { + children: ReactNode; + content: ReactNode; + /** + * The additional offset applied along the main axis between the element and its anchor element. + * + * @default 5 + */ + offset?: number; +} + +/** + * ```js + * import { Tooltip } from '@kinsta-martech/spectra-core'; + * ``` + */ +export const Tooltip = forwardRef( + (props, forwardedRef) => { + const { + children, + closeDelay, + content, + delay, + offset = DEFAULT_OFFSET, + role, + } = props; + const tooltipRef = useObjectRef(forwardedRef); + const triggerRef = useRef(null); + const { componentProps, rootProps } = useProps('Tooltip', props); + const state = useTooltipTriggerState({ + ...componentProps, + closeDelay: + typeof closeDelay !== 'number' ? DEFAULT_CLOSE_DELAY : closeDelay, + delay: typeof delay !== 'number' ? DEFAULT_DELAY : delay, + }); + const { overlayProps } = useOverlayPosition({ + isOpen: state.isOpen, + offset, + overlayRef: tooltipRef, + placement: 'left', + targetRef: triggerRef, + }); + const { tooltipProps: triggerTooltipProps, triggerProps } = + useTooltipTrigger(componentProps, state, triggerRef); + const { tooltipProps } = useTooltip( + { + ...mergeProps(componentProps, overlayProps), + ...triggerTooltipProps, + }, + state + ); + + return ( + <> + + {children} + + {state.isOpen && ( + +
+ {content} +
+
+ )} + + ); + } +); + +Tooltip.displayName = 'Tooltip'; diff --git a/packages/kubrick/src/Tooltip/index.ts b/packages/kubrick/src/Tooltip/index.ts new file mode 100644 index 0000000..7594a8f --- /dev/null +++ b/packages/kubrick/src/Tooltip/index.ts @@ -0,0 +1 @@ +export * from './Tooltip'; diff --git a/packages/kubrick/src/index.ts b/packages/kubrick/src/index.ts index 24af6ba..fdb9ab6 100644 --- a/packages/kubrick/src/index.ts +++ b/packages/kubrick/src/index.ts @@ -19,4 +19,5 @@ export * from './Tabs'; export * from './Tabs/Tab'; export * from './TextArea'; export * from './TextField'; +export * from './Tooltip'; export * from './useClasses';