Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected].0/build/esm/styles.css"
href="https://unpkg.com/@shopify/[email protected].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/[email protected].0/build/esm/styles.css"
href="https://unpkg.com/@shopify/[email protected].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/[email protected].0/build/esm/styles.css"
href="https://unpkg.com/@shopify/[email protected].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/[email protected].0/build/esm/styles.css"
href="https://unpkg.com/@shopify/[email protected].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
Loading