Skip to content

Commit

Permalink
feat: contextual help (#118)
Browse files Browse the repository at this point in the history
* feat: contextual help

* rm unusued

* v0.9.22-0

* v0.9.22
  • Loading branch information
mikeldking committed Apr 5, 2023
1 parent b5d3655 commit 1fa66b7
Show file tree
Hide file tree
Showing 17 changed files with 409 additions and 30 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.9.21",
"version": "0.9.22",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
29 changes: 28 additions & 1 deletion src/button/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -22,7 +29,7 @@ function ActionButton(
ref: FocusableRef<HTMLButtonElement>
) {
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 });

Expand All @@ -36,12 +43,32 @@ function ActionButton(
'is-hovered': isHovered,
})}
style={style}
css={isQuiet ? quietButtonCSS : null}
>
{children}
</button>
);
}

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.
Expand Down
6 changes: 1 addition & 5 deletions src/content/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,7 @@ function Heading(props: HeadingProps, ref: DOMRef<HTMLHeadingElement>) {
return (
<HeadingTag
{...otherProps}
css={css`
${headingCSS};
${headingSizeCSS(level)};
${headingWeightCSS(weight)};
`}
css={css(headingCSS, headingSizeCSS(level), headingWeightCSS(weight))}
ref={domRef}
>
{children}
Expand Down
76 changes: 76 additions & 0 deletions src/contextualhelp/ContextualHelp.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>
) {
let {
variant = 'help',
placement = 'bottom start',
children,
delay = 0,
...otherProps
} = props;

const iconSVG = variant === 'info' ? <InfoOutline /> : <QuestionOutline />;
const icon = <Icon svg={iconSVG} style={{ fontSize: 'inherit' }} />;

let labelProps = useLabels(otherProps);

return (
<TooltipTrigger {...otherProps} placement={placement} delay={delay}>
<ActionButton
{...mergeProps(otherProps, labelProps, { isDisabled: false })}
ref={ref}
isQuiet
>
{icon}
</ActionButton>
<HelpTooltip>{children}</HelpTooltip>
</TooltipTrigger>
);
}

/**
* 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 };
1 change: 1 addition & 0 deletions src/contextualhelp/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ContextualHelp';
5 changes: 4 additions & 1 deletion src/field/FieldLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ function FieldLabel(props: FieldLabelProps, ref: DOMRef<HTMLLabelElement>) {
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}
Expand All @@ -90,6 +91,8 @@ function FieldLabel(props: FieldLabelProps, ref: DOMRef<HTMLLabelElement>) {
className="ac-field-label__label-extra"
css={css`
margin-left: ${theme.spacing.padding4}px;
display: inline-flex;
align-items: center;
`}
>
{labelExtra}
Expand Down
18 changes: 18 additions & 0 deletions src/icon/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,24 @@ export const InfoOutline = () => (
</svg>
);

export const QuestionOutline = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g data-name="Layer 2">
<g data-name="menu-arrow-circle">
<rect
width="24"
height="24"
transform="rotate(180 12 12)"
opacity="0"
/>
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z" />
<path d="M12 6a3.5 3.5 0 0 0-3.5 3.5 1 1 0 0 0 2 0A1.5 1.5 0 1 1 12 11a1 1 0 0 0-1 1v2a1 1 0 0 0 2 0v-1.16A3.49 3.49 0 0 0 12 6z" />
<circle cx="12" cy="17" r="1" />
</g>
</g>
</svg>
);

export const InfoFilled = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g data-name="Layer 2">
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> {
// /** custom content, defaults to 'the snozzberries taste like snozzberries' */
Expand Down
1 change: 1 addition & 0 deletions src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const theme = {
},
tooltip: {
backgroundColor: '#3C4C5D',
borderColor: lighten(0.3, '#3C4C5D'),
},
actionTooltip: {
backgroundColor: '#2D3845',
Expand Down
70 changes: 70 additions & 0 deletions src/tooltip/HelpTooltip.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
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 (
<div
id={id}
{...tooltipProps}
style={style}
className={classNames(
'ac-help-tooltip',
`ac-help-tooltip--${placement}`,
{
'is-open': isOpen,
}
)}
ref={overlayRef}
css={helpTooltipCSS({ placement })}
>
{props.children}
</div>
);
}

/**
* Display container for Tooltip content. Has a directional arrow dependent on its placement.
*/
let _HelpTooltip = React.forwardRef(HelpTooltip);
export { _HelpTooltip as HelpTooltip };
1 change: 1 addition & 0 deletions src/tooltip/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './TooltipTrigger';
export * from './Tooltip';
export * from './ActionTooltip';
export * from './TriggerWrap';
export * from './HelpTooltip';
70 changes: 68 additions & 2 deletions src/tooltip/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
`;
};
Loading

0 comments on commit 1fa66b7

Please sign in to comment.