Skip to content

Conversation

@WillsonSmith
Copy link
Contributor

@WillsonSmith WillsonSmith commented May 25, 2020

WHY are these changes introduced?

The current primaryAction on Page only allows for one button, I'm currently working on a project that has a need for more flexible primary actions.

WHAT is this pull request doing?

This PR maintains the existing primaryAction prop, while adding the option to pass a ReactNode instead for customizability.

What other approaches were considered?

We had considered building the API/UI for primaryAction to suit our specific need. We decided against this because we are not sure it's the right solution for everyone. This way if the provided option PrimaryAction doesn't cover every scenario, a custom option can be implemented.

How to 🎩

🖥 Local development instructions
🗒 General tophatting guidelines
📄 Changelog guidelines

Copy-paste this code in playground/Playground.tsx:
import React from 'react';

import {Page, Button} from '../src';

export function Playground() {
  return (
    <Page
      title="Playground"
      primaryAction={
        <Button
          primary
          connectedDisclosure={{
            accessibilityLabel: 'Other save actions',
            actions: [{content: 'Save as new'}],
          }}
        >
          Save
        </Button>
      }
    >
      {/* Add the code you want to test in here */}
    </Page>
  );
}

🎩 checklist

  • Tested on mobile
  • Tested on multiple browsers
  • Tested for accessibility
  • Updated the component's README.md with documentation changes
  • Tophatted documentation changes in the style guide
  • For visual design changes, pinged one of @ HYPD, @ mirualves, @ sarahill, or @ ry5n to update the Polaris UI kit

@ghost
Copy link

ghost commented May 25, 2020

👋 Thanks for opening your first pull request. A contributor should give feedback soon. If you haven’t already, please check out the contributing guidelines.

@github-actions
Copy link
Contributor

github-actions bot commented May 25, 2020

🟢 This pull request modifies 5 files and might impact 1 other files.

Details:
All files potentially affected (total: 1)
📄 UNRELEASED.md (total: 0)

Files potentially affected (total: 0)

🧩 src/components/Page/Page.tsx (total: 0)

Files potentially affected (total: 0)

📄 src/components/Page/README.md (total: 0)

Files potentially affected (total: 0)

🧩 src/components/Page/components/Header/Header.tsx (total: 1)

Files potentially affected (total: 1)

🧩 src/components/Page/components/Header/tests/Header.test.tsx (total: 0)

Files potentially affected (total: 0)

@WillsonSmith WillsonSmith changed the title [WIP][Page] Add React.ReactNode as an accepted primaryAction prop value [WIP][Page] Add ReactNode as an accepted primaryAction prop value May 25, 2020
title,
buttons: transformActions(appBridge, {
primaryAction,
primaryAction: isPrimaryAction(primaryAction)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BPScott Thoughts on how I'm handing the typing here & the helper function in general? Will probably ping you for review too, but was curious on your initial thoughts.

Copy link
Member

Choose a reason for hiding this comment

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

This seems alright at first glance. AppBridge integration is going away in v5. The PR has been open for an age and hopefully we'll get around to releasing it in 2 months, so I'd prioritize how to handle this stulf in non-AppBridge-land which you're doing anyway.

Copy link
Member

Choose a reason for hiding this comment

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

There's a fancy way to add optional object params

{
        ...(isPrimaryAction(primaryAction) && {primaryAction})
}

const primary =
primaryAction &&
(primaryAction.primary === undefined ? true : primaryAction.primary);
function getPrimaryActionMarkup() {
Copy link
Member

Choose a reason for hiding this comment

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

Nested function definitions tend to feel odd to me - you need to go check for any closed over variables that appear in the scope. Can this be pulled out to the top level?

}

return (
<ConditionalWrapper
Copy link
Member

@BPScott BPScott May 26, 2020

Choose a reason for hiding this comment

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

If you're writing if statements above then doing an early return of if (newDesignLanguage) { return content; } might be more readable than using ConditionalWrapper

@WillsonSmith WillsonSmith force-pushed the page-primary-actions-section branch from b677a49 to cfa3aec Compare May 27, 2020 15:25
@WillsonSmith WillsonSmith changed the title [WIP][Page] Add ReactNode as an accepted primaryAction prop value [Page] Add ReactNode as an accepted primaryAction prop value May 27, 2020
@WillsonSmith WillsonSmith marked this pull request as ready for review May 27, 2020 17:14
@WillsonSmith WillsonSmith requested a review from BPScott May 27, 2020 18:53
Copy link
Member

@BPScott BPScott left a comment

Choose a reason for hiding this comment

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

Two little things, but no major concerns. I'm gonna duck out of reviewing for now as I'll be on holiday till the 4th.

export function isPrimaryAction(
x: PrimaryAction | React.ReactNode,
): x is PrimaryAction {
return !React.isValidElement(x) && x !== undefined;
Copy link
Member

Choose a reason for hiding this comment

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

This feels a little odd to me - this condition is more "this is not a react node" rather than "this is a primary action", which is alright for now but might get confusing if we want to pass more sorts of things into this. Is there a way to check for action's shape rather than not ReactNode's shape?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree that it feels a little odd, the reason I did it this way is there are no required properties on primaryAction

return layout.slots;
}

function getPrimaryActionMarkup(
Copy link
Member

Choose a reason for hiding this comment

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

A function that returns some JSX is a React component. What do you think to naming it and treating it as such? PrimaryActionMarkup() and then calling it like a react component above const primaryActionMarkup = primaryAction ? <PrimaryActionMarkup ... /> : null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah this is the right decision, i'll do this.

Copy link
Member

@AndrewMusgrave AndrewMusgrave left a comment

Choose a reason for hiding this comment

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

Left a few comments but otherwise looks good to me 🎉 💯

Comment on lines 102 to 111
it('renders a `ReactNode`', () => {
const primaryAction = <div>Hello</div>;

const header = mountWithAppProvider(
<Header {...mockProps} primaryAction={primaryAction} />,
);

expect(header.contains(primaryAction)).toBeTruthy();
});
Copy link
Member

Choose a reason for hiding this comment

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

Another cool way this could be handled is using react-testing matchers!

const PrimaryAction = () => null;

const header = mountWithApp(
  <Header {...mockProps} primaryAction={<PrimaryAction />} />,
);

expect(header).toContainReactComponent(PrimaryAction);

title,
buttons: transformActions(appBridge, {
primaryAction,
primaryAction: isPrimaryAction(primaryAction)
Copy link
Member

Choose a reason for hiding this comment

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

There's a fancy way to add optional object params

{
        ...(isPrimaryAction(primaryAction) && {primaryAction})
}

},
)}
</ConditionalWrapper>
<PrimaryActionMarkup primaryAction={primaryAction} />
Copy link
Member

Choose a reason for hiding this comment

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

We usually create an if statement rather than a component in situations like these, but the separation of concern is nice. Curious what others think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is fine.

Comment on lines +224 to +225
let content = primaryAction;
if (isPrimaryAction(primaryAction)) {
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: With an early return we don't have to wrap the entire component 😄

Suggested change
let content = primaryAction;
if (isPrimaryAction(primaryAction)) {
let content = primaryAction;
if (!isPrimaryAction(primaryAction)) return null
const primary = ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I never want to return null with this component. This is wrapped in the condition because it changes content to be the button instead of primaryAction (which could be a React node). The ConditionalWrapper should always be returned with whatever content is as the children.

Comment on lines +222 to +223
const {isNavigationCollapsed} = useMediaQuery();
const {newDesignLanguage} = useFeatures();
Copy link
Member

Choose a reason for hiding this comment

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

Curious if anyone thinks we should pass these in as props?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I was curious about this too.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd leave it. It will be easier to clean-up and really doesn't become part of the API 🤷

Copy link
Contributor

@dleroux dleroux left a comment

Choose a reason for hiding this comment

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

Looks good! :shipit:

@WillsonSmith WillsonSmith force-pushed the page-primary-actions-section branch 2 times, most recently from 35d4ec6 to 18d21db Compare June 1, 2020 15:08
@WillsonSmith WillsonSmith force-pushed the page-primary-actions-section branch from 5245417 to 090bc61 Compare June 1, 2020 17:10
@alex-page
Copy link
Member

This is good to merge 🚀

@alex-page alex-page merged commit 2015bba into master Jun 1, 2020
@alex-page alex-page deleted the page-primary-actions-section branch June 1, 2020 17:48
@ghost
Copy link

ghost commented Jun 1, 2020

🎉 Thanks for your contribution to Polaris React!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants