Skip to content
Merged
6 changes: 6 additions & 0 deletions .changeset/wicked-knives-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/polaris': minor
---

- Added `customActivator` prop to `TopBar.UserMenu`
- Added support for setting a `ReactNode ` on `ActionList` `Section` `title`
2 changes: 1 addition & 1 deletion polaris-react/src/components/ActionList/ActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function ActionList({
const sectionMarkup = finalSections.map((section, index) => {
return section.items.length > 0 ? (
<Section
key={section.title || index}
key={typeof section.title === 'string' ? section.title : index}
section={section}
hasMultipleSections={hasMultipleSections}
actionRole={actionRole}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,25 @@ export function Section({
},
);

const titleMarkup = section.title ? (
<Box
paddingBlockStart="4"
paddingInlineStart="4"
paddingBlockEnd="2"
paddingInlineEnd="4"
>
<Text as="p" variant="headingXs">
{section.title}
</Text>
</Box>
) : null;
let titleMarkup: string | React.ReactNode = null;

if (section.title) {
titleMarkup =
typeof section.title === 'string' ? (
<Box
paddingBlockStart="4"
paddingInlineStart="4"
paddingBlockEnd="2"
paddingInlineEnd="4"
>
<Text as="p" variant="headingXs">
{section.title}
</Text>
</Box>
) : (
<Box padding="2">{section.title}</Box>
);
}

let sectionRole: 'menu' | 'presentation' | undefined;
switch (actionRole) {
Expand Down
126 changes: 113 additions & 13 deletions polaris-react/src/components/TopBar/TopBar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import React, {useCallback, useState} from 'react';
import type {ComponentMeta} from '@storybook/react';
import {ActionList, Frame, Icon, TopBar, Text} from '@shopify/polaris';
import {ActionList, Frame, Icon, TopBar, Text, Avatar} from '@shopify/polaris';
import {ArrowLeftMinor, QuestionMarkMajor} from '@shopify/polaris-icons';

import type {UserMenuProps} from '../../../build/ts/latest/src/components/TopBar';

export default {
component: TopBar,
} as ComponentMeta<typeof TopBar>;

export function Default() {
function TopBarWrapper({
userActions,
name,
detail,
initials,
customActivator,
message,
}: {
userActions?: UserMenuProps['actions'];
name?: UserMenuProps['name'];
detail?: UserMenuProps['detail'];
initials?: UserMenuProps['initials'];
customActivator?: UserMenuProps['customActivator'];
message?: UserMenuProps['message'];
}) {
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [isSecondaryMenuOpen, setIsSecondaryMenuOpen] = useState(false);
const [isSearchActive, setIsSearchActive] = useState(false);
Expand Down Expand Up @@ -47,17 +63,23 @@ export function Default() {

const userMenuMarkup = (
<TopBar.UserMenu
actions={[
{
items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
},
{
items: [{content: 'Community forums'}],
},
]}
name="Dharma"
detail="Jaded Pixel"
initials="D"
actions={
userActions
? userActions
: [
{
items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
},
{
items: [{content: 'Community forums'}],
},
]
}
name={name ? name : 'Dharma'}
detail={detail && detail}
initials={initials ? initials : 'JD'}
customActivator={customActivator}
message={message}
open={isUserMenuOpen}
onToggle={toggleIsUserMenuOpen}
/>
Expand Down Expand Up @@ -118,3 +140,81 @@ export function Default() {
</div>
);
}

export function Default() {
const userActions: UserMenuProps['actions'] = [
{
items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
},
{
items: [{content: 'Community forums'}],
},
];
return <TopBarWrapper userActions={userActions} name="Dharma" initials="D" />;
}

export function WithCustomActivator() {
const userActions: UserMenuProps['actions'] = [
{
items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
},
{
items: [{content: 'Community forums'}],
},
];

const customActivator = (
<>
<Avatar size="small" initials="D" name="Dharma" />
<span style={{marginLeft: '0.5rem'}}>
<Text as="p" alignment="start" fontWeight="medium" truncate>
Dharma
</Text>
<Text
as="p"
variant="bodySm"
alignment="start"
color="subdued"
truncate
>
Jaded Pixel
</Text>
</span>
</>
);

return (
<TopBarWrapper
userActions={userActions}
name="Dharma"
detail="Jaded Pixel"
customActivator={customActivator}
/>
);
}

export function withMessage() {
const userActions: UserMenuProps['actions'] = [
{
items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
},
{
items: [{content: 'Community forums'}],
},
];

return (
<TopBarWrapper
userActions={userActions}
name="Dharma"
detail="Jaded Pixel"
initials="JD"
message={{
title: 'Message title',
description: 'Message description',
link: {to: 'https://www.shopify.com', content: 'Link content'},
action: {content: 'Action content', onClick: () => {}},
}}
/>
);
}
4 changes: 1 addition & 3 deletions polaris-react/src/components/TopBar/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ export function Menu(props: MenuProps) {
/>
);

const isFullHeight = Boolean(message);

return (
<Popover
activator={
Expand All @@ -76,7 +74,7 @@ export function Menu(props: MenuProps) {
active={open}
onClose={onClose}
fixed
fullHeight={isFullHeight}
fullHeight
preferredAlignment="right"
>
<ActionList onActionAnyItem={onClose} sections={actions} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

.Details {
max-width: 160px;
margin-left: var(--p-space-2);
margin-right: var(--p-space-2);

@media #{$p-breakpoints-md-down} {
display: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export interface UserMenuProps {
open: boolean;
/** A callback function to handle opening and closing the user menu */
onToggle(): void;
/** A custom activator that can be used when the default activator is not desired */
customActivator?: React.ReactNode;
Copy link
Member

Choose a reason for hiding this comment

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

Can we just name this activator?

Copy link
Member

Choose a reason for hiding this comment

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

I think since the majority of the props map to the built-in activator, it might be confusing (name, avatar, accessibilityLabel, initials, detail) since the other components with an activator prop don't render their own internally.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree ^. Also, there's a default activator already; passing a new one would be customizing it.

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough! We do have a custom activator on the Sheet component which follows the activator? but it works slightly differently and is also a deprecated component so I'm fine with setting a new standard here

Copy link
Member

@chloerice chloerice Apr 13, 2023

Choose a reason for hiding this comment

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

Oh I thought Sheet's activator prop was analogous to Modal's 👀. In any case, bringing the related convo from other PR here:

@chloerice 6 hours ago
Are all user menus going to have the store switcher eventually instead of the Stores action list item that links out to an identity page? If not, should this use case be documented as its own story since it's unique to merchants who have the store switcher?

@zakwarsame 4 hours ago
Yup! Our https://github.com/Shopify/core-workflows/issues/881 aims to roll out the new store switcher to all our merchants except Plus, who have a separate nav.

Ah okay! In that case, should we:

  • Make this a layout change internal to the component instead of adding a customActivator prop, since the pattern is being updated across the board?
  • Add an optional logo prop that renders a Thumbnail to the right of the name (instead of an avatar on the left) when provided, and make initials optional?

Copy link
Contributor Author

@zakwarsame zakwarsame Apr 14, 2023

Choose a reason for hiding this comment

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

@chloerice

Make this a layout change internal to the component instead of adding a customActivator prop, since the pattern is being updated across the board?

Making it an internal layout change is a good idea. Although I think we still need the customActivator prop because the release for the store switcher change is set for Editions; and also, Plus stores will not have their layout changed even after the release.

Add an optional logo prop that renders a Thumbnail to the right of the name (instead of an avatar on the left) when provided, and make initials optional?

I think this depends on how much flexibility we'll want in Polaris. We can allow for both of them to be optional in a prefix / suffix type of way like we do in ActionList. On the other hand if we want to be stricter, we can just use one internal layout, and then allow the customActivator prop for the other one-off situations. For this case the avatar prop already acts as a logo since it takes in a url that can be passed to the Avatar component.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds like what you have is th ebest way to move forward 💯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you! FYI, I've made the new layout change internal and will temporarily pass in the old activator as customActivator until launch.

}

export function UserMenu({
Expand All @@ -41,18 +43,14 @@ export function UserMenu({
onToggle,
open,
accessibilityLabel,
customActivator,
}: UserMenuProps) {
const showIndicator = Boolean(message);

const activatorContentMarkup = (
const activatorContentMarkup = customActivator ? (
customActivator
) : (
<>
<MessageIndicator active={showIndicator}>
<Avatar
size="small"
source={avatar}
initials={initials && initials.replace(' ', '')}
/>
</MessageIndicator>
<span className={styles.Details}>
<Text as="p" alignment="start" fontWeight="medium" truncate>
{name}
Expand All @@ -67,6 +65,14 @@ export function UserMenu({
{detail}
</Text>
</span>
<MessageIndicator active={showIndicator}>
<Avatar
shape="square"
size="small"
initials={initials && initials.replace(' ', '')}
source={avatar}
/>
</MessageIndicator>
</>
);

Expand Down
2 changes: 1 addition & 1 deletion polaris-react/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ export interface ActionListItemDescriptor

export interface ActionListSection {
/** Section title */
title?: string;
title?: string | React.ReactNode;
/** Collection of action items for the list */
items: readonly ActionListItemDescriptor[];
}
Expand Down