Skip to content

Commit 54cfc5d

Browse files
aminpakschloerice
authored andcommitted
Add tooltip to Page primary & secondary actions
1 parent 19f6b13 commit 54cfc5d

File tree

10 files changed

+133
-11
lines changed

10 files changed

+133
-11
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+
Support tooltip for Page component's primary & secondary actions

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import React, {useEffect, useRef} from 'react';
22

33
import {classNames} from '../../../../utilities/css';
44
import {Button} from '../../../Button';
5+
import {tooltipFrom} from '../../../Tooltip';
56
import type {ButtonProps} from '../../../Button';
7+
import type {ActionWithTooltip} from '../../../../types';
68

79
import styles from './SecondaryAction.scss';
810

9-
interface SecondaryAction extends ButtonProps {
11+
interface SecondaryAction extends ButtonProps, ActionWithTooltip {
1012
onAction?(): void;
1113
getOffsetWidth?(width: number): void;
1214
}
@@ -16,6 +18,7 @@ export function SecondaryAction({
1618
destructive,
1719
onAction,
1820
getOffsetWidth,
21+
tooltip,
1922
...rest
2023
}: SecondaryAction) {
2124
const secondaryActionsRef = useRef<HTMLSpanElement>(null);
@@ -26,6 +29,16 @@ export function SecondaryAction({
2629
getOffsetWidth(secondaryActionsRef.current?.offsetWidth);
2730
}, [getOffsetWidth]);
2831

32+
let button = (
33+
<Button onClick={onAction} {...rest}>
34+
{children}
35+
</Button>
36+
);
37+
38+
if (tooltip != null) {
39+
button = tooltipFrom(tooltip, button);
40+
}
41+
2942
return (
3043
<span
3144
className={classNames(
@@ -34,9 +47,7 @@ export function SecondaryAction({
3447
)}
3548
ref={secondaryActionsRef}
3649
>
37-
<Button onClick={onAction} {...rest}>
38-
{children}
39-
</Button>
50+
{button}
4051
</span>
4152
);
4253
}

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

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

33
import type {ComplexAction} from '../../types';
4+
import {tooltipFrom} from '../Tooltip';
45

56
import {Button, ButtonProps} from './Button';
67

@@ -25,13 +26,15 @@ export function buttonsFrom(
2526
}
2627

2728
export function buttonFrom(
28-
{content, onAction, ...action}: ComplexAction,
29+
{content, onAction, tooltip, ...action}: ComplexAction,
2930
overrides?: Partial<ButtonProps>,
3031
key?: any,
3132
) {
32-
return (
33+
const button = (
3334
<Button key={key} onClick={onAction} {...action} {...overrides}>
3435
{content}
3536
</Button>
3637
);
38+
39+
return tooltip ? tooltipFrom(tooltip, button) : button;
3740
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,31 @@ Use when the page needs visual separation between the page header and the conten
427427
</Page>
428428
```
429429

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+
430455
---
431456

432457
## Related components

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import {
1010
ConditionalWrapper,
1111
} from '../../../../utilities/components';
1212
import type {
13-
MenuGroupDescriptor,
14-
MenuActionDescriptor,
13+
ActionWithTooltip,
1514
DestructableAction,
1615
DisableableAction,
17-
LoadableAction,
1816
IconableAction,
17+
LoadableAction,
18+
MenuActionDescriptor,
19+
MenuGroupDescriptor,
1920
} from '../../../../types';
2021
import {Breadcrumbs, BreadcrumbsProps} from '../../../Breadcrumbs';
2122
import {Pagination, PaginationProps} from '../../../Pagination';
@@ -32,7 +33,8 @@ interface PrimaryAction
3233
extends DestructableAction,
3334
DisableableAction,
3435
LoadableAction,
35-
IconableAction {
36+
IconableAction,
37+
ActionWithTooltip {
3638
/** Provides extra visual weight and identifies the primary action in a set of buttons */
3739
primary?: boolean;
3840
}

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import {animationFrame} from '@shopify/jest-dom-mocks';
33
import {mountWithApp} from 'tests/utilities';
44

55
import type {ActionMenuProps} from '../../ActionMenu';
6+
import type {ActionTooltip} from '../../../types';
67
import {Badge} from '../../Badge';
78
import {Card} from '../../Card';
9+
import {Tooltip} from '../../Tooltip';
810
import {Page, PageProps} from '../Page';
911
import {Header} from '../components';
1012

@@ -290,6 +292,47 @@ describe('<Page />', () => {
290292
expect(page).not.toContainReactComponent(Header);
291293
});
292294
});
295+
296+
describe('<Tooltip />', () => {
297+
const saveTooltip: ActionTooltip = {
298+
content: 'Save tooltip',
299+
dismissOnMouseOut: true,
300+
};
301+
302+
it('is rendered when available for primary action', () => {
303+
const page = mountWithApp(
304+
<Page
305+
{...mockProps}
306+
primaryAction={{content: 'Save', tooltip: saveTooltip}}
307+
/>,
308+
);
309+
expect(page).toContainReactComponent(Tooltip, saveTooltip);
310+
});
311+
312+
it('is rendered when available for secondary actions', () => {
313+
const page = mountWithApp(
314+
<Page
315+
{...mockProps}
316+
secondaryActions={[{content: 'Save', tooltip: saveTooltip.content}]}
317+
/>,
318+
);
319+
expect(page).toContainReactComponent(Tooltip, {
320+
content: saveTooltip.content,
321+
});
322+
});
323+
324+
it('will NOT be rendered when primary & secondary actions have no tooltip passed', () => {
325+
const page = mountWithApp(
326+
<Page
327+
{...mockProps}
328+
primaryAction={{content: 'Save'}}
329+
secondaryActions={[{content: 'Load'}]}
330+
/>,
331+
);
332+
333+
expect(page).not.toContainReactComponent(Tooltip);
334+
});
335+
});
293336
});
294337

295338
function noop() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './utils';
12
export * from './Tooltip';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import type {ReactNode} from 'react';
3+
4+
import type {ActionTooltip} from '../../types';
5+
import {isInterface} from '../../utilities/is-interface';
6+
7+
import {Tooltip, TooltipProps} from './Tooltip';
8+
9+
export function tooltipFrom(
10+
tooltip: ReactNode | ActionTooltip,
11+
button: ReactNode,
12+
) {
13+
const tooltipProps: TooltipProps =
14+
isInterface(tooltip) && isMinimalTooltipProps(tooltip)
15+
? tooltip
16+
: {content: tooltip};
17+
18+
return <Tooltip {...tooltipProps}>{button}</Tooltip>;
19+
}
20+
21+
function isMinimalTooltipProps(input: any): input is TooltipProps {
22+
return typeof input === 'object' && input?.content != null;
23+
}

polaris-react/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ export type {ThumbnailProps} from './components/Thumbnail';
354354
export {Toast} from './components/Toast';
355355
export type {ToastProps} from './components/Toast';
356356

357-
export {Tooltip} from './components/Tooltip';
357+
export {Tooltip, tooltipFrom} from './components/Tooltip';
358358
export type {TooltipProps} from './components/Tooltip';
359359

360360
export {TopBar} from './components/TopBar';

polaris-react/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type {AvatarProps} from './components/Avatar';
44
import type {IconProps} from './components/Icon';
55
// eslint-disable-next-line @shopify/strict-component-boundaries
66
import type {ThumbnailProps} from './components/Thumbnail';
7+
// eslint-disable-next-line @shopify/strict-component-boundaries
8+
import type {TooltipProps} from './components/Tooltip';
79

810
export interface OptionDescriptor {
911
/** Value of the option */
@@ -155,6 +157,12 @@ export interface IconableAction extends Action {
155157
icon?: IconSource;
156158
}
157159

160+
export type ActionTooltip = Omit<TooltipProps, 'children'>;
161+
162+
export interface ActionWithTooltip extends Action {
163+
tooltip?: React.ReactNode | ActionTooltip;
164+
}
165+
158166
export interface LoadableAction extends Action {
159167
/** Should a spinner be displayed */
160168
loading?: boolean;
@@ -212,6 +220,7 @@ export interface ComplexAction
212220
IconableAction,
213221
OutlineableAction,
214222
LoadableAction,
223+
ActionWithTooltip,
215224
PlainAction {}
216225

217226
export interface MenuActionDescriptor extends ComplexAction {

0 commit comments

Comments
 (0)