Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Popover] Set right position when preferred alignment is right. #2587

Merged
merged 6 commits into from
Jan 13, 2020

Conversation

dleroux
Copy link
Contributor

@dleroux dleroux commented Jan 3, 2020

WHY are these changes introduced?

Fixes #1673

WHAT is this pull request doing?

From what I can tell the issue is that the Scrollable scrollbars appear inside the Popover after the PositionOverlay has done its calculations and since the content is responsive, once the scrollbars are added the content wraps.

For example. If the window is 1000px wide, the overlay 100px wide, the left position will be calculated at 900px. Once the scrollbars appear, the left position does not get recalculated because the content is responsive. The left position pushed the overlay against the right side of the browser and when too close to the chrome causes the content to wrap. This occurs only when the popover is too close to the right of the window.

This is one of a few attempts at fixing this issue. When a popover has a preferred right, we go from the right instead of the left. That way, even if scrollbars appear, the right position stays the same, and the left does not counter it.

It also contains a change that when content can be scrolled in a Scrollable container, it sets overflow-y: scroll then reverts to auto when the content cannot be scrolled. This forces the layout engines to set the element width properly.

How to 🎩

🖥 Local development instructions
🗒 General tophatting guidelines
📄 Changelog guidelines

Copy-paste this code in playground/Playground.tsx:
import React, {useState} from 'react';
import {ArrowLeftMinor} from '@shopify/polaris-icons';
import {
  AppProvider,
  Frame,
  Page,
  Card,
  ActionList,
  TopBar,
  Button,
  Popover,
} from '../src';

export function Playground() {
  const [userMenuOpen, setUserMenuOpen] = useState(false);
  const [searchActive, setSearchActive] = useState(false);
  const [searchText, setSearchText] = useState('');
  const [popover1Active, setPopover1Active] = useState(false);
  const [popover2Active, setPopover2Active] = useState(false);
  const [popover3Active, setPopover3Active] = useState(false);
  const [popover1PositionBelow, setPopover1PositionBelow] = useState(true);
  const [popover2PositionBelow, setPopover2PositionBelow] = useState(true);
  const [popover3PositionBelow, setPopover3PositionBelow] = useState(true);
  const [showScrollingUserMenu, setShowScrollingUserMenu] = useState(true);

  const toggleUserMenu = () => {
    setUserMenuOpen(!userMenuOpen);
  };

  const toggleScrollingUserMenu = () => {
    setShowScrollingUserMenu(!showScrollingUserMenu);
  };

  const togglePopover = (popoverID: string) => () => {
    setPopover1Active(popoverID === 'popover1' ? !popover1Active : false);
    setPopover2Active(popoverID === 'popover2' ? !popover2Active : false);
    setPopover3Active(popoverID === 'popover3' ? !popover3Active : false);
  };

  const handleSearchResultsDismiss = () => {
    setSearchActive(false);
    setSearchText('');
  };

  const handleSearchChange = (value) => {
    setSearchText(value);
    if (value.length > 0) {
      setSearchActive(true);
    } else {
      setSearchActive(false);
    }
  };

  const theme = {
    colors: {
      topBar: {
        background: '#357997',
        backgroundLighter: '#6192a9',
        color: '#FFFFFF',
      },
    },
    logo: {
      width: 124,
      topBarSource:
        'https://cdn.shopify.com/s/files/1/0446/6937/files/jaded-pixel-logo-color.svg?6215648040070010999',
      url: 'http://jadedpixel.com',
      accessibilityLabel: 'Jaded Pixel',
    },
  };

  const actionsWithScroll = [
    {
      items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
  ];

  const actions = [
    {
      items: [{content: 'Back to Shopify', icon: ArrowLeftMinor}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
    {
      items: [{content: 'Community forums'}],
    },
  ];

  const userMenuMarkup = (
    <TopBar.UserMenu
      actions={showScrollingUserMenu ? actionsWithScroll : actions}
      name="Dharma"
      detail="Jaded Pixel"
      initials="D"
      open={userMenuOpen}
      onToggle={toggleUserMenu}
    />
  );

  const searchResultsMarkup = (
    <Card>
      <ActionList
        items={[
          {content: 'Shopify help center'},
          {content: 'Community forums'},
        ]}
      />
    </Card>
  );

  const searchFieldMarkup = (
    <TopBar.SearchField
      onChange={handleSearchChange}
      value={searchText}
      placeholder="Search"
    />
  );

  const topBarMarkup = (
    <TopBar
      showNavigationToggle
      userMenu={userMenuMarkup}
      searchResultsVisible={searchActive}
      searchField={searchFieldMarkup}
      searchResults={searchResultsMarkup}
      onSearchResultsDismiss={handleSearchResultsDismiss}
    />
  );

  const activator1 = (
    <Button onClick={togglePopover('popover1')}>Popover Left</Button>
  );

  const activator2 = (
    <Button onClick={togglePopover('popover2')}>Popover Center</Button>
  );

  const activator3 = (
    <Button onClick={togglePopover('popover3')}>Popover Right</Button>
  );

  return (
    <AppProvider i18n={{}} theme={theme}>
      <Frame topBar={topBarMarkup}>
        <Page
          title="Popover alignment/width bug"
          primaryAction={{
            content: `Render ${
              showScrollingUserMenu ? 'non-scrolling' : 'scrolling'
            } user menu`,
            onAction: toggleScrollingUserMenu,
          }}
        >
          <Card>
            <Card.Section
              actions={[
                {
                  content: 'Toggle position',
                  onAction: () =>
                    setPopover1PositionBelow(!popover1PositionBelow),
                },
              ]}
            >
              <Popover
                active={popover1Active}
                activator={activator1}
                onClose={togglePopover('popover1')}
                preferredAlignment="left"
                preferredPosition={popover1PositionBelow ? 'below' : 'above'}
              >
                <ActionList
                  items={[{content: 'Import'}, {content: 'Export'}]}
                />
              </Popover>
            </Card.Section>
            <Card.Section
              actions={[
                {
                  content: 'Toggle position',
                  onAction: () =>
                    setPopover2PositionBelow(!popover2PositionBelow),
                },
              ]}
            >
              <Popover
                active={popover2Active}
                activator={activator2}
                onClose={togglePopover('popover2')}
                preferredAlignment="center"
                preferredPosition={popover2PositionBelow ? 'below' : 'above'}
              >
                <ActionList
                  items={[{content: 'Import'}, {content: 'Export'}]}
                />
              </Popover>
            </Card.Section>
            <Card.Section
              actions={[
                {
                  content: 'Toggle position',
                  onAction: () =>
                    setPopover3PositionBelow(!popover3PositionBelow),
                },
              ]}
            >
              <Popover
                active={popover3Active}
                activator={activator3}
                onClose={togglePopover('popover3')}
                preferredAlignment="right"
                preferredPosition={popover3PositionBelow ? 'below' : 'above'}
              >
                <ActionList
                  items={[{content: 'Import'}, {content: 'Export'}]}
                />
              </Popover>
            </Card.Section>
          </Card>
        </Page>
      </Frame>
    </AppProvider>
  );
}

🎩 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

@@ -52,7 +52,7 @@ $content-max-width: rem(400px);
}

.positionedAbove {
margin: spacing() 0 $visible-portion-of-arrow spacing(tight);
margin: spacing() spacing(tight) $visible-portion-of-arrow;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure why we needed to remove the right margin when positioned above.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2020

💦 Potential splash zone of changes introduced to src/**/*.tsx in this pull request:

Files modified8
Files potentially affected42

Details

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

Files potentially affected (total: 0)

🎨 src/components/Popover/Popover.scss (total: 25)

Files potentially affected (total: 25)

🧩 src/components/PositionedOverlay/PositionedOverlay.tsx (total: 28)

Files potentially affected (total: 28)

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

Files potentially affected (total: 0)

🧩 src/components/PositionedOverlay/utilities/math.ts (total: 29)

Files potentially affected (total: 29)

🎨 src/components/Scrollable/Scrollable.scss (total: 42)

Files potentially affected (total: 42)

🧩 src/components/Scrollable/Scrollable.tsx (total: 41)

Files potentially affected (total: 41)

🧩 src/components/TopBar/components/Menu/Menu.tsx (total: 2)

Files potentially affected (total: 2)


This comment automatically updates as changes are made to this pull request.
Feedback, troubleshooting: open an issue or reach out on Slack in #polaris-tooling.

left: horizontalPosition,
left:
preferredAlignment !== 'right' ? horizontalPosition : undefined,
right:
Copy link
Contributor Author

@dleroux dleroux Jan 3, 2020

Choose a reason for hiding this comment

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

We're only going to go from the right if the preferred alignment is right.

@@ -298,7 +317,7 @@ function windowRect() {
top: window.scrollY,
left: window.scrollX,
height: window.innerHeight,
width: window.innerWidth,
width: document.body.clientWidth,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

window.innerWidth includes the scrollbars, We don't want this when aligning from the right and it's shouldn't impact the left.

Copy link
Member

Choose a reason for hiding this comment

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

TIL! Will this change alone fix the measuring then and the other stuff in this PR is just a nice to have?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, the right positioning is what is needed. I'll write it in the description.

@@ -98,13 +98,11 @@ export function calculateHorizontalPosition(
Math.max(0, activatorRect.left - overlayMargins.horizontal),
);
} else if (preferredAlignment === 'right') {
const activatorRight = activatorRect.left + activatorRect.width;
const activatorRight =
containerRect.width - (activatorRect.left + activatorRect.width);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the right position calculation.

@@ -57,6 +57,7 @@ export function Menu(props: MenuProps) {
onClose={onClose}
fixed
fullHeight={isFullHeight}
preferredAlignment="right"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The user menu was causing the issue. This fixed it.

@dleroux dleroux requested a review from kyledurand January 3, 2020 15:13
@dleroux dleroux changed the title [WIP][Popover] Set right position when preferred alignment is right. [Popover] Set right position when preferred alignment is right. Jan 3, 2020
@dleroux dleroux changed the title [Popover] Set right position when preferred alignment is right. [wip][Popover] Set right position when preferred alignment is right. Jan 3, 2020
@dleroux dleroux force-pushed the popover-position-fix branch from 121e07d to 782d7b1 Compare January 3, 2020 16:31
@dleroux dleroux changed the title [wip][Popover] Set right position when preferred alignment is right. [Popover] Set right position when preferred alignment is right. Jan 3, 2020
horizontal: parseFloat(nodeStyles.marginLeft || ''),
activator: parseFloat(nodeStyles.marginTop || '0'),
container: parseFloat(nodeStyles.marginBottom || '0'),
horizontal: parseFloat(nodeStyles.marginLeft || '0'),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

defaulting to 0. an empty string didn't make sense for the tests.


expect((positionedOverlay.find('div').prop('style') as any).right).toBe(
0,
);
expect(
(positionedOverlay.find('div').prop('style') as any).left,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test didn't make sense. No matter whar left would be undefined, which it shouldn't be.

@dleroux
Copy link
Contributor Author

dleroux commented Jan 3, 2020

:( Just saw that safari still wraps for no good reason.

@dleroux dleroux changed the title [Popover] Set right position when preferred alignment is right. [WIP][Popover] Set right position when preferred alignment is right. Jan 3, 2020
@dleroux
Copy link
Contributor Author

dleroux commented Jan 6, 2020

Looks like this approach works perfectly on Chrome, but Firefox and Safari do not resize the parent PositionOverlay when the scrollbars appear. I was hoping a simple CSS change would fix it but I was unsuccessful.

@chloerice
Copy link
Member

chloerice commented Jan 8, 2020

Looks like this approach works perfectly on Chrome, but Firefox and Safari do not resize the parent PositionOverlay when the scrollbars appear. I was hoping a simple CSS change would fix it but I was unsuccessful.

Yeah, been there with these changes 😞 Every approach we tried was inconsistent across browsers. Only idea I have left is to start from scratch with this component... I feel like a lot of the logic is unnecessary, but because it's unclear what some of it is meant to do, refactoring it while trying to fix this just kept breaking it in other ways 😣.

@dleroux
Copy link
Contributor Author

dleroux commented Jan 8, 2020

@chloerice I thought of redoing it but after wrapping my head around it it's actually pretty good and I fear we'll end up in the same spot. The only thing I can think of is to add complexity and measure whether or not the content will cause scrollbars, then measure the scrollbar, then add the scrollbar width to the container. The math is all correct.

I asked @tmlayton to take a look and see if he can see something we're missing.

It works perfectly in Chrome right now. It's almost tempting to ship this.

@AndrewMusgrave
Copy link
Member

It's almost tempting to ship this.

We could always revert this when we have a better fix. Getting something out that'll work for the majority of consumers seems like a win to me.

@alex-page
Copy link
Member

alex-page commented Jan 10, 2020

I made this PR #2362 that starts the logic again from scratch, removing the confusing Math.max. This was working in all browsers last time I tested but it was a bigger code change and higher risk.

As Tim mentioned it needs tests to make sure it doesn't break anything.

@tmlayton tmlayton force-pushed the popover-position-fix branch from 5db1747 to a3998fa Compare January 11, 2020 00:34
@tmlayton tmlayton changed the title [WIP][Popover] Set right position when preferred alignment is right. [Popover] Set right position when preferred alignment is right. Jan 11, 2020
@tmlayton
Copy link
Contributor

Alright, this should be good now. When content can be scrolled in a Scrollable container, it sets overflow-y: scroll and reverts to auto when the content cannot be scrolled. This forces the layout engines to set the element width properly. Here are screenshots from Chrome, FF, Safari, and Edge with and without scrollbars:

Always on scrollbars

Screen Shot 2020-01-10 at 4 31 05 PM

Always on scrollbars with non-scrolling content

Screen Shot 2020-01-10 at 4 31 25 PM

Floating scrollbars (does not apply to Edge)

Screen Shot 2020-01-10 at 4 32 37 PM

Copy link
Contributor

@tmlayton tmlayton left a comment

Choose a reason for hiding this comment

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

👍

Copy link
Member

@kyledurand kyledurand left a comment

Choose a reason for hiding this comment

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

🌟 🙌

@tmlayton tmlayton merged commit ffd3ed5 into master Jan 13, 2020
@tmlayton tmlayton deleted the popover-position-fix branch January 13, 2020 18:26
@dleroux
Copy link
Contributor Author

dleroux commented Jan 20, 2020

@tmlayton you are a 🌟 🙇

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.

PositionOverlay incorrectly calculating the left value for right aligned components with scroll bars
6 participants