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 @@ -12,6 +12,7 @@ Use [the changelog guidelines](https://git.io/polaris-changelog-guidelines) to f

- Added `onKeyPress`, `onKeyDown`, and `onKeyUp` to `Button` ([#860](https://github.com/Shopify/polaris-react/pull/860))
- Added `monochrome` prop to `Button` and `Link` component ([#821](https://github.com/Shopify/polaris-react/pull/821))
- Updated `Frame` layout and made `TopBar.UserMenu` visible on mobile ([#852](https://github.com/Shopify/polaris-react/pull/852))

### Design updates

Expand Down
7 changes: 5 additions & 2 deletions src/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import EventListener from '../EventListener';
import {withAppProvider, WithAppProviderProps} from '../AppProvider';
import Backdrop from '../Backdrop';
import TrapFocus from '../TrapFocus';
import {UserMenuProvider} from '../TopBar';
import {dataPolarisTopBar, layer, Duration} from '../shared';
import {setRootProperty} from '../../utilities/setRootProperty';
import {
Expand Down Expand Up @@ -246,11 +247,13 @@ export class Frame extends React.PureComponent<CombinedProps, State> {
{...navigationAttributes}
>
{skipMarkup}
{topBarMarkup}
<UserMenuProvider mobileView={mobileView || false}>
{topBarMarkup}
{navigationMarkup}
</UserMenuProvider>
{contextualSaveBarMarkup}
{loadingMarkup}
{navigationOverlayMarkup}
{navigationMarkup}
<main
className={styles.Main}
id={APP_FRAME_MAIN}
Expand Down
24 changes: 24 additions & 0 deletions src/components/Frame/tests/Frame.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
ContextualSaveBar as FrameContextualSavebar,
Loading as FrameLoading,
} from '../components';
import TopBar, {UserMenuProvider} from '../../TopBar';
import Navigation from '../../Navigation';

window.matchMedia =
window.matchMedia ||
Expand Down Expand Up @@ -216,4 +218,26 @@ describe('<Frame />', () => {
);
expect(frame.find(FrameLoading).exists()).toBe(true);
});

describe('<UserMenuProvider />', () => {
it('renders', () => {
const frame = mountWithAppProvider(<Frame />);
expect(frame.find(UserMenuProvider).exists()).toBe(true);
});

it('receives a mobileView boolean', () => {
const frame = mountWithAppProvider(<Frame />);
expect(frame.find(UserMenuProvider).prop('mobileView')).toBe(false);
});

it('receives the given top bar and navigation as its children', () => {
const topBar = <TopBar />;
const navigation = <Navigation location="" />;
const frame = mountWithAppProvider(
<Frame topBar={topBar} navigation={navigation} />,
);
expect(frame.find(UserMenuProvider).contains(topBar)).toBe(true);
expect(frame.find(UserMenuProvider).contains(navigation)).toBe(true);
});
});
});
1 change: 0 additions & 1 deletion src/components/Navigation/Navigation.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ $nav-max-width: rem(360px);

@include breakpoint-after(nav-min-window-corrected()) {
max-width: layout-width(nav);
border-right: border();
@include safe-area-for(max-width, layout-width(nav), left);
}
}
Expand Down
172 changes: 26 additions & 146 deletions src/components/Navigation/components/UserMenu/UserMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import * as React from 'react';

import {classNames} from '@shopify/react-utilities/styles';
import {autobind, memoize} from '@shopify/javascript-utilities/decorators';

import {autobind} from '@shopify/javascript-utilities/decorators';
import {UserMenuModifier} from '../../../TopBar';
import {IconableAction} from '../../../../types';

import Avatar, {Props as AvatarProps} from '../../../Avatar';
import MessageIndicator from '../../../MessageIndicator';
import Icon from '../../../Icon';
import UnstyledLink from '../../../UnstyledLink';

import Message, {Props as MessageProps} from '../Message';

import * as styles from './UserMenu.scss';
import {Props as MessageProps} from '../Message';
import {Props as AvatarProps} from '../../../Avatar';

interface UserActionSection {
id: string;
Expand All @@ -29,155 +20,44 @@ export interface Props {
}

interface State {
userMenuExpanded?: boolean;
open: boolean;
}

export default class UserMenu extends React.PureComponent<Props, State> {
state: State = {
userMenuExpanded: false,
class UserMenu extends React.Component<Props, State> {
state = {
open: false,
};

render() {
const {
name,
detail,
avatarInitials,
avatarSource,
actions,
message,
avatarInitials,
avatarSource,
} = this.props;
const {userMenuExpanded} = this.state;

const className = classNames(
styles.UserMenu,
userMenuExpanded && styles.expanded,
);
const {open} = this.state;

const itemClassName = styles.Item;
const tabIndex = userMenuExpanded ? 0 : -1;

const itemsMarkup =
actions &&
actions.map((section) => {
return (
<div className={styles.Section} key={section.id}>
{section.items.map((item) => {
const icon = item.icon;
return item.url ? (
<UnstyledLink
url={item.url}
key={item.content}
className={itemClassName}
tabIndex={tabIndex}
onClick={this.handleClick}
>
{icon && (
<span className={styles.Icon}>
<Icon source={icon} />
</span>
)}
{item.content}
</UnstyledLink>
) : (
<button
type="button"
key={item.content}
onClick={
item.onAction
? this.createActionHandler(item.onAction)
: this.handleClick
}
className={itemClassName}
tabIndex={tabIndex}
>
{item.icon && (
<span className={styles.Icon}>
<Icon source={item.icon} />
</span>
)}
{item.content}
</button>
);
})}
</div>
);
});

const badgeProps = message &&
message.badge && {
content: message.badge.content,
status: message.badge.status,
};
const messageMarkup = message && (
<div className={styles.Section}>
<Message
title={message.title}
description={message.description}
action={{
onClick: message.action.onClick,
content: message.action.content,
}}
link={{to: message.link.to, content: message.link.content}}
badge={badgeProps}
/>
</div>
);

const showIndicator = Boolean(message);

return (
<div className={className}>
<button
type="button"
className={styles.Button}
onClick={this.handleClick}
onMouseUp={handleMouseUp}
>
<span className={styles.Avatar}>
<MessageIndicator active={showIndicator}>
<Avatar
size="small"
source={avatarSource}
initials={avatarInitials && avatarInitials.replace(' ', '')}
/>
</MessageIndicator>
</span>
<span className={styles.Details}>
<span className={styles.Name}>{name}</span>
<span className={styles.Detail}>{detail}</span>
</span>
<span className={styles.DisclosureIcon}>
<Icon
source="chevronDown"
color="inkLightest"
accessibilityLabel="Show user menu"
/>
</span>
</button>
<div className={styles.List} aria-hidden={!userMenuExpanded}>
{itemsMarkup}
{messageMarkup}
</div>
</div>
);
}

@memoize()
private createActionHandler(handler: () => void) {
return () => {
handler();
this.handleClick();
const userMenuProps = {
actions: actions || [],
message,
name: name || '',
detail,
initials: avatarInitials,
avatar: avatarSource,
onToggle: this.handleToggle,
open,
};

return <UserMenuModifier userMenuProps={userMenuProps} />;
}

@autobind
private handleClick() {
this.setState(({userMenuExpanded}) => ({
userMenuExpanded: !userMenuExpanded,
}));
private handleToggle() {
const {open} = this.state;
this.setState({open: !open});
}
}

function handleMouseUp({currentTarget}: React.MouseEvent<HTMLButtonElement>) {
currentTarget.blur();
}
export default UserMenu;
Loading