Skip to content

Commit e7f09e2

Browse files
committed
[Page] Add support for tooltips on primaryAction and secondaryActions
1 parent 54cfc5d commit e7f09e2

File tree

17 files changed

+159
-166
lines changed

17 files changed

+159
-166
lines changed

.changeset/famous-emus-learn.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@shopify/polaris': minor
33
---
44

5-
Support tooltip for Page component's primary & secondary actions
5+
Added `Tooltip` support for `Page` `primaryAction` and `secondaryActions`

polaris-react/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Otherwise include the CSS in your HTML. We suggest copying the styles file into
3333
```html
3434
<link
3535
rel="stylesheet"
36-
href="https://unpkg.com/@shopify/[email protected].0/build/esm/styles.css"
36+
href="https://unpkg.com/@shopify/[email protected].1/build/esm/styles.css"
3737
/>
3838
```
3939

@@ -70,7 +70,7 @@ If React doesn’t make sense for your application, you can use a CSS-only versi
7070
```html
7171
<link
7272
rel="stylesheet"
73-
href="https://unpkg.com/@shopify/[email protected].0/build/esm/styles.css"
73+
href="https://unpkg.com/@shopify/[email protected].1/build/esm/styles.css"
7474
/>
7575
```
7676

polaris-react/src/components/ActionMenu/components/Actions/tests/Actions.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {mountWithApp} from 'tests/utilities';
33

44
import {ActionMenuProps, ActionMenu} from '../../..';
55
import {Actions, MenuGroup, RollupActions, SecondaryAction} from '../..';
6+
import {Tooltip} from '../../../../Tooltip';
67

78
describe('<Actions />', () => {
89
const mockProps: ActionMenuProps = {
@@ -40,6 +41,21 @@ describe('<Actions />', () => {
4041
expect(wrapper.findAll(SecondaryAction)).toHaveLength(3);
4142
});
4243

44+
it('renders a <Tooltip /> when helpText is set on an action', () => {
45+
const toolTipAction = {
46+
content: 'Refund',
47+
helpText:
48+
'You need permission from your store administrator to issue refunds.',
49+
};
50+
51+
const wrapper = mountWithApp(<ActionMenu actions={[toolTipAction]} />);
52+
const action = wrapper.find(SecondaryAction);
53+
54+
expect(action).toContainReactComponent(Tooltip, {
55+
content: toolTipAction.helpText,
56+
});
57+
});
58+
4359
it('renders a MenuGroup', () => {
4460
const wrapper = mountWithApp(
4561
<ActionMenu groups={[{title: 'group', actions: []}]} />,
Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
import React, {useEffect, useRef} from 'react';
22

33
import {classNames} from '../../../../utilities/css';
4+
import {Tooltip} from '../../../Tooltip';
45
import {Button} from '../../../Button';
5-
import {tooltipFrom} from '../../../Tooltip';
66
import type {ButtonProps} from '../../../Button';
7-
import type {ActionWithTooltip} from '../../../../types';
87

98
import styles from './SecondaryAction.scss';
109

11-
interface SecondaryAction extends ButtonProps, ActionWithTooltip {
10+
interface SecondaryAction extends ButtonProps {
11+
helpText?: React.ReactNode;
1212
onAction?(): void;
1313
getOffsetWidth?(width: number): void;
1414
}
1515

1616
export function SecondaryAction({
1717
children,
1818
destructive,
19+
helpText,
1920
onAction,
2021
getOffsetWidth,
21-
tooltip,
2222
...rest
2323
}: SecondaryAction) {
2424
const secondaryActionsRef = useRef<HTMLSpanElement>(null);
@@ -29,15 +29,17 @@ export function SecondaryAction({
2929
getOffsetWidth(secondaryActionsRef.current?.offsetWidth);
3030
}, [getOffsetWidth]);
3131

32-
let button = (
32+
const buttonMarkup = (
3333
<Button onClick={onAction} {...rest}>
3434
{children}
3535
</Button>
3636
);
3737

38-
if (tooltip != null) {
39-
button = tooltipFrom(tooltip, button);
40-
}
38+
const actionMarkup = helpText ? (
39+
<Tooltip content={helpText}>{buttonMarkup}</Tooltip>
40+
) : (
41+
buttonMarkup
42+
);
4143

4244
return (
4345
<span
@@ -47,7 +49,7 @@ export function SecondaryAction({
4749
)}
4850
ref={secondaryActionsRef}
4951
>
50-
{button}
52+
{actionMarkup}
5153
</span>
5254
);
5355
}

polaris-react/src/components/ActionMenu/tests/ActionMenu.test.tsx

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -179,41 +179,43 @@ describe('<ActionMenu />', () => {
179179
});
180180
});
181181

182-
it('uses Button and ButtonGroup as subcomponents', () => {
183-
const wrapper = mountWithApp(
184-
<ActionMenu {...mockProps} actions={mockActions} />,
185-
);
182+
describe('<Actions />', () => {
183+
it('uses Button and ButtonGroup as subcomponents', () => {
184+
const wrapper = mountWithApp(
185+
<ActionMenu {...mockProps} actions={mockActions} />,
186+
);
186187

187-
expect(wrapper.findAll(Button)).toHaveLength(2);
188-
expect(wrapper.findAll(ButtonGroup)).toHaveLength(1);
189-
});
188+
expect(wrapper.findAll(Button)).toHaveLength(2);
189+
expect(wrapper.findAll(ButtonGroup)).toHaveLength(1);
190+
});
191+
192+
it('passes action callbacks through to Button', () => {
193+
const spy = jest.fn();
194+
const wrapper = mountWithApp(
195+
<ActionMenu
196+
{...mockProps}
197+
actions={[{content: 'mock', onAction: spy}]}
198+
/>,
199+
);
190200

191-
it('action callbacks are passed through to Button', () => {
192-
const spy = jest.fn();
193-
const wrapper = mountWithApp(
194-
<ActionMenu
195-
{...mockProps}
196-
actions={[{content: 'mock', onAction: spy}]}
197-
/>,
198-
);
201+
wrapper.find(Button)!.trigger('onClick');
199202

200-
wrapper.find(Button)!.trigger('onClick');
203+
expect(spy).toHaveBeenCalledTimes(1);
204+
});
201205

202-
expect(spy).toHaveBeenCalledTimes(1);
203-
});
206+
it('passes `onActionRollup` if set', () => {
207+
const onActionRollup = jest.fn();
208+
const wrapper = mountWithApp(
209+
<ActionMenu
210+
{...mockProps}
211+
actions={[{content: 'mock'}]}
212+
onActionRollup={onActionRollup}
213+
/>,
214+
);
204215

205-
it('renders <Actions /> passing `onActionRollup` as prop if it exists', () => {
206-
const onActionRollup = jest.fn();
207-
const wrapper = mountWithApp(
208-
<ActionMenu
209-
{...mockProps}
210-
actions={[{content: 'mock'}]}
211-
onActionRollup={onActionRollup}
212-
/>,
213-
);
214-
215-
expect(wrapper).toContainReactComponent(Actions, {
216-
onActionRollup,
216+
expect(wrapper).toContainReactComponent(Actions, {
217+
onActionRollup,
218+
});
217219
});
218220
});
219221
});

polaris-react/src/components/Button/utils.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22

33
import type {ComplexAction} from '../../types';
4-
import {tooltipFrom} from '../Tooltip';
54

65
import {Button, ButtonProps} from './Button';
76

@@ -26,15 +25,13 @@ export function buttonsFrom(
2625
}
2726

2827
export function buttonFrom(
29-
{content, onAction, tooltip, ...action}: ComplexAction,
28+
{content, onAction, ...action}: ComplexAction,
3029
overrides?: Partial<ButtonProps>,
3130
key?: any,
3231
) {
33-
const button = (
32+
return (
3433
<Button key={key} onClick={onAction} {...action} {...overrides}>
3534
{content}
3635
</Button>
3736
);
38-
39-
return tooltip ? tooltipFrom(tooltip, button) : button;
4037
}

polaris-react/src/components/Page/README.md

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,30 @@ Use to create a custom secondary action.
254254
</Page>
255255
```
256256

257+
### With tooltip action
258+
259+
Use when merchants or their staff will benefit from context on why a page action is disabled.
260+
261+
````jsx
262+
<Page
263+
title="Product"
264+
primaryAction={{
265+
content: 'Save',
266+
}}
267+
secondaryActions={[
268+
{
269+
content: 'Import',
270+
disabled: true,
271+
helpText: 'You need permission to import products.',
272+
},
273+
]}
274+
>
275+
<Card title="Product X" sectioned>
276+
<p>Product X information</p>
277+
</Card>
278+
</Page>
279+
```
280+
257281
### With subtitle
258282

259283
Use when the page title benefits from secondary content.
@@ -269,7 +293,7 @@ Use when the page title benefits from secondary content.
269293
<p>Credit card information</p>
270294
</Card>
271295
</Page>
272-
```
296+
````
273297
274298
### With external link
275299
@@ -427,31 +451,6 @@ Use when the page needs visual separation between the page header and the conten
427451
</Page>
428452
```
429453

430-
### Page actions with tooltip
431-
432-
Use actions with tooltip to give more context or if the action button is disabled to indicate the reason as to why it's disabled.
433-
434-
```jsx
435-
<Page
436-
title="Product"
437-
primaryAction={{
438-
content: 'Save',
439-
tooltip: <span>Save is an <strong>async</strong> operation</span>,
440-
}}
441-
secondaryActions={[
442-
{
443-
content: 'Import',
444-
disabled: true,
445-
tooltip: 'You need permission to import products',
446-
},
447-
]}
448-
>
449-
<Card title="Product X" sectioned>
450-
<p>Product X information</p>
451-
</Card>
452-
</Page>
453-
```
454-
455454
---
456455

457456
## Related components

polaris-react/src/components/Page/components/Header/Header.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import React from 'react';
22

33
import {classNames} from '../../../../utilities/css';
4-
import {buttonsFrom} from '../../../Button';
4+
import {buttonFrom} from '../../../Button';
55
import {TextStyle} from '../../../TextStyle';
6+
import {Tooltip} from '../../../Tooltip';
67
import {useMediaQuery} from '../../../../utilities/media-query';
78
import {useI18n} from '../../../../utilities/i18n';
89
import {
910
ConditionalRender,
1011
ConditionalWrapper,
1112
} from '../../../../utilities/components';
1213
import type {
13-
ActionWithTooltip,
1414
DestructableAction,
1515
DisableableAction,
1616
IconableAction,
1717
LoadableAction,
1818
MenuActionDescriptor,
1919
MenuGroupDescriptor,
20+
TooltipAction,
2021
} from '../../../../types';
2122
import {Breadcrumbs, BreadcrumbsProps} from '../../../Breadcrumbs';
2223
import {Pagination, PaginationProps} from '../../../Pagination';
@@ -34,7 +35,7 @@ interface PrimaryAction
3435
DisableableAction,
3536
LoadableAction,
3637
IconableAction,
37-
ActionWithTooltip {
38+
TooltipAction {
3839
/** Provides extra visual weight and identifies the primary action in a set of buttons */
3940
primary?: boolean;
4041
}
@@ -231,20 +232,26 @@ function PrimaryActionMarkup({
231232
primaryAction: PrimaryAction | React.ReactNode;
232233
}) {
233234
const {isNavigationCollapsed} = useMediaQuery();
234-
let content = primaryAction;
235-
if (isInterface(primaryAction)) {
236-
const primary =
237-
primaryAction.primary === undefined ? true : primaryAction.primary;
238235

239-
content = buttonsFrom(
236+
let actionMarkup = primaryAction;
237+
if (isInterface(primaryAction)) {
238+
const {primary: isPrimary, helpText} = primaryAction;
239+
const primary = isPrimary === undefined ? true : isPrimary;
240+
const content = buttonFrom(
240241
shouldShowIconOnly(isNavigationCollapsed, primaryAction),
241242
{
242243
primary,
243244
},
244245
);
246+
247+
actionMarkup = helpText ? (
248+
<Tooltip content={helpText}>{content}</Tooltip>
249+
) : (
250+
content
251+
);
245252
}
246253

247-
return <div className={styles.PrimaryActionWrapper}>{content}</div>;
254+
return <div className={styles.PrimaryActionWrapper}>{actionMarkup}</div>;
248255
}
249256

250257
function shouldShowIconOnly(

polaris-react/src/components/Page/components/Header/tests/Header.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {Breadcrumbs} from '../../../../Breadcrumbs';
88
import {Button} from '../../../../Button';
99
import {ButtonGroup} from '../../../../ButtonGroup';
1010
import {Pagination} from '../../../../Pagination';
11+
import {Tooltip} from '../../../../Tooltip';
1112
import type {LinkAction, MenuActionDescriptor} from '../../../../../types';
1213
import {Header, HeaderProps} from '../Header';
1314

@@ -106,6 +107,19 @@ describe('<Header />', () => {
106107

107108
expect(header).toContainReactComponent(PrimaryAction);
108109
});
110+
111+
it('renders a <Tooltip /> when helpText is provided', () => {
112+
const primaryAction = {
113+
content: 'Save',
114+
helpText: 'Helpful text',
115+
};
116+
const header = mountWithApp(
117+
<Header {...mockProps} primaryAction={primaryAction} />,
118+
);
119+
expect(header).toContainReactComponent(Tooltip, {
120+
content: primaryAction.helpText,
121+
});
122+
});
109123
});
110124

111125
describe('pagination', () => {

0 commit comments

Comments
 (0)