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
1 change: 1 addition & 0 deletions UNRELEASED.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f
- Added consistent spacing to `ActionList` ([#4355](https://github.com/Shopify/polaris-react/pull/4355))
- Added `ariaLabelledBy` props to `Navigation` component to allow a hidden label for accessibility ([#4343](https://github.com/Shopify/polaris-react/pull/4343))
- Add `lastColumnSticky` prop to `IndexTable` to create a sticky last cell and optional sticky last heading on viewports larger than small ([#4150](https://github.com/Shopify/polaris-react/pull/4150))
- Allow promoted actions to be rendered as a menu on the `BulkAction` component ([#4266](https://github.com/Shopify/polaris-react/pull/4266))

### Bug fixes

Expand Down
72 changes: 54 additions & 18 deletions src/components/BulkActions/BulkActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
DisableableAction,
Action,
ActionListSection,
MenuGroupDescriptor,
} from '../../types';
import {ActionList} from '../ActionList';
import {Popover} from '../Popover';
Expand All @@ -19,10 +20,10 @@ import {ButtonGroup} from '../ButtonGroup';
import {EventListener} from '../EventListener';
import {CheckableButton} from '../CheckableButton';

import {BulkActionButton} from './components';
import {BulkActionButton, BulkActionMenu} from './components';
import styles from './BulkActions.scss';

type BulkAction = DisableableAction & BadgeAction;
export type BulkAction = DisableableAction & BadgeAction;

type BulkActionListSection = ActionListSection;

Expand All @@ -42,7 +43,7 @@ export interface BulkActionsProps {
/** List is in a selectable state */
selectMode?: boolean;
/** Actions that will be given more prominence */
promotedActions?: BulkAction[];
promotedActions?: (BulkAction | MenuGroupDescriptor)[];
/** List of actions */
actions?: (BulkAction | BulkActionListSection)[];
/** Text to select all across pages */
Expand Down Expand Up @@ -178,6 +179,28 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
}
}

private rolledInPromotedActions() {
const {promotedActions} = this.props;
const numberOfPromotedActionsToRender = this.numberOfPromotedActionsToRender();

if (
!promotedActions ||
promotedActions.length === 0 ||
numberOfPromotedActionsToRender >= promotedActions.length
) {
return [];
}

const rolledInPromotedActions = promotedActions.map((action) => {
if (instanceOfMenuGroupDescriptor(action)) {
return {items: [...action.actions]};
}
return {items: [action]};
});

return rolledInPromotedActions.slice(numberOfPromotedActionsToRender);
}

// eslint-disable-next-line @typescript-eslint/member-ordering
componentDidMount() {
const {actions, promotedActions} = this.props;
Expand Down Expand Up @@ -297,21 +320,28 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
promotedActions && numberOfPromotedActionsToRender > 0
? [...promotedActions]
.slice(0, numberOfPromotedActionsToRender)
.map((action, index) => (
<BulkActionButton
disabled={disabled}
{...action}
key={index}
handleMeasurement={this.handleMeasurement}
/>
))
.map((action, index) => {
if (instanceOfMenuGroupDescriptor(action)) {
return (
<BulkActionMenu
key={index}
{...action}
isNewBadgeInBadgeActions={this.isNewBadgeInBadgeActions()}
/>
);
}
return (
<BulkActionButton
disabled={disabled}
{...action}
key={index}
handleMeasurement={this.handleMeasurement}
/>
);
})
: null;

const rolledInPromotedActions =
promotedActions &&
numberOfPromotedActionsToRender < promotedActions.length
? [...promotedActions].slice(numberOfPromotedActionsToRender)
: [];
const rolledInPromotedActions = this.rolledInPromotedActions();

const activatorLabel =
!promotedActions ||
Expand All @@ -326,11 +356,11 @@ class BulkActionsInner extends PureComponent<CombinedProps, State> {
let combinedActions: ActionListSection[] = [];

if (actionSections && rolledInPromotedActions.length > 0) {
combinedActions = [{items: rolledInPromotedActions}, ...actionSections];
combinedActions = [...rolledInPromotedActions, ...actionSections];
} else if (actionSections) {
combinedActions = actionSections;
} else if (rolledInPromotedActions.length > 0) {
combinedActions = [{items: rolledInPromotedActions}];
combinedActions = [...rolledInPromotedActions];
}

const actionsPopover =
Expand Down Expand Up @@ -548,6 +578,12 @@ function instanceOfBulkActionArray(
return actions.length === validList.length;
}

function instanceOfMenuGroupDescriptor(
action: MenuGroupDescriptor | BulkAction,
): action is MenuGroupDescriptor {
return 'title' in action;
}

export function BulkActions(props: BulkActionsProps) {
const i18n = useI18n();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';

import {Popover} from '../../../Popover';
import {ActionList} from '../../../ActionList';
import {BulkActionButton} from '../BulkActionButton';
import {useToggle} from '../../../../utilities/use-toggle';
import type {MenuGroupDescriptor} from '../../../../types';

export interface BulkActionsMenuProps extends MenuGroupDescriptor {
isNewBadgeInBadgeActions: boolean;
}

export function BulkActionMenu({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't need a lot, but would you mind adding a few tests 🙏

title,
actions,
isNewBadgeInBadgeActions,
}: BulkActionsMenuProps) {
const {value: isVisible, toggle: toggleMenuVisibility} = useToggle(false);

return (
<>
<Popover
active={isVisible}
activator={
<BulkActionButton
disclosure
onAction={toggleMenuVisibility}
content={title}
indicator={isNewBadgeInBadgeActions}
/>
}
onClose={toggleMenuVisibility}
preferInputActivator
>
<ActionList items={actions} onActionAnyItem={toggleMenuVisibility} />
</Popover>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BulkActionMenu';
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import {Popover, ActionList} from 'components';
import {mountWithApp} from 'test-utilities';

import {BulkActionMenu, BulkActionsMenuProps, BulkActionButton} from '../..';

const defaultProps: BulkActionsMenuProps = {
title: 'New promoted action',
actions: [{content: 'Action1', onAction: jest.fn}],
isNewBadgeInBadgeActions: false,
};

describe('BulkActionMenu', () => {
describe('initial render', () => {
it('renders an inactive Popover', () => {
const bulkActionMenu = mountWithApp(<BulkActionMenu {...defaultProps} />);

expect(bulkActionMenu).toContainReactComponent(Popover, {
active: false,
});
});

it('does not render an ActionList', () => {
const bulkActionMenu = mountWithApp(<BulkActionMenu {...defaultProps} />);

expect(bulkActionMenu).not.toContainReactComponent(ActionList);
});

it('renders a BulkActionButton as the Popover activator with the right props', () => {
const bulkActionMenu = mountWithApp(<BulkActionMenu {...defaultProps} />);

expect(bulkActionMenu).toContainReactComponent(BulkActionButton, {
indicator: defaultProps.isNewBadgeInBadgeActions,
disclosure: true,
content: defaultProps.title,
});
});
});

describe('upon click', () => {
it('renders an active Popover', () => {
const bulkActionMenu = mountWithApp(<BulkActionMenu {...defaultProps} />);
const bulkActionButton = bulkActionMenu.find(BulkActionButton);
bulkActionButton!.trigger('onAction');

expect(bulkActionMenu).toContainReactComponent(Popover, {
active: true,
});
});

it('renders an ActionList with the supplied actions', () => {
const bulkActionMenu = mountWithApp(<BulkActionMenu {...defaultProps} />);
const bulkActionButton = bulkActionMenu.find(BulkActionButton);
bulkActionButton!.trigger('onAction');

expect(bulkActionMenu).toContainReactComponent(ActionList, {
items: defaultProps.actions,
});
});

it('closes the Popover when an action is clicked', () => {
const bulkActionMenu = mountWithApp(<BulkActionMenu {...defaultProps} />);
const bulkActionButton = bulkActionMenu.find(BulkActionButton);
bulkActionButton!.trigger('onAction');
const actionList = bulkActionMenu.find(ActionList);
actionList!.trigger('onActionAnyItem');

expect(bulkActionMenu).toContainReactComponent(Popover, {
active: false,
});
});
});
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

1 change: 1 addition & 0 deletions src/components/BulkActions/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './BulkActionButton';
export * from './BulkActionMenu';
Loading