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

[Collapsible] Add support for disabling the transition, and fix state machine #7364

Merged
merged 14 commits into from
Oct 13, 2022

Conversation

Bringer128
Copy link
Contributor

@Bringer128 Bringer128 commented Oct 6, 2022

WHY are these changes introduced?

Fixes #7318

Today the Collapsible component can get stuck in the "animating" state for 2 reasons, both of which cause the onTransitionEnd event not to fire.

  1. The prop transition={{ duration: "0ms" }} is used as Navigation Items does.
  2. The children of the Collapsible have no margin and it is rendered for the first time with open=true (as raised in the linked issue). This causes the div to have max-height not change, which means the transition does not occur.

Fundamentally the issue with this component is that it relies on the onTransitionEnd event to move out of the animating state. There are 2 ways that this event won't fire, similarly related:

  1. If the animation is started but the height doesn't change (e.g. from 0px to 0px)
  2. If the animation is started but the duration is set to 0 (0ms, 0s or -some-var-that-evaluates-to-0)

This PR does not solve the dependency on the onTransitionEnd event. Instead it:

  1. Adds formal support for disabling the transition by setting transition={false}. This prevents the need to pass 0ms duration. (Note this does not stop the bug occurring)
  2. Handles if the user passes 0ms or 0s to also disable the transition.
  3. Removes the "re-measure on child update" logic, as this was a workaround for the fact that the component was getting stuck in the animating state.

As such there will still be a bug if the computed height of children is set to 0px. However I'm choosing not to address this in this PR as the chances are rare. To trigger this bug you must:

  1. Render the Collapsible in closed state (open={false})
  2. Toggle the open state to true, while the children is 0px high as computed by element.scrollHeight
  3. Change the children to have content

WHAT is this pull request doing?

  1. It reverts the changes from [Collapsible] Optionally prevent measuring onChildUpdate.  #6283 and [Collapsible] Re-measure when children change #6100 which caused the linked bug.
  2. Adds support for passing false to the transition prop to disable the transition.
  3. Changes the Navigation Secondary component to use transition={false}

How to 🎩

🖥 Local development instructions
🗒 General tophatting guidelines
📄 Changelog guidelines

Copy-paste this code in playground/Playground.tsx:
import {HomeMinor, OrdersMinor, ProductsMinor} from '@shopify/polaris-icons';
import React, {useState} from 'react';

import {Button, Card, Collapsible, Frame, Navigation} from '../src';

const list1 = [
  {label: 'a', url: 'path/to/one'},
  {label: 'b', url: 'path/to/two'},
];
const list2 = [
  {label: 'one', url: 'path/to/one'},
  {label: 'two', url: 'path/to/two'},
  {label: 'three', url: 'path/to/three'},
];
const list3 = [
  {label: '1', url: 'path/to/one'},
  {label: '2', url: 'path/to/two'},
  {label: '3', url: 'path/to/three'},
  {label: '4', url: 'path/to/four'},
  {label: '5', url: 'path/to/five'},
];

export function Playground() {
  const [open, setOpen] = useState(true);
  const toggleOpen = () => {
    setOpen(!open);
  };
  const scenario1 = (
    <div>
      <h1>Scenario 1</h1>
      <Collapsible open={open} id="1">
        <ul>
          <li>List item</li>
        </ul>
      </Collapsible>
    </div>
  );

  const scenario2 = (
    <div>
      <h1>Scenario 2</h1>
      <button onClick={toggleOpen}>Toggle open</button>
      <Collapsible open={open} id="2">
        <div>Hello</div>
      </Collapsible>
    </div>
  );

  const [selected, setSelected] = useState('home');
  const [subitems, setSubitems] = useState(list2);

  const scenario3 = (
    <Frame>
      <button onClick={() => setSubitems(list1)}>list 1</button>
      <button onClick={() => setSubitems(list2)}>list 2</button>
      <button onClick={() => setSubitems(list3)}>list 3</button>
      <Navigation location="/">
        <Navigation.Section
          items={[
            {
              url: '/',
              label: 'Home',
              icon: HomeMinor,
              selected: selected === 'home',
              onClick: () => setSelected('home'),
            },
            {
              url: '/',
              label: 'Orders',
              icon: OrdersMinor,
              subNavigationItems: subitems,
              selected: selected === 'orders',
              onClick: () => setSelected('orders'),
            },
            {
              url: '/',
              label: 'Products',
              icon: ProductsMinor,
              selected: selected === 'products',
              onClick: () => setSelected('products'),
            },
          ]}
        />
        <Navigation.Item
          url="/"
          label="Expected List"
          subNavigationItems={subitems}
        />
      </Navigation>
    </Frame>
  );
  const scenario4 = (
    <Card>
      Scenario 4
      <Collapsible id="4" open={open} transition={{duration: '100ms'}}>
        Content goes here
      </Collapsible>
    </Card>
  );

  const [firstOpen, setFirstOpen] = useState(false);
  const [secondOpen, setSecondOpen] = useState(false);
  const scenario5 = (
    <Card>
      <h1>Scenario 5</h1>
      <button onClick={() => setFirstOpen(!firstOpen)}>First</button>
      <button onClick={() => setSecondOpen(!secondOpen)}>Second</button>
      <Collapsible id="5" open={firstOpen} transition={{duration: '100ms'}}>
        <Collapsible id="5.1" open={secondOpen}>
          <div
            style={{width: '100px', height: '100px', backgroundColor: 'purple'}}
          />
        </Collapsible>
      </Collapsible>
    </Card>
  );

  const scenario6 = (
    <Card title="Scenario 6">
      <Collapsible id="6" open={open}>
        <div style={{height: 0}} />
      </Collapsible>
    </Card>
  );
  return (
    <div>
      <button onClick={toggleOpen}>Toggle open</button>
      {scenario1}
      {scenario2}
      {scenario3}
      {scenario4}
      {scenario5}
      {scenario6}
    </div>
  );
}

The code has 6 scenarios:

  • A Collapsible with ul as its child (this has margin-top and margin-bottom)
  • A Collapsible with div as its child (this has no margins)
  • The original scenario from [Collapsible] Re-measure when children change #6100 to show that this does not cause a regression.
  • Duration 100ms explicitly set
  • Nested collapsibles
  • A Collapsible that has children that is 0px high (not fixed by this PR)

🎩 checklist

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2022

size-limit report 📦

Path Size
polaris-react-cjs 209.11 KB (+0.03% 🔺)
polaris-react-esm 135.59 KB (+0.06% 🔺)
polaris-react-esnext 190.95 KB (+0.04% 🔺)
polaris-react-css 41.52 KB (0%)

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.

This is looking good for 0ms | 0s durations but there's a wee update loop bug when a non-zero duration is passed in.

collapsible.mp4

I can give you a hand tomorrow or Monday if you want it

Once we get rid of the loop we should:

  1. Make sure this works with nested Collapsible components
  2. Make sure this covers the needs of the last two contributors
  3. /snapit and ping folks for 🎩


Deprecated Collapsible preventMeasuringOnChildrenUpdate.
Fixed bug where Collapsible would get stuck in animating state.
Support for 0ms transition duration in Collapsible.
Copy link
Member

Choose a reason for hiding this comment

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

This also fixes #4194 👏

@aishad
Copy link
Contributor

aishad commented Oct 6, 2022

/snapit

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2022

🫰✨ Thanks @aishad! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

yarn add @shopify/plugin-polaris@0.0.0-snapshot-release-20221006204649
yarn add @shopify/polaris-migrator@0.0.0-snapshot-release-20221006204649
yarn add @shopify/polaris@0.0.0-snapshot-release-20221006204649

@Bringer128
Copy link
Contributor Author

This is looking good for 0ms | 0s durations but there's a wee update loop bug when a non-zero duration is passed in.

I can give you a hand tomorrow or Monday if you want it

Once we get rid of the loop we should:

  1. Make sure this works with nested Collapsible components
  2. Make sure this covers the needs of the last two contributors
  3. /snapit and ping folks for 🎩

Thanks for finding this! This was caused by adding a new dependency to the useEffect that triggers the transition. Every time the height changed, the startAnimation callback would change, which would trigger the useEffect to move to measuring state again. This caused the max-height to flip-flop between renders as it was trying to restart the animation. The incoming commit fixes it.

I also found thanks to your comment that 100ms was being caught by my regex because I forgot to catch the start and end of line stuff.

I notice in the Storybook for Collapsible that it sets the duration to a CSS variable. This was a scenario I hadn't considered, and the bug will still be present if the CSS variable evaluates to 0s or 0ms. Perhaps we should make disable an explicit prop? It looks like there are kind of hacky JavaScript ways to extract the global styles from the document element but it probably makes more sense to make an explicit prop.

@aishad
Copy link
Contributor

aishad commented Oct 7, 2022

I 🎩 to make sure there was no regression for our use case, and all looks good.
Created https://github.com/Shopify/web/issues/75074 to remove the prop from our Component.

@Bringer128
Copy link
Contributor Author

This blog shows how to get the value from a CSS property: https://www.designcise.com/web/tutorial/how-to-get-the-value-of-a-css-variable-using-javascript

In theory in this component we could do the following code:

const duration = transition?.duration;
const disableTransition = useMemo(() => {
  if(!duration || !collapsibleContainer.current) return false;
  if(/^0+(ms|s)$/.test(duration.trim()) return true;

  const styles = getComputedStyle(collapsibleContainer.current);
  const computedDuration = styles.getPropertyValue(duration);
  return /^0+(ms|s)$/.test(computedDuration);
}, [duration]);

@kyledurand
Copy link
Member

I'd prefer a prop over reading computed styles. I left this thought in slack but I'll leave it here as well for others to chime in but what if we merged the transitionDisabled prop with transition. The interface would look something like:

/** Assign transition properties to the collapsible
 * @default true
 */
transition?: boolean | Transition;
<Collapsible transition="false" /> 
<Collapsible transition={{duration: 'var(--p-duration-150)', timingFunction: 'var(--p-ease-in-out)'}} />

@kyledurand
Copy link
Member

/snapit

@github-actions
Copy link
Contributor

github-actions bot commented Oct 7, 2022

🫰✨ Thanks @kyledurand! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

yarn add @shopify/plugin-polaris@0.0.0-snapshot-release-20221007160227
yarn add @shopify/polaris-migrator@0.0.0-snapshot-release-20221007160227
yarn add @shopify/polaris@0.0.0-snapshot-release-20221007160227

@Bringer128
Copy link
Contributor Author

/snapit

@Bringer128
Copy link
Contributor Author

/snapit

@github-actions
Copy link
Contributor

github-actions bot commented Oct 7, 2022

🫰✨ Thanks @Bringer128! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

yarn add @shopify/plugin-polaris@0.0.0-snapshot-release-20221007203242
yarn add @shopify/polaris-migrator@0.0.0-snapshot-release-20221007203242
yarn add @shopify/polaris@0.0.0-snapshot-release-20221007203242

@Bringer128 Bringer128 changed the title [Collapsible] Handle transition duration of 0 [Collapsible] Add support for disabling the transition, and fix state machine Oct 11, 2022
@Bringer128
Copy link
Contributor Author

/snapit

@Bringer128
Copy link
Contributor Author

@kyledurand I've updated the PR description based on the solution we chose; and would love your fresh 👀 . I've tested nested Collapsibles and a few browsers and it all seems to work fine.

@github-actions
Copy link
Contributor

🫰✨ Thanks @Bringer128! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

yarn add @shopify/plugin-polaris@0.0.0-snapshot-release-20221011180026
yarn add @shopify/polaris-migrator@0.0.0-snapshot-release-20221011180026
yarn add @shopify/polaris@0.0.0-snapshot-release-20221011180026

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.

This is looking really good. My main concern would be disabling transition when 0 values are passed in as duration. Adding that means consumers don't have to use the new false flag for disabling the transition

transition?: Transition;
/** Prevents component from re-measuring when child is updated **/
/** Override transition properties. When set to false, disables transition completely. Defaults to true, which uses
* default transition properties.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* default transition properties.
* @default transition={{duration: 'var(--p-duration-150)', timingFunction: 'var(--p-ease-in-out)'}}

@@ -51,11 +53,14 @@ export function Collapsible({
expandOnPrint && styles.expandOnPrint,
);

const transitionDisabled = transition === false;
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 we still care about transition.duration being 0 || 0ms || 0s here. Can we disable transition on those values as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed in Slack, the CSS spec requires the unit for the transition-duration field so I'm not addressing the 0 scenario.

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.

👏 Fantastic work @Bringer128. Thanks for taking this on 🥇

@Bringer128 Bringer128 merged commit e4b2c36 into main Oct 13, 2022
@Bringer128 Bringer128 deleted the collapsible_handle_no_transition branch October 13, 2022 16:16
@github-actions github-actions bot mentioned this pull request Oct 13, 2022
kyledurand pushed a commit that referenced this pull request Oct 14, 2022
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @shopify/polaris-migrator@0.5.0

### Minor Changes

- [#7373](#7373)
[`56c82ee8d`](56c82ee)
Thanks [@aaronccasanova](https://github.com/aaronccasanova)! - Add
`getFunctionArgs` utility

### Patch Changes

- Updated dependencies
\[[`c3f427c17`](c3f427c)]:
    -   @shopify/polaris-tokens@6.2.1

## @shopify/polaris@10.8.0

### Minor Changes

- [#7364](#7364)
[`e4b2c36d8`](e4b2c36)
Thanks [@Bringer128](https://github.com/Bringer128)! - Deprecated
Collapsible preventMeasuringOnChildrenUpdate.
Fixed bug where Collapsible would get stuck in animating state when
duration is 0.
Add support for intentionally disabling the transition in Collapsible.

### Patch Changes

- [#7363](#7363)
[`8a6c323e2`](8a6c323)
Thanks [@aveline](https://github.com/aveline)! - Added `id` prop to
`Text` and `Box`


- [#7348](#7348)
[`ea2a45bbb`](ea2a45b)
Thanks [@aveline](https://github.com/aveline)! - Added `setMediaWidth`
breakpoints test utility


- [#7388](#7388)
[`5bc885765`](5bc8857)
Thanks [@kyledurand](https://github.com/kyledurand)! - Fixed a re-render
bug with Page Actions

- Updated dependencies
\[[`c3f427c17`](c3f427c)]:
    -   @shopify/polaris-tokens@6.2.1

## @shopify/plugin-polaris@0.0.11

### Patch Changes

- Updated dependencies
\[[`56c82ee8d`](56c82ee)]:
    -   @shopify/polaris-migrator@0.5.0

## @shopify/polaris-tokens@6.2.1

### Patch Changes

- [#7385](#7385)
[`c3f427c17`](c3f427c)
Thanks [@laurkim](https://github.com/laurkim)! - Refactored exported
alias and scale types in `breakpoints`, `depth`, `font`, `motion`,
`shape`, `spacing`, and `zIndex`.

## @shopify/stylelint-polaris@4.3.2

### Patch Changes

- Updated dependencies
\[[`c3f427c17`](c3f427c)]:
    -   @shopify/polaris-tokens@6.2.1

## polaris.shopify.com@0.22.0

### Minor Changes

- [#7032](#7032)
[`40ee692aa`](40ee692)
Thanks [@gwyneplaine](https://github.com/gwyneplaine)! - Added Playroom
integration to Polaris docs site.

### Patch Changes

- [#7032](#7032)
[`40ee692aa`](40ee692)
Thanks [@gwyneplaine](https://github.com/gwyneplaine)! - Improved the
design of the Sandbox feature.


- [#7400](#7400)
[`9f9fe1f99`](9f9fe1f)
Thanks [@kyledurand](https://github.com/kyledurand)! - Fixed a scaling
bug caused by content overflow
Fixed a bug where examples that don't have any content wouldn't show up
- Updated dependencies
\[[`8a6c323e2`](8a6c323),
[`e4b2c36d8`](e4b2c36),
[`c3f427c17`](c3f427c),
[`ea2a45bbb`](ea2a45b),
[`5bc885765`](5bc8857)]:
    -   @shopify/polaris@10.8.0
    -   @shopify/polaris-tokens@6.2.1

## polaris-for-figma@0.0.24

### Patch Changes

- Updated dependencies
\[[`8a6c323e2`](8a6c323),
[`e4b2c36d8`](e4b2c36),
[`ea2a45bbb`](ea2a45b),
[`5bc885765`](5bc8857)]:
    -   @shopify/polaris@10.8.0

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.

Collapsible does not always handle growing children
4 participants