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

Enable programmatic close on dropdown component #399

Merged
merged 5 commits into from
Jun 22, 2022

Conversation

alex-ju
Copy link
Member

@alex-ju alex-ju commented Jun 22, 2022

📌 Summary

Enable programmatic close on dropdown component.

🛠️ Detailed description

Based on feedback from @jgwhite we realised that we're not exposing a way for developers to close a dropdown component. In this PR we start by renaming the deactivate function in disclosure utility component to close then forward it to the dropdown component.

A simplified example of a call would look like this:

<Hds::Dropdown id="test-dropdown" as |dd|>
  <dd.ToggleButton @text="toggle button" id="test-toggle-button" />
  <dd.Interactive @route="components.dropdown" @text="interactive" id="test-list-item-interactive" {{on "click" dd.close}} />
</Hds::Dropdown>

🔗 External links

Jira issue


👀 How to review

👉 Review commit-by-commit

Reviewer's checklist:

  • +1 Percy if applicable
  • Confirm that PR has a changelog update via Changesets if needed

💬 Please consider using conventional comments when reviewing this PR.

We rename the `deactivate` function to `close` (to ease the adoption and keep it simple) and expose it as part of the disclosure utility component API
We forward the `close` function from disclosure to dropdown and expose it as part of the dropdown component API
@changeset-bot
Copy link

changeset-bot bot commented Jun 22, 2022

🦋 Changeset detected

Latest commit: 899dec3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hashicorp/design-system-components Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Jun 22, 2022

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated
hds-components ✅ Ready (Inspect) Visit Preview Jun 22, 2022 at 11:45AM (UTC)
hds-flight-website ✅ Ready (Inspect) Visit Preview Jun 22, 2022 at 11:45AM (UTC)

Copy link
Contributor

@didoo didoo left a comment

Choose a reason for hiding this comment

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

@alex-ju added a couple of comments related to the documentation (the other code is OK, actually very nice implementation and refactoring).

await click('button#test-toggle-button');
assert.dom('#test-dropdown #test-list-item-interactive').exists();
await click('#test-list-item-interactive');
assert.dom('#test-dropdown #test-list-item-interactive').doesNotExist();
Copy link
Contributor

Choose a reason for hiding this comment

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

[potential issue] what if it doesn't exist anymore, because in the meantime the page has navigated away? probably to avoid this risk in the render above (line 100) instead of a route to a different "page" I would use a @href="#"? (similar to what you did in the other test)

PS: if you want to test the actual behaviour you can

  • add a line after line 106, with this code: await pauseTest(); (the method needs to be imported from @ember/test-helpers)
  • launch the tests via ember test --server --filter 'Integration | Component | hds/dropdown/index' - this will launch a new browser that renders the content, and pause the test wherever you put the pauseTest() call, and you can inspect the DOM and see what it does

(FYI I did the test, and strangely enough it shows both the dropdown and the content of the target webpage, so not sure how to interpret this).

Copy link
Member Author

@alex-ju alex-ju Jun 22, 2022

Choose a reason for hiding this comment

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

Fair concern. I forgot the trigger in this example is not a simple HTML button element, but an interactive component with a set route which renders as a link.

<ul class="dummy-disclosure-content-list-of-links">
<li>
<a href="https://google.com">Link to Google</a>
</li>
<li>
<a href="https://apple.com">Link with a much longer text</a>
<a href="https://apple.com" {{on "click" c.close}}>Link to Apple (closes the disclosed content on click)</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

[nitpick] probably in order to be able to see that actually closes the dropdown, it would be better to add target="_blank" to the link (otherwise it opens in the same window and everything disappears). Another option would be to have a <button> instead of a link, that does nothing more that closing the disclosure?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tested it locally with command+click so I preserve the current page, but yes, button would be more suitable. Same for integration testing.

@@ -444,7 +448,7 @@
&lt;dd.Interactive @route="..." @text="Item One" /&gt;
&lt;dd.Interactive @route="..." @text="Item Two" /&gt;
&lt;dd.Interactive @route="..." @text="Item Three" /&gt;
&lt;dd.Interactive @route="..." @text="Item Four" /&gt;
&lt;dd.Interactive @route="..." @text="Item Four (closes on click)" \{{on "click" dd.close}} /&gt;
Copy link
Contributor

Choose a reason for hiding this comment

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

[nitpick] same as above: this reloads the page, so there is no way to check that actually works. If you remove the @route it will render a <button> and when you click it you see that it closes.

@alex-ju
Copy link
Member Author

alex-ju commented Jun 22, 2022

@alex-ju added a couple of comments related to the documentation (the other code is OK, actually very nice implementation and refactoring).

Good points about using buttons for clarity in examples and accuracy in tests. I've updated based on feedback and it's good for a 2nd review.

@alex-ju alex-ju requested a review from didoo June 22, 2022 11:44
@vercel vercel bot temporarily deployed to Preview – hds-flight-website June 22, 2022 11:45 Inactive
@vercel vercel bot temporarily deployed to Preview – hds-components June 22, 2022 11:45 Inactive
Copy link
Contributor

@didoo didoo left a comment

Choose a reason for hiding this comment

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

Very nice!

@alex-ju alex-ju merged commit a912a72 into main Jun 22, 2022
@alex-ju alex-ju deleted the alex-ju/enable-programmatic-close-on-dropdown branch June 22, 2022 12:27
@hashibot-hds hashibot-hds mentioned this pull request Jun 22, 2022
@jgwhite
Copy link
Contributor

jgwhite commented Jun 22, 2022

Thanks for this!

@jgwhite
Copy link
Contributor

jgwhite commented Jun 30, 2022

I just discovered a fun little footgun related to this feature that others may run into…

So, given the use case from the description:

<Hds::Dropdown id="test-dropdown" as |dd|>
  <dd.ToggleButton @text="toggle button" />
  <dd.Interactive
    @route="components.dropdown"
    @text="interactive"
    {{on "click" dd.close}}
  />
</Hds::Dropdown>

My assumption was that {{on "click" dd.close}} is added after whatever listeners <LinkTo> adds implicitly, allowing Ember to do its normal routing stuff first, then closing the dropdown after.

Unfortunately, this was a faulty assumption. A peek at link-to.hbs reveals the truth:

<a
  {{!-- for compatibility --}}
  id={{this.id}}
  class={{this.class}}

  {{!-- deprecated attribute bindings --}}
  role={{this.role}}
  title={{this.title}}
  rel={{this.rel}}
  tabindex={{this.tabindex}}
  target={{this.target}}

  ...attributes  <---------------+
                                 |
  href={{this.href}}             | OH MY
                                 |
  {{on 'click' this.click}}  <---+
>{{yield}}</a>

Splattributes are applied before the click handler 😬 Which means dd.close is called first, which in turn means the element and all event handlers are removed before we ever get to that implicit click handler 🤦‍♀️

Funnily enough, the result almost looks like it’s working, because we still have a perfectly functional href, so it’s easy not to notice the browser is doing a full page load.

The solution is to schedule the dd.close for the next event cycle, something like this:

{{! my-component.hbs}}

<Hds::Dropdown id="test-dropdown" as |dd|>
  <dd.ToggleButton @text="toggle button" />
  <dd.Interactive
    @route="components.dropdown"
    @text="interactive"
    {{on "click" (fn this.later dd.close)}}
  />
</Hds::Dropdown>
// my-component.js

export default class extends Component {

  @action
  later(fn) {
    // Runs fn in the next event cycle (after sync listeners)
    setTimeout(fn);
  }

}

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.

3 participants