Skip to content

Commit

Permalink
Support tooltip for Page primary & secondary actions (#6709)
Browse files Browse the repository at this point in the history
* Add tooltip to Page primary & secondary actions

* [Page] Add support for tooltips on primaryAction and secondaryActions

Co-authored-by: Chloe Rice <chloe.rice@shopify.com>
  • Loading branch information
aminpaks and chloerice committed Jul 28, 2022
1 parent 55b7348 commit 47e8244
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-emus-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/polaris': minor
---

Added `Tooltip` support for `Page` `primaryAction` and `secondaryActions`
4 changes: 2 additions & 2 deletions polaris-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Otherwise include the CSS in your HTML. We suggest copying the styles file into
```html
<link
rel="stylesheet"
href="https://unpkg.com/@shopify/polaris@9.21.0/build/esm/styles.css"
href="https://unpkg.com/@shopify/polaris@9.21.1/build/esm/styles.css"
/>
```

Expand Down Expand Up @@ -70,7 +70,7 @@ If React doesn’t make sense for your application, you can use a CSS-only versi
```html
<link
rel="stylesheet"
href="https://unpkg.com/@shopify/polaris@9.21.0/build/esm/styles.css"
href="https://unpkg.com/@shopify/polaris@9.21.1/build/esm/styles.css"
/>
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {mountWithApp} from 'tests/utilities';

import {ActionMenuProps, ActionMenu} from '../../..';
import {Actions, MenuGroup, RollupActions, SecondaryAction} from '../..';
import {Tooltip} from '../../../../Tooltip';

describe('<Actions />', () => {
const mockProps: ActionMenuProps = {
Expand Down Expand Up @@ -40,6 +41,21 @@ describe('<Actions />', () => {
expect(wrapper.findAll(SecondaryAction)).toHaveLength(3);
});

it('renders a <Tooltip /> when helpText is set on an action', () => {
const toolTipAction = {
content: 'Refund',
helpText:
'You need permission from your store administrator to issue refunds.',
};

const wrapper = mountWithApp(<ActionMenu actions={[toolTipAction]} />);
const action = wrapper.find(SecondaryAction);

expect(action).toContainReactComponent(Tooltip, {
content: toolTipAction.helpText,
});
});

it('renders a MenuGroup', () => {
const wrapper = mountWithApp(
<ActionMenu groups={[{title: 'group', actions: []}]} />,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import React, {useEffect, useRef} from 'react';

import {classNames} from '../../../../utilities/css';
import {Tooltip} from '../../../Tooltip';
import {Button} from '../../../Button';
import type {ButtonProps} from '../../../Button';

import styles from './SecondaryAction.scss';

interface SecondaryAction extends ButtonProps {
helpText?: React.ReactNode;
onAction?(): void;
getOffsetWidth?(width: number): void;
}

export function SecondaryAction({
children,
destructive,
helpText,
onAction,
getOffsetWidth,
...rest
Expand All @@ -26,6 +29,18 @@ export function SecondaryAction({
getOffsetWidth(secondaryActionsRef.current?.offsetWidth);
}, [getOffsetWidth]);

const buttonMarkup = (
<Button onClick={onAction} {...rest}>
{children}
</Button>
);

const actionMarkup = helpText ? (
<Tooltip content={helpText}>{buttonMarkup}</Tooltip>
) : (
buttonMarkup
);

return (
<span
className={classNames(
Expand All @@ -34,9 +49,7 @@ export function SecondaryAction({
)}
ref={secondaryActionsRef}
>
<Button onClick={onAction} {...rest}>
{children}
</Button>
{actionMarkup}
</span>
);
}
62 changes: 32 additions & 30 deletions polaris-react/src/components/ActionMenu/tests/ActionMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,41 +179,43 @@ describe('<ActionMenu />', () => {
});
});

it('uses Button and ButtonGroup as subcomponents', () => {
const wrapper = mountWithApp(
<ActionMenu {...mockProps} actions={mockActions} />,
);
describe('<Actions />', () => {
it('uses Button and ButtonGroup as subcomponents', () => {
const wrapper = mountWithApp(
<ActionMenu {...mockProps} actions={mockActions} />,
);

expect(wrapper.findAll(Button)).toHaveLength(2);
expect(wrapper.findAll(ButtonGroup)).toHaveLength(1);
});
expect(wrapper.findAll(Button)).toHaveLength(2);
expect(wrapper.findAll(ButtonGroup)).toHaveLength(1);
});

it('passes action callbacks through to Button', () => {
const spy = jest.fn();
const wrapper = mountWithApp(
<ActionMenu
{...mockProps}
actions={[{content: 'mock', onAction: spy}]}
/>,
);

it('action callbacks are passed through to Button', () => {
const spy = jest.fn();
const wrapper = mountWithApp(
<ActionMenu
{...mockProps}
actions={[{content: 'mock', onAction: spy}]}
/>,
);
wrapper.find(Button)!.trigger('onClick');

wrapper.find(Button)!.trigger('onClick');
expect(spy).toHaveBeenCalledTimes(1);
});

expect(spy).toHaveBeenCalledTimes(1);
});
it('passes `onActionRollup` if set', () => {
const onActionRollup = jest.fn();
const wrapper = mountWithApp(
<ActionMenu
{...mockProps}
actions={[{content: 'mock'}]}
onActionRollup={onActionRollup}
/>,
);

it('renders <Actions /> passing `onActionRollup` as prop if it exists', () => {
const onActionRollup = jest.fn();
const wrapper = mountWithApp(
<ActionMenu
{...mockProps}
actions={[{content: 'mock'}]}
onActionRollup={onActionRollup}
/>,
);

expect(wrapper).toContainReactComponent(Actions, {
onActionRollup,
expect(wrapper).toContainReactComponent(Actions, {
onActionRollup,
});
});
});
});
Expand Down
24 changes: 24 additions & 0 deletions polaris-react/src/components/Page/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,30 @@ Use to create a custom secondary action.
</Page>
```

### With tooltip action

Use when merchants or their staff will benefit from context on why a page action is disabled.

```jsx
<Page
title="Product"
primaryAction={{
content: 'Save',
}}
secondaryActions={[
{
content: 'Import',
disabled: true,
helpText: 'You need permission to import products.',
},
]}
>
<Card title="Product X" sectioned>
<p>Product X information</p>
</Card>
</Page>
```

### With subtitle

Use when the page title benefits from secondary content.
Expand Down
31 changes: 20 additions & 11 deletions polaris-react/src/components/Page/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import React from 'react';

import {classNames} from '../../../../utilities/css';
import {buttonsFrom} from '../../../Button';
import {buttonFrom} from '../../../Button';
import {TextStyle} from '../../../TextStyle';
import {Tooltip} from '../../../Tooltip';
import {useMediaQuery} from '../../../../utilities/media-query';
import {useI18n} from '../../../../utilities/i18n';
import {
ConditionalRender,
ConditionalWrapper,
} from '../../../../utilities/components';
import type {
MenuGroupDescriptor,
MenuActionDescriptor,
DestructableAction,
DisableableAction,
LoadableAction,
IconableAction,
LoadableAction,
MenuActionDescriptor,
MenuGroupDescriptor,
TooltipAction,
} from '../../../../types';
import {Breadcrumbs, BreadcrumbsProps} from '../../../Breadcrumbs';
import {Pagination, PaginationProps} from '../../../Pagination';
Expand All @@ -32,7 +34,8 @@ interface PrimaryAction
extends DestructableAction,
DisableableAction,
LoadableAction,
IconableAction {
IconableAction,
TooltipAction {
/** Provides extra visual weight and identifies the primary action in a set of buttons */
primary?: boolean;
}
Expand Down Expand Up @@ -229,20 +232,26 @@ function PrimaryActionMarkup({
primaryAction: PrimaryAction | React.ReactNode;
}) {
const {isNavigationCollapsed} = useMediaQuery();
let content = primaryAction;
if (isInterface(primaryAction)) {
const primary =
primaryAction.primary === undefined ? true : primaryAction.primary;

content = buttonsFrom(
let actionMarkup = primaryAction;
if (isInterface(primaryAction)) {
const {primary: isPrimary, helpText} = primaryAction;
const primary = isPrimary === undefined ? true : isPrimary;
const content = buttonFrom(
shouldShowIconOnly(isNavigationCollapsed, primaryAction),
{
primary,
},
);

actionMarkup = helpText ? (
<Tooltip content={helpText}>{content}</Tooltip>
) : (
content
);
}

return <div className={styles.PrimaryActionWrapper}>{content}</div>;
return <div className={styles.PrimaryActionWrapper}>{actionMarkup}</div>;
}

function shouldShowIconOnly(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Breadcrumbs} from '../../../../Breadcrumbs';
import {Button} from '../../../../Button';
import {ButtonGroup} from '../../../../ButtonGroup';
import {Pagination} from '../../../../Pagination';
import {Tooltip} from '../../../../Tooltip';
import type {LinkAction, MenuActionDescriptor} from '../../../../../types';
import {Header, HeaderProps} from '../Header';

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

expect(header).toContainReactComponent(PrimaryAction);
});

it('renders a <Tooltip /> when helpText is provided', () => {
const primaryAction = {
content: 'Save',
helpText: 'Helpful text',
};
const header = mountWithApp(
<Header {...mockProps} primaryAction={primaryAction} />,
);
expect(header).toContainReactComponent(Tooltip, {
content: primaryAction.helpText,
});
});
});

describe('pagination', () => {
Expand Down
4 changes: 2 additions & 2 deletions polaris-react/src/components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Include the CSS in your HTML. We suggest copying the styles file into your own p
```html
<link
rel="stylesheet"
href="https://unpkg.com/@shopify/polaris@9.21.0/build/esm/styles.css"
href="https://unpkg.com/@shopify/polaris@9.21.1/build/esm/styles.css"
/>
```

Expand Down Expand Up @@ -98,7 +98,7 @@ Include the CSS stylesheet in your HTML. We suggest copying the styles file into
```html
<link
rel="stylesheet"
href="https://unpkg.com/@shopify/polaris@9.21.0/build/esm/styles.css"
href="https://unpkg.com/@shopify/polaris@9.21.1/build/esm/styles.css"
/>
```

Expand Down
16 changes: 11 additions & 5 deletions polaris-react/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// eslint-disable-next-line @shopify/strict-component-boundaries
import type React from 'react';

/* eslint-disable @shopify/strict-component-boundaries */
import type {AvatarProps} from './components/Avatar';
// eslint-disable-next-line @shopify/strict-component-boundaries
import type {IconProps} from './components/Icon';
// eslint-disable-next-line @shopify/strict-component-boundaries
import type {ThumbnailProps} from './components/Thumbnail';
/* eslint-enable @shopify/strict-component-boundaries */

export interface OptionDescriptor {
/** Value of the option */
Expand Down Expand Up @@ -170,6 +171,11 @@ export interface PlainAction extends Action {
plain?: boolean;
}

export interface TooltipAction {
/** Text content to render in a tooltip */
helpText?: React.ReactNode;
}

export interface ActionListItemDescriptor
extends DisableableAction,
DestructableAction {
Expand All @@ -181,7 +187,7 @@ export interface ActionListItemDescriptor
content: string;
};
/** Additional hint text to display with item */
helpText?: string;
helpText?: React.ReactNode;
/** @deprecated Source of the icon */
icon?: IconSource;
/** @deprecated Image source */
Expand Down Expand Up @@ -214,7 +220,7 @@ export interface ComplexAction
LoadableAction,
PlainAction {}

export interface MenuActionDescriptor extends ComplexAction {
export interface MenuActionDescriptor extends ComplexAction, TooltipAction {
/** Zero-indexed numerical position. Overrides the action's order in the menu */
index?: number;
}
Expand Down
3 changes: 3 additions & 0 deletions polaris.shopify.com/content/components/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ examples:
- fileName: page-with-custom-secondary-action.tsx
title: With custom secondary action
description: Use to create a custom secondary action.
- fileName: page-with-tooltip-action.tsx
title: With tooltip action
description: Use when merchants or their staff will benefit from context on why a page action is disabled.
- fileName: page-with-subtitle.tsx
title: With subtitle
description: Use when the page title benefits from secondary content.
Expand Down

0 comments on commit 47e8244

Please sign in to comment.