Skip to content

Commit 47e8244

Browse files
aminpakschloerice
andauthored
Support tooltip for Page primary & secondary actions (#6709)
* Add tooltip to Page primary & secondary actions * [Page] Add support for tooltips on primaryAction and secondaryActions Co-authored-by: Chloe Rice <[email protected]>
1 parent 55b7348 commit 47e8244

File tree

12 files changed

+168
-53
lines changed

12 files changed

+168
-53
lines changed

.changeset/famous-emus-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/polaris': minor
3+
---
4+
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: []}]} />,

polaris-react/src/components/ActionMenu/components/SecondaryAction/SecondaryAction.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import React, {useEffect, useRef} from 'react';
22

33
import {classNames} from '../../../../utilities/css';
4+
import {Tooltip} from '../../../Tooltip';
45
import {Button} from '../../../Button';
56
import type {ButtonProps} from '../../../Button';
67

78
import styles from './SecondaryAction.scss';
89

910
interface SecondaryAction extends ButtonProps {
11+
helpText?: React.ReactNode;
1012
onAction?(): void;
1113
getOffsetWidth?(width: number): void;
1214
}
1315

1416
export function SecondaryAction({
1517
children,
1618
destructive,
19+
helpText,
1720
onAction,
1821
getOffsetWidth,
1922
...rest
@@ -26,6 +29,18 @@ export function SecondaryAction({
2629
getOffsetWidth(secondaryActionsRef.current?.offsetWidth);
2730
}, [getOffsetWidth]);
2831

32+
const buttonMarkup = (
33+
<Button onClick={onAction} {...rest}>
34+
{children}
35+
</Button>
36+
);
37+
38+
const actionMarkup = helpText ? (
39+
<Tooltip content={helpText}>{buttonMarkup}</Tooltip>
40+
) : (
41+
buttonMarkup
42+
);
43+
2944
return (
3045
<span
3146
className={classNames(
@@ -34,9 +49,7 @@ export function SecondaryAction({
3449
)}
3550
ref={secondaryActionsRef}
3651
>
37-
<Button onClick={onAction} {...rest}>
38-
{children}
39-
</Button>
52+
{actionMarkup}
4053
</span>
4154
);
4255
}

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/Page/README.md

Lines changed: 24 additions & 0 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.

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +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-
MenuGroupDescriptor,
14-
MenuActionDescriptor,
1514
DestructableAction,
1615
DisableableAction,
17-
LoadableAction,
1816
IconableAction,
17+
LoadableAction,
18+
MenuActionDescriptor,
19+
MenuGroupDescriptor,
20+
TooltipAction,
1921
} from '../../../../types';
2022
import {Breadcrumbs, BreadcrumbsProps} from '../../../Breadcrumbs';
2123
import {Pagination, PaginationProps} from '../../../Pagination';
@@ -32,7 +34,8 @@ interface PrimaryAction
3234
extends DestructableAction,
3335
DisableableAction,
3436
LoadableAction,
35-
IconableAction {
37+
IconableAction,
38+
TooltipAction {
3639
/** Provides extra visual weight and identifies the primary action in a set of buttons */
3740
primary?: boolean;
3841
}
@@ -229,20 +232,26 @@ function PrimaryActionMarkup({
229232
primaryAction: PrimaryAction | React.ReactNode;
230233
}) {
231234
const {isNavigationCollapsed} = useMediaQuery();
232-
let content = primaryAction;
233-
if (isInterface(primaryAction)) {
234-
const primary =
235-
primaryAction.primary === undefined ? true : primaryAction.primary;
236235

237-
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(
238241
shouldShowIconOnly(isNavigationCollapsed, primaryAction),
239242
{
240243
primary,
241244
},
242245
);
246+
247+
actionMarkup = helpText ? (
248+
<Tooltip content={helpText}>{content}</Tooltip>
249+
) : (
250+
content
251+
);
243252
}
244253

245-
return <div className={styles.PrimaryActionWrapper}>{content}</div>;
254+
return <div className={styles.PrimaryActionWrapper}>{actionMarkup}</div>;
246255
}
247256

248257
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', () => {

polaris-react/src/components/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Include the CSS in your HTML. We suggest copying the styles file into your own p
6262
```html
6363
<link
6464
rel="stylesheet"
65-
href="https://unpkg.com/@shopify/[email protected].0/build/esm/styles.css"
65+
href="https://unpkg.com/@shopify/[email protected].1/build/esm/styles.css"
6666
/>
6767
```
6868

@@ -98,7 +98,7 @@ Include the CSS stylesheet in your HTML. We suggest copying the styles file into
9898
```html
9999
<link
100100
rel="stylesheet"
101-
href="https://unpkg.com/@shopify/[email protected].0/build/esm/styles.css"
101+
href="https://unpkg.com/@shopify/[email protected].1/build/esm/styles.css"
102102
/>
103103
```
104104

polaris-react/src/types.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
// eslint-disable-next-line @shopify/strict-component-boundaries
1+
import type React from 'react';
2+
3+
/* eslint-disable @shopify/strict-component-boundaries */
24
import type {AvatarProps} from './components/Avatar';
3-
// eslint-disable-next-line @shopify/strict-component-boundaries
45
import type {IconProps} from './components/Icon';
5-
// eslint-disable-next-line @shopify/strict-component-boundaries
66
import type {ThumbnailProps} from './components/Thumbnail';
7+
/* eslint-enable @shopify/strict-component-boundaries */
78

89
export interface OptionDescriptor {
910
/** Value of the option */
@@ -170,6 +171,11 @@ export interface PlainAction extends Action {
170171
plain?: boolean;
171172
}
172173

174+
export interface TooltipAction {
175+
/** Text content to render in a tooltip */
176+
helpText?: React.ReactNode;
177+
}
178+
173179
export interface ActionListItemDescriptor
174180
extends DisableableAction,
175181
DestructableAction {
@@ -181,7 +187,7 @@ export interface ActionListItemDescriptor
181187
content: string;
182188
};
183189
/** Additional hint text to display with item */
184-
helpText?: string;
190+
helpText?: React.ReactNode;
185191
/** @deprecated Source of the icon */
186192
icon?: IconSource;
187193
/** @deprecated Image source */
@@ -214,7 +220,7 @@ export interface ComplexAction
214220
LoadableAction,
215221
PlainAction {}
216222

217-
export interface MenuActionDescriptor extends ComplexAction {
223+
export interface MenuActionDescriptor extends ComplexAction, TooltipAction {
218224
/** Zero-indexed numerical position. Overrides the action's order in the menu */
219225
index?: number;
220226
}

0 commit comments

Comments
 (0)