From 1fa66b78bdb9c61ee18a7a9f719f4d1cea9ff01f Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Wed, 5 Apr 2023 11:40:33 -0600 Subject: [PATCH] feat: contextual help (#118) * feat: contextual help * rm unusued * v0.9.22-0 * v0.9.22 --- package.json | 2 +- src/button/ActionButton.tsx | 29 ++++++++- src/content/Heading.tsx | 6 +- src/contextualhelp/ContextualHelp.tsx | 76 +++++++++++++++++++++++ src/contextualhelp/index.tsx | 1 + src/field/FieldLabel.tsx | 5 +- src/icon/Icons.tsx | 18 ++++++ src/index.tsx | 1 + src/theme.ts | 1 + src/tooltip/HelpTooltip.tsx | 70 ++++++++++++++++++++++ src/tooltip/index.ts | 1 + src/tooltip/styles.ts | 70 +++++++++++++++++++++- src/types/dialog.ts | 2 +- stories/ContextualHelp.stories.tsx | 86 +++++++++++++++++++++++++++ stories/Picker.stories.tsx | 11 +++- stories/TextArea.stories.tsx | 8 +-- stories/TextField.stories.tsx | 52 ++++++++++++---- 17 files changed, 409 insertions(+), 30 deletions(-) create mode 100644 src/contextualhelp/ContextualHelp.tsx create mode 100644 src/contextualhelp/index.tsx create mode 100644 src/tooltip/HelpTooltip.tsx create mode 100644 stories/ContextualHelp.stories.tsx diff --git a/package.json b/package.json index d6d2ccbc..e255e120 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.9.21", + "version": "0.9.22", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/button/ActionButton.tsx b/src/button/ActionButton.tsx index 37d1590c..1e4098ac 100644 --- a/src/button/ActionButton.tsx +++ b/src/button/ActionButton.tsx @@ -6,9 +6,16 @@ import { useHover } from '@react-aria/interactions'; import { FocusableRef, PressEvents } from '../types'; import { useFocusableRef } from '../utils/useDOMRef'; import { BaseButtonProps } from '../types/button'; +import { css } from '@emotion/react'; +import { theme } from '../theme'; interface ActionButtonProps extends BaseButtonProps, PressEvents { style?: CSSProperties; + /** + * Whether the button gets button styles removed or not + * @default false + */ + isQuiet?: boolean; } /** @@ -22,7 +29,7 @@ function ActionButton( ref: FocusableRef ) { let domRef = useFocusableRef(ref); - const { isDisabled, children, style, ...otherProps } = props; + const { isDisabled, children, style, isQuiet = false, ...otherProps } = props; const { buttonProps, isPressed } = useButton(props, domRef); const { hoverProps, isHovered } = useHover({ isDisabled }); @@ -36,12 +43,32 @@ function ActionButton( 'is-hovered': isHovered, })} style={style} + css={isQuiet ? quietButtonCSS : null} > {children} ); } +const quietButtonCSS = css` + border: none; + margin: 0; + padding: 0.2em; + color: inherit; + background: none; + cursor: pointer; + border-radius: ${theme.rounding.rounding4}px; + opacity: 0.8; + transition: all 0.2s ease-in-out; + &:hover { + opacity: 1; + background-color: ${theme.colors.gray500}; + } + svg { + padding: var(--ac-dimension-size-85); + } +`; + /** * ActionButtons allow users to perform an action. * They’re used for similar, task-based options within a workflow, and are ideal for interfaces where buttons aren’t meant to draw a lot of attention. diff --git a/src/content/Heading.tsx b/src/content/Heading.tsx index 15c6d83d..b5dbaf59 100644 --- a/src/content/Heading.tsx +++ b/src/content/Heading.tsx @@ -92,11 +92,7 @@ function Heading(props: HeadingProps, ref: DOMRef) { return ( {children} diff --git a/src/contextualhelp/ContextualHelp.tsx b/src/contextualhelp/ContextualHelp.tsx new file mode 100644 index 00000000..2fa20645 --- /dev/null +++ b/src/contextualhelp/ContextualHelp.tsx @@ -0,0 +1,76 @@ +import { ActionButton } from '../button'; +import { + AriaLabelingProps, + DOMProps, + FocusableRef, + OverlayTriggerProps, + Placement, + PositionProps, +} from '../types'; + +import { mergeProps, useLabels } from '@react-aria/utils'; +import React, { ReactNode } from 'react'; + +import { Icon, InfoOutline, QuestionOutline } from '../icon'; +import { HelpTooltip, TooltipTrigger } from '../tooltip'; + +export interface ContextualHelpProps + extends OverlayTriggerProps, + PositionProps, + DOMProps, + AriaLabelingProps { + /** Contents of the Contextual Help popover. */ + children: ReactNode; + /** + * Indicates whether contents are informative or provides helpful guidance. + * @default 'help' + */ + variant?: 'help' | 'info'; + /** + * The placement of the popover with respect to the action button. + * @default 'bottom start' + */ + placement?: Placement; + /** + * The delay time for the tooltip to show up in milliseconds + * @default 0 + */ + delay?: number; +} + +function ContextualHelp( + props: ContextualHelpProps, + ref: FocusableRef +) { + let { + variant = 'help', + placement = 'bottom start', + children, + delay = 0, + ...otherProps + } = props; + + const iconSVG = variant === 'info' ? : ; + const icon = ; + + let labelProps = useLabels(otherProps); + + return ( + + + {icon} + + {children} + + ); +} + +/** + * Contextual help shows a user extra information about the state of an adjacent component, or a total view. + */ +let _ContextualHelp = React.forwardRef(ContextualHelp); +export { _ContextualHelp as ContextualHelp }; diff --git a/src/contextualhelp/index.tsx b/src/contextualhelp/index.tsx new file mode 100644 index 00000000..08698e90 --- /dev/null +++ b/src/contextualhelp/index.tsx @@ -0,0 +1 @@ +export * from './ContextualHelp'; diff --git a/src/field/FieldLabel.tsx b/src/field/FieldLabel.tsx index 74dd8f41..956ee2cd 100644 --- a/src/field/FieldLabel.tsx +++ b/src/field/FieldLabel.tsx @@ -63,7 +63,8 @@ function FieldLabel(props: FieldLabelProps, ref: DOMRef) { font-weight: ${theme.typography.weights.heavy}; color: ${theme.textColors.white90}; padding: ${theme.spacing.padding4}px 0; - display: inline-block; + display: inline-flex; + align-items: center; `} > {children} @@ -90,6 +91,8 @@ function FieldLabel(props: FieldLabelProps, ref: DOMRef) { className="ac-field-label__label-extra" css={css` margin-left: ${theme.spacing.padding4}px; + display: inline-flex; + align-items: center; `} > {labelExtra} diff --git a/src/icon/Icons.tsx b/src/icon/Icons.tsx index 0d867707..e6d7e47f 100644 --- a/src/icon/Icons.tsx +++ b/src/icon/Icons.tsx @@ -364,6 +364,24 @@ export const InfoOutline = () => ( ); +export const QuestionOutline = () => ( + + + + + + + + + + +); + export const InfoFilled = () => ( diff --git a/src/index.tsx b/src/index.tsx index 95d7bbc5..4b8cbbcb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -29,6 +29,7 @@ export * from './navlist'; export * from './progress'; export * from './switch'; export * from './empty'; +export * from './contextualhelp'; export { theme, designationColors } from './theme'; // export interface Props extends HTMLAttributes { // /** custom content, defaults to 'the snozzberries taste like snozzberries' */ diff --git a/src/theme.ts b/src/theme.ts index 18c7db20..e4e17dd0 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -80,6 +80,7 @@ export const theme = { }, tooltip: { backgroundColor: '#3C4C5D', + borderColor: lighten(0.3, '#3C4C5D'), }, actionTooltip: { backgroundColor: '#2D3845', diff --git a/src/tooltip/HelpTooltip.tsx b/src/tooltip/HelpTooltip.tsx new file mode 100644 index 00000000..5d5bba2e --- /dev/null +++ b/src/tooltip/HelpTooltip.tsx @@ -0,0 +1,70 @@ +import React, { ReactNode, useContext, HTMLProps, CSSProperties } from 'react'; +import { useTooltip } from '@react-aria/tooltip'; +import { classNames } from '../utils'; +import { PlacementAxis, DOMRef } from '../types'; +import { mergeProps } from '@react-aria/utils'; +import { TooltipContext } from '../tooltip/context'; +import { helpTooltipCSS } from './styles'; + +interface HelpTooltipProps extends HTMLProps { + isOpen?: boolean; + /** + * The placement of the element with respect to its anchor element. + * @default 'right' + */ + placement?: PlacementAxis; + children: ReactNode; + /** + * Style overrides. Not guaranteed to be not overridden + */ + UNSAFE_style?: CSSProperties; +} + +/** + * A variant of the tooltip that is intended for users to gain info or help + * @param props + * @returns + */ +function HelpTooltip(props: HelpTooltipProps, _ref: DOMRef) { + const { + ref: overlayRef, + arrowProps, + state, + ...tooltipProviderProps + } = useContext(TooltipContext); + + props = mergeProps(props, tooltipProviderProps); + const { placement = 'right', isOpen, style: propsStyle, id } = props; + const { tooltipProps } = useTooltip(props, state); + + const style = { + ...props?.UNSAFE_style, + ...propsStyle, + ...tooltipProps.style, + }; + + return ( +
+ {props.children} +
+ ); +} + +/** + * Display container for Tooltip content. Has a directional arrow dependent on its placement. + */ +let _HelpTooltip = React.forwardRef(HelpTooltip); +export { _HelpTooltip as HelpTooltip }; diff --git a/src/tooltip/index.ts b/src/tooltip/index.ts index 5cdc651c..b0545e7a 100644 --- a/src/tooltip/index.ts +++ b/src/tooltip/index.ts @@ -2,3 +2,4 @@ export * from './TooltipTrigger'; export * from './Tooltip'; export * from './ActionTooltip'; export * from './TriggerWrap'; +export * from './HelpTooltip'; diff --git a/src/tooltip/styles.ts b/src/tooltip/styles.ts index 5fa933c1..d4723ca7 100644 --- a/src/tooltip/styles.ts +++ b/src/tooltip/styles.ts @@ -33,6 +33,7 @@ export const tooltipCSS = ({ placement }: { placement: PlacementAxis }) => { --tooltip-animation-distance: ${theme.spacing.tooltip.offset}px; --tooltip-target-offset: ${theme.spacing.tooltip.offset}px; --tooltip-tip-width: 8px; + --tooltip-max-inline-size: 200px; color: ${theme.textColors.white90}; background-color: ${tooltipStyles.backgroundColor}; position: relative; @@ -48,7 +49,7 @@ export const tooltipCSS = ({ placement }: { placement: PlacementAxis }) => { padding: ${theme.spacing.padding8}px; border-radius: 4px; min-height: 24px; - max-inline-size: 200px; + max-inline-size: var(--tooltip-max-inline-size); word-break: break-word; -webkit-font-smoothing: antialiased; @@ -156,6 +157,7 @@ export const actionTooltipCSS = ({ --tooltip-animation-distance: ${theme.spacing.tooltip.offset}px; --tooltip-target-offset: ${theme.spacing.tooltip.offset}px; --tooltip-tip-width: 8px; + --tooltip-max-inline-size: 500px; color: ${theme.textColors.white90}; background-color: ${actionTooltipStyles.backgroundColor}; border-radius: 8px; @@ -172,7 +174,7 @@ export const actionTooltipCSS = ({ border-radius: 4px; min-height: 24px; - max-inline-size: 500px; + max-inline-size: var(--tooltip-max-inline-size); word-break: break-word; -webkit-font-smoothing: antialiased; @@ -195,3 +197,67 @@ export const actionTooltipHeaderWrap = css` border-bottom: 1px solid ${actionTooltipStyles.borderColor}; padding: ${theme.spacing.padding8}px; `; + +export const helpTooltipCSS = ({ placement }: { placement: PlacementAxis }) => { + let transformCSS = css``; + switch (placement) { + case 'bottom': + transformCSS = css` + transform: translateY(var(--tooltip-animation-distance)); + `; + break; + case 'top': + transformCSS = css` + transform: translateY(calc(-1 * var(--tooltip-animation-distance))); + `; + break; + case 'left': + transformCSS = css` + transform: translateX(calc(-1 * var(--tooltip-animation-distance))); + `; + break; + case 'right': + transformCSS = css` + transform: translateX(var(--tooltip-animation-distance)); + `; + break; + } + return css` + --tooltip-animation-distance: ${theme.spacing.tooltip.offset}px; + --tooltip-target-offset: ${theme.spacing.tooltip.offset}px; + --tooltip-max-inline-size: 300px; + color: ${theme.textColors.white90}; + background-color: ${tooltipStyles.backgroundColor}; + position: relative; + box-sizing: border-box; + font-size: ${theme.typography.sizes.medium.fontSize}px; + + vertical-align: top; + + width: auto; + padding: ${theme.spacing.padding16}px; + border-radius: ${theme.rounding.rounding4}px; + border: 1px solid ${tooltipStyles.borderColor}; + min-height: 24px; + max-inline-size: var(--tooltip-max-inline-size); + box-shadow: 0 4px 4px 4px rgba(0, 0, 0, 0.1); + + word-break: break-word; + -webkit-font-smoothing: antialiased; + + visibility: hidden; + opacity: 0; + transition: transform 200ms ease-in-out, opacity 200ms ease-in-out, + visibility 200ms linear; + &.is-open { + visibility: visible; + opacity: 1; + transition-delay: 0ms; + pointer-events: auto; + ${transformCSS}; + } + .ac-content { + margin: ${theme.spacing.margin8}px 0 ${theme.spacing.margin8}px 0; + } + `; +}; diff --git a/src/types/dialog.ts b/src/types/dialog.ts index 981ea341..05562ebf 100644 --- a/src/types/dialog.ts +++ b/src/types/dialog.ts @@ -28,7 +28,7 @@ export interface DialogProps extends AriaDialogProps { /** * The title that will be displayed at the top of the dialog. */ - title: ReactNode; + title?: ReactNode; /** * component to display on the top-right of the dialog. Useful for things like pagination */ diff --git a/stories/ContextualHelp.stories.tsx b/stories/ContextualHelp.stories.tsx new file mode 100644 index 00000000..a142e814 --- /dev/null +++ b/stories/ContextualHelp.stories.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { Meta, Story } from '@storybook/react'; +import { withDesign } from 'storybook-addon-designs'; +import { + ActionButton, + Provider, + Placement, + ContextualHelp, + Text, + Heading, + theme, + Content, +} from '../src'; + +import css from '@emotion/css'; + +const placements: Placement[] = [ + 'start', + 'end', + 'right', + 'left', + 'top', + 'bottom', + 'top start', + 'top end', + 'bottom start', + 'bottom end', +]; + +const meta: Meta = { + title: 'ContextualHelp', + component: ContextualHelp, + decorators: [withDesign], + argTypes: { + placement: { + control: { + type: 'select', + options: placements, + }, + }, + }, + parameters: { + controls: { expanded: true }, + design: { + type: 'figma', + url: + 'https://www.figma.com/file/5mMInYH9JdJY389s8iBVQm/Component-Library?node-id=503%3A0', + }, + }, +}; + +export default meta; + +export const Gallery = () => ( + +
    + {placements.map((placement, index) => { + return ( +
  • + + + Need help? + + + + If you're having issues accessing your account, contact our + customer support team for help. + + +
    + Learn more about accounts +
    +
    +
  • + ); + })} +
+
+); diff --git a/stories/Picker.stories.tsx b/stories/Picker.stories.tsx index 53ed5f55..1edbb50b 100644 --- a/stories/Picker.stories.tsx +++ b/stories/Picker.stories.tsx @@ -1,6 +1,13 @@ import React from 'react'; import { Meta, Story } from '@storybook/react'; -import { Item, Picker, PickerProps, Text, Button } from '../src'; +import { + Item, + Picker, + PickerProps, + Text, + Button, + ContextualHelp, +} from '../src'; import { Provider } from '../src'; import { css } from '@emotion/react'; import InfoTip from './components/InfoTip'; @@ -191,7 +198,7 @@ const Gallery: Story = () => { This is info} + labelExtra={This is info} selectedKey={frequency} onSelectionChange={selected => setFrequency(selected as string)} > diff --git a/stories/TextArea.stories.tsx b/stories/TextArea.stories.tsx index 8f718559..fa463633 100644 --- a/stories/TextArea.stories.tsx +++ b/stories/TextArea.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Meta, Story } from '@storybook/react'; import { withDesign } from 'storybook-addon-designs'; -import { Form, TextArea, TextAreaProps } from '../src'; +import { ContextualHelp, Form, TextArea, TextAreaProps } from '../src'; import InfoTip from './components/InfoTip'; const meta: Meta = { @@ -54,7 +54,7 @@ export const Gallery = () => (