Skip to content

Commit eab002f

Browse files
ktaborsLFDanLu
authored andcommitted
ContextualHelp (#162)
* initial ContextualHelp implementation * Adding ContextualHelp to FieldLabel * fix crash in textfield contextual help story * Updating ContextualHelp positioning in FieldLabel * field label and contextual help button sizes match * adding contextualhelp to monopackage for release * storybook autodocs working * Improving ContextualHelp positioning * adding combobox contextualhelp story * Improving popover focus * Improving popover focus * Improving contextual help placement in field and aria labeling * updating links to have hrefs * Fixing button context and popover style * updated useId and aria-label in field label --------- Co-authored-by: Kyle Taborski <[email protected]> Co-authored-by: danilu <[email protected]>
1 parent 386dcc4 commit eab002f

15 files changed

+383
-79
lines changed

packages/@react-spectrum/s2/src/CheckboxGroup.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {StyleProps, field, getAllowedOverrides} from './style-utils' with {type:
2323
import {useDOMRef} from '@react-spectrum/utils';
2424
import {CheckboxContext} from './Checkbox';
2525

26-
export interface CheckboxGroupProps extends Omit<AriaCheckboxGroupProps, 'className' | 'style' | 'children'>, StyleProps, Omit<SpectrumLabelableProps, 'contextualHelp'>, HelpTextProps {
26+
export interface CheckboxGroupProps extends Omit<AriaCheckboxGroupProps, 'className' | 'style' | 'children'>, StyleProps, SpectrumLabelableProps, HelpTextProps {
2727
/**
2828
* The size of the Checkboxes in the CheckboxGroup.
2929
*
@@ -91,7 +91,8 @@ function CheckboxGroup(props: CheckboxGroupProps, ref: DOMRef<HTMLDivElement>) {
9191
size={size}
9292
labelPosition={labelPosition}
9393
labelAlign={labelAlign}
94-
necessityIndicator={necessityIndicator}>
94+
necessityIndicator={necessityIndicator}
95+
contextualHelp={props.contextualHelp}>
9596
{label}
9697
</FieldLabel>
9798
<div

packages/@react-spectrum/s2/src/ColorField.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {Ref, forwardRef, useContext, useImperativeHandle, useRef} from 'react';
2424
import {TextFieldRef} from '@react-types/textfield';
2525
import {createFocusableRef} from '@react-spectrum/utils';
2626

27-
export interface ColorFieldProps extends Omit<AriaColorFieldProps, 'children' | 'className' | 'style'>, StyleProps, Omit<SpectrumLabelableProps, 'contextualHelp'>, HelpTextProps {
27+
export interface ColorFieldProps extends Omit<AriaColorFieldProps, 'children' | 'className' | 'style'>, StyleProps, SpectrumLabelableProps, HelpTextProps {
2828
/**
2929
* The size of the color field.
3030
*
@@ -81,7 +81,8 @@ function ColorField(props: ColorFieldProps, ref: Ref<TextFieldRef>) {
8181
size={props.size}
8282
labelPosition={labelPosition}
8383
labelAlign={labelAlign}
84-
necessityIndicator={necessityIndicator}>
84+
necessityIndicator={necessityIndicator}
85+
contextualHelp={props.contextualHelp}>
8586
{label}
8687
</FieldLabel>
8788
<FieldGroup role="presentation" isDisabled={isDisabled} isInvalid={isInvalid} size={props.size}>

packages/@react-spectrum/s2/src/ColorHandle.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
import {ColorThumb} from 'react-aria-components';
1414
import {style} from '../style/spectrum-theme' with {type: 'macro'};
15-
import {RefObject, cloneElement, useId, useState} from 'react';
16-
import {useLayoutEffect} from '@react-aria/utils';
15+
import {RefObject, cloneElement, useState} from 'react';
16+
import {useId, useLayoutEffect} from '@react-aria/utils';
1717
import {createPortal} from 'react-dom';
1818
import {keyframes} from '../style/style-macro' with {type: 'macro'};
1919

packages/@react-spectrum/s2/src/ComboBox.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export interface ComboBoxProps<T extends object> extends
6868
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection'>,
6969
ComboboxStyleProps,
7070
StyleProps,
71-
Omit<SpectrumLabelableProps, 'contextualHelp'>,
71+
SpectrumLabelableProps,
7272
HelpTextProps,
7373
Pick<ListBoxProps<T>, 'items'>,
7474
Pick<AriaPopoverProps, 'shouldFlip'> {
@@ -217,7 +217,8 @@ function ComboBox<T extends object>(props: ComboBoxProps<T>, ref: FocusableRef<H
217217
size={size}
218218
labelPosition={labelPosition}
219219
labelAlign={labelAlign}
220-
necessityIndicator={necessityIndicator}>
220+
necessityIndicator={necessityIndicator}
221+
contextualHelp={props.contextualHelp}>
221222
{label}
222223
</FieldLabel>
223224
<FieldGroup
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import {ActionButton, ActionButtonProps} from './ActionButton';
2+
import {AriaLabelingProps, DOMProps, FocusableRef} from '@react-types/shared';
3+
import {ContentContext, FooterContext, HeadingContext} from './Content';
4+
import {ContextValue, Dialog as RACDialog, DEFAULT_SLOT, Provider, TextContext, useContextProps} from 'react-aria-components';
5+
import {ReactNode, createContext, forwardRef} from 'react';
6+
import {dialogInner} from './Dialog';
7+
import {DialogTrigger, DialogTriggerProps} from './DialogTrigger';
8+
import {filterDOMProps, mergeProps, useLabels} from '@react-aria/utils';
9+
import HelpIcon from '../s2wf-icons/assets/svg/S2_Icon_HelpCircle_20_N.svg';
10+
import InfoIcon from '../s2wf-icons/assets/svg/S2_Icon_InfoCircle_20_N.svg';
11+
import {mergeStyles} from '../style/runtime';
12+
import {Popover, PopoverProps} from './Popover';
13+
import {size as styleSize, style} from '../style/spectrum-theme' with {type: 'macro'};
14+
import {StyleProps} from './style-utils' with { type: 'macro' };
15+
import {useFocusableRef} from '@react-spectrum/utils';
16+
17+
export interface ContextualHelpStyleProps {
18+
/**
19+
* Indicates whether contents are informative or provides helpful guidance.
20+
*
21+
* @default 'help'
22+
*/
23+
variant?: 'info' | 'help'
24+
}
25+
export interface ContextualHelpProps extends
26+
Pick<DialogTriggerProps, 'isOpen' | 'defaultOpen' | 'onOpenChange' | 'shouldFlip' | 'offset' | 'crossOffset' | 'placement'>,
27+
Pick<PopoverProps, 'containerPadding'>,
28+
Pick<ActionButtonProps, 'size'>,
29+
ContextualHelpStyleProps, StyleProps, DOMProps, AriaLabelingProps {
30+
/** Contents of the Contextual Help popover. */
31+
children?: ReactNode
32+
}
33+
34+
const popover = style({
35+
fontFamily: 'sans',
36+
minWidth: '[218px]',
37+
width: '[218px]',
38+
padding: 24
39+
});
40+
41+
export const ContextualHelpContext = createContext<ContextValue<ContextualHelpProps, HTMLButtonElement>>({});
42+
43+
function ContextualHelp(props: ContextualHelpProps, ref: FocusableRef<HTMLButtonElement>) {
44+
let domRef = useFocusableRef(ref);
45+
[props, domRef] = useContextProps(props, domRef, ContextualHelpContext);
46+
let {
47+
children,
48+
defaultOpen,
49+
// containerPadding = 24, // See popover() above. Issue noted in Popover.tsx.
50+
size = 'XS',
51+
crossOffset,
52+
isOpen,
53+
offset = 8,
54+
onOpenChange,
55+
placement = 'bottom start',
56+
shouldFlip,
57+
UNSAFE_className,
58+
UNSAFE_style,
59+
variant = 'help'
60+
} = props;
61+
62+
// In a FieldLabel we're getting the context's aria-labeledby, so we need to
63+
// manually set the aria-label after useLabels() to keep the order of label
64+
// then ContextualHelp variant
65+
let labelProps = useLabels(props);
66+
// Translate variant
67+
labelProps['aria-label'] = labelProps['aria-label'] ? labelProps['aria-label'] + ' ' + variant : variant;
68+
69+
let buttonProps = filterDOMProps(props, {labelable: true});
70+
71+
return (
72+
<DialogTrigger
73+
isOpen={isOpen}
74+
defaultOpen={defaultOpen}
75+
onOpenChange={onOpenChange}>
76+
<ActionButton
77+
slot={null}
78+
ref={ref}
79+
size={size}
80+
{...mergeProps(buttonProps, labelProps)}
81+
UNSAFE_style={UNSAFE_style}
82+
UNSAFE_className={UNSAFE_className}
83+
isQuiet>
84+
{variant === 'info' ? <InfoIcon /> : <HelpIcon />}
85+
</ActionButton>
86+
<Popover
87+
placement={placement}
88+
shouldFlip={shouldFlip}
89+
// not working => containerPadding={containerPadding}
90+
offset={offset}
91+
crossOffset={crossOffset}
92+
hideArrow
93+
UNSAFE_className={popover}>
94+
<RACDialog className={mergeStyles(dialogInner, style({borderRadius: 'none'}))}>
95+
<Provider
96+
values={[
97+
[TextContext, {
98+
slots: {
99+
[DEFAULT_SLOT]: {}
100+
}
101+
}],
102+
[HeadingContext, {className: style({
103+
fontSize: 'heading-xs',
104+
fontWeight: 'heading',
105+
lineHeight: 'heading',
106+
color: 'heading',
107+
margin: 0,
108+
marginBottom: styleSize(8) // This only makes it 10px on mobile and should be 12px
109+
})}],
110+
[ContentContext, {className: style({
111+
fontSize: 'ui',
112+
fontWeight: 'normal',
113+
lineHeight: 'body',
114+
color: 'body'
115+
})}],
116+
[FooterContext, {className: style({
117+
fontSize: 'ui',
118+
lineHeight: 'body',
119+
color: 'body',
120+
marginTop: 16
121+
})}]
122+
]}>
123+
{children}
124+
</Provider>
125+
</RACDialog>
126+
</Popover>
127+
</DialogTrigger>
128+
);
129+
}
130+
131+
/**
132+
* Contextual help shows a user extra information about the state of an adjacent component, or a total view.
133+
*/
134+
let _ContextualHelp = forwardRef(ContextualHelp);
135+
export {_ContextualHelp as ContextualHelp};

packages/@react-spectrum/s2/src/Dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ function Dialog(props: DialogProps, ref: DOMRef) {
154154
let _Dialog = forwardRef(Dialog);
155155
export {_Dialog as Dialog};
156156

157-
const dialogInner = style({
157+
export const dialogInner = style({
158158
display: 'flex',
159159
flexDirection: 'column',
160160
flexGrow: 1,

0 commit comments

Comments
 (0)