Code Monkey home page Code Monkey logo

ember-basic-dropdown's Introduction

Ember-basic-dropdown

Build Status

This is a very minimal dropdown. That means that it is agnostic about what it is going to contain.

It is intended to be a building block for more complex components but is perfectly usable. It is by example the addon on which ember-power-select or ember-paper's menu component are built upon.

Compatibility

  • Ember.js v3.28 or above
  • Ember CLI v3.28 or above

Versions 1.X require Ember 2.16 or greater

Version 2.X requires Ember 3.13 or greater

Version 3.X - 6.X requires Ember 3.24 or greater

Version 7.X - 8.X requires Ember 3.28 or greater

Installation

ember install ember-basic-dropdown

For more installation details see documentation

Usage

This component leverages contextual components for its API:

<BasicDropdown as |dd|>
  <dd.Trigger>Click me</dd.Trigger>
  <dd.Content>Content of the trigger</dd.Content>
</BasicDropdown>

The yielded dropdown object is the public API of the component, and contains properties and actions that you can use to control the component.

{
  uniqueId: <string>,
  isOpen: <boolean>,
  disabled: <boolean>,
  actions: {
    open: <action>,
    close: <action>,
    toggle: <action>,
    reposition: <action>
  }
}

Check the full documentation with live examples in http://ember-basic-dropdown.com

Features

Renders on the body or in place

By default this component will render the dropdown in the body using #-in-element and absolutely position it to place it in the proper coordinates.

You can opt out to this behavior by passing renderInPlace=true. That will add the dropdown just below the trigger.

Close automatically when clicking outside the component

You don't need to care about adding or removing events, it does that for you.

You can make the dropdown content standout a little more by adding overlay=true to the content options, see example below. This will add a semi transparent overlay covering the whole screen. Also this will stop bubbling the click/touch event which closed the dropdown.

<BasicDropdown as |dd|>
  <dd.Trigger>Click me!</dd.Trigger>
  <dd.Content @overlay={{true}}>
    {{! here! }}
    content!
  </dd.Content>
</BasicDropdown>

NOTE: If for some reason clicking outside a dropdown doesn't work, you might want to make sure the <body> spans the entire viewport. Adding a css rule like body {min-height: 100vh;} would do the trick. It ensures that wherever you click on the page, it will close the dropdown.

Close automatically when clicking inside the component

If you'd like the dropdown to close itself after a user clicks on it, you can use dd.actions.close from our public API.

<BasicDropdown as |dd|>
  <dd.Trigger>Click me!</dd.Trigger>
  <dd.Content>
    <div {{action dd.actions.close}}>
      {{yield dd}}
    </div>
  </dd.Content>
</BasicDropdown>

Keyboard and touchscreen support

The trigger of the component is focusable by default, and when focused can be triggered using Enter or Space. It also listen to touch events so it works in mobile.

Easy to extend

The components provide hooks like onFocus, onBlur, onKeydown, onMouseEnter and more so you can do pretty much anything you want.

Easy to animate.

You can animate it, in an out, with just CSS3 animations. Check the example in the Ember Power Select documentation

Intelligent and customizable positioning

This component is smart about where to position the dropdown. It will detect the best place to render it based on the space around the trigger, and also will take care of reposition if if the screen is resized, scrolled, the device changes it orientation or the content of the dropdown changes.

You can force the component to be fixed in one position by passing verticalPosition = above | below and/or horizontalPosition = right | center | left.

If even that doesn't match your preferences and you feel brave enough, you can roll your own positioning logic if you pass a calculatePosition function. It's signature is:

calculatePosition(trigger, dropdown, { previousHorizontalPosition, horizontalPosition, previousVerticalPosition, verticalPosition, matchTriggerWidth })

The return value must be an object with this interface: { horizontalPosition, verticalPosition, style } where where horizontalPosition is a string ("right" | "center" | "left"), verticalPosition is also a string ("above" | "below") and style is an object with CSS properties, typically top and left/right.

Test helpers

It has a handy collection of test helpers to make interaction with the component seamless in your test suite.

Providing an Ember Twiddle

If you want to provide an Ember Twiddle with an issue/reproduction you need to add the following to the end of your template: <div id="ember-basic-dropdown-wormhole"></div>

Since Ember Twiddle does not run EmberCLI's hooks this div won't be added to the application and it's required (There's an issue in Ember Twiddle tracking this).

In order to create the Ember Twiddle you'll also need to add a reference to ember-basic-dropdown: version in the addons section of twiddle.json

ember-basic-dropdown's People

Contributors

chrisgame avatar cibernox avatar dependabot[bot] avatar ef4 avatar gilest avatar gwak avatar ilucin avatar jdurand avatar jerwilkins avatar johanrd avatar kilowhisky avatar ksin avatar locks avatar mayatron avatar meirish avatar miguelcobain avatar mkszepp avatar mydea avatar nathanhammond avatar paulcwatts avatar raido avatar rastopyr avatar rlivsey avatar serabe avatar simonihmig avatar snewcomer avatar ssured avatar techn1x avatar xiphiasuvella avatar xomaczar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

ember-basic-dropdown's Issues

destination and position of dropdown

I am seeing two issues in my app with this component.

For the first issue, unless I specify the destination, destination="foo", the dropdown always appears at the very top of the page.

With the second issue, the position is not being picked up. What I mean by that is, when the target is all the way at the bottom of the page, the dropdown should reposition and display the dropdown above the target...but it does not. Similarly, if I add "verticalPosition=above" it has no affect and the dropdown is always below the target.

My feeling is that these two issues are related. Sadly, I can not reproduce the issues in a brand new clean app that only contains this plugin...so I must have something that is interfering. Any thoughts on what might be causing these two issues?

Support keyboard navigation

While I recognize this has a broader scope than just as a form element replacement, it will likely be used for that purpose pretty often. Given that, most standard browser dropdowns allow for keyboard navigation, which helps with accessibility

  • up/down arrows open the dropdown and position the focus on the last/first item respectively
  • up/down arrows navigate up and down the dropdown items after the dropdown is open
  • typing any letter should focus the first element whose first letter matches
  • typing any letter again should focus the next element whose first letter matches
  • hitting space or enter when an item is focused selects that item and closes the dropdown
  • hitting tab when an item is focused selects that item, closes the dropdown and moves to the next element in the page's taborder

horizontalPosition="center" do not work

Trying to apply horizontalPosition="center" seems to stop working after some update

{{#basic-dropdown horizontalPosition="center" onOpen=(action "openDropdown") as |dropdown|}}
  {{#dropdown.trigger}}
    Trigger
  {{/dropdown.trigger}}

  {{#dropdown.content}}
    Content
  {{/dropdown.content}}
{{/basic-dropdown}}

We expect it to be centered, but instead its position by default, to left

Should ember-native-dom-helpers be in dependencies, rather than devDependencies?

I was including clickTrigger in a component integration test like this:

import { clickTrigger } from '../../helpers/ember-basic-dropdown';

When I upgraded from v0.24.2 to v0.24.4, I got the error:

Error: Could not find module `ember-native-dom-helpers/test-support/helpers` imported from `tycho/tests/helpers/ember-basic-dropdown`
TypeError: Cannot read property 'exports' of undefined at http://localhost:7357/assets/test-support.js, line 5774

ember-native-dom-helpers isn't getting installed via yarn because it's in devDependencies.

Should this library be included in dependencies so that users of the test helpers don't have to install the library manually? Or am I including clickTrigger incorrectly?

`calculateWormholedPosition` is not correct

When calling calculateWormholedPosition the content is not at his final position which can impact trigger.getBoundingClientRect().

Should we not first attach the content to the body and then call calculateWormholedPosition?

Adding hide/show actions

Hey,

I have a edge case (so it is totally fine if you don't want that in here). My dropdown is composed of buttons that trigger a confirm modal. I use ember-modal-dialog. The issue is that when I click the button inside the dropdown, I want to open the modal and hide the dropdown. I can't use dropdown.actions.close because it destroys the dropwdown and my modal at the same time. So I was wondering if you could had a show and a hide action that would keep the dropdown in the DOM while making it invisible.

UPDATE: it is actually trickier than what I describe because any click outside the modal will cause it to close ๐Ÿ˜• . Any idea how to work around that?

Thanks!

Does not work with ember-cli-sass

Created a new cli project with the following devDependencies:

  "ember-basic-dropdown": "0.11.0",
  "ember-cli-sass": "5.3.1"

and ember-basic-dropdown is no longer able to calculate the proper coordinates.

The sample usage from the README, for example yields the following:

<div id="ember-basic-dropdown-content-ember383" class="ember-basic-dropdown-content ember-basic-dropdown--transitioning-in  ember-basic-dropdown--below ember-basic-dropdown--left" style="width: 1447px; top: 44px; left: 8px;">
        Content of the dropdown once it appears


</div>

Note the style attribute: style="width: 1447px; top: 44px; left: 8px;"

Content width not matching trigger width

I am using ember-basic-dropdown(v0.19.4) from ember-power-select(1.0.3) which comes within ember-paper(v1.0.0-alpha.13) and is used from ember-paper-select.

The issue is that when an option that is below the first visible options is selected, the dropdown content width does not match the trigger width.

When the "reposition" function is called, the "calculatePosition" function variable seems to take on a function from paper-autocomplete-dropdown.js instead. I believe it was supposed to use one of the functions from utils/calculate-position, but that doesn't happen. I am not sure if this behavior is intended or if there is a problem. If it is an intentional behavior, then the problem is just to make if calculate the width correctly.

Example video of the issue: http://sendvid.com/sifwmukd

Always use integral values for dropdown position and dimensions

Fractional dropdown width causes surprising behaviour with certain styles in Chrome 54. Please see this JSfiddle. You should observe a 1px vertical red line, despite inner and outer container having the same width.
Even though this is a Chrome bug, I don't think that it makes sense to operate on subpixel level. Or does it?

Integration tests failing due to missing `service:-document`

I am currently unable to test a component's use of {{ember-power-select}} in an integration test, due to a missing injection related to ember-basic-dropdown's use of ember-wormhole. Acceptance tests work fine.

For reference, I don't believe this deficiency is related directly to ember-basic-dropdown or its consumers, but folks may find this helpful when researching why their integration tests are failing.

To replicate:

  • Create a custom component that implements an ember-basic-dropdown
  • Render the custom component in an integration test
  • Invoke the clickTrigger helper
  • [your test should now be throwing]

The same symptoms have been observed in ember-wormhole; an issue is being tracked upstream at yapplabs/ember-wormhole#77

h/t @chundabear, who first saw this and was instrumental in tracing the error :)

Feature request: Horizontal alignment

It would be very nice if there was a possibility to influence the horizontal placement.

Right when you create a dropdown on the far right side, it will be pushed against the right border of the window:
image

When setting the CSS position with right instead of left, the result is nicer:
image

Is this on the component roadmap? Thanks for the nice lib!

Closing dropdown from child element causes Ember to skip child element action

Not sure if it is a good place to ask this question it maybe not this plugin's fault.

So here is the deal: I have dropdown content yielded to parent element and I want that when user click on any link in that content, dropdown would be closed. I do this by first registering dropdown with my component:

<div class="{{dropdownWrapperClass}}">
  {{#basic-dropdown registerAPI=(action 'registerDropdown') as |dropdown|}}
    {{#dropdown.trigger}}Trigger{{/dropdown.trigger}}
    {{#dropdown.content renderInPlace=true}}
      <ul class="{{dropdownMenuClass}}" {{action 'mouseDown' on='click'}}>
        {{yield}}
      </ul>
    {{/dropdown.content}}
  {{/basic-dropdown}}
</div>

Then when mouseDown action is called, I check if e.target is a link or button, here is component code:

mouseDown: function(e) {
  const $target = Ember.$(e.target);
  const ul = $target.parents(`.${this.get('dropdownMenuClass')}`)[0];
  if ($target.is('button, a:link') && ul) {
      this.get('dropdown').actions.close();
  }
  return false;
},

actions: {
  registerDropdown: function(dropdown) {
    this.set('dropdown', dropdown);
  },
}

When I do this, dropdown gets closed but the action that was assigned on the link does not get executed.

Now there is a workaround for this problem:

Ember.$(ul).css('opacity', '0'); // Hide the dropdown.content so the action will seem immediate to user
Ember.run.later(() => {
  Ember.$(ul).css('opacity', '1');
  self.get('dropdown').actions.close(); // Now really close it
 }, 100);

However I don't like solutions based on some magic timeouts.

I dug into the code of previous plugin that I've used: rl-dropdown, looks like they've toggled display: none/block css property to control visibility of dropdown.content which worked fine.

I think that this use case is quite common, you do need to close dropdown when child of content is clicked, so there are couple of things I would like to suggest:

  1. It would be nice to add similar functionality that rl-dropdown has, that will allow users of plugin pass selector which will cause dropdown to close on matching elements, basically:
{{#dropdown.content closeOnClickClick='.button'}}
  1. If there is no way to add close event to the end of Ember event call chain (I don't know any) then I think we are stuck with current solution. Unless you think that it is possible to change the way dropdown is closed right now by manipulating visibility instead not rendering content at all.

Dropdown cannot be triggered via mousedown or click events

In our acceptance tests, we use click('.ember-basic-dropdown-trigger') to trigger the dropdown. It worked well in ember-basic-dropdown version 0.8.2. But once we updated to version 0.11.7, click('.ember-basic-dropdown-trigger') didn't work anymore.

The only workaround we found is to create a native js event and dispatch it to the element:

var event = new Event('mousedown');
document.querySelector('.ember-basic-dropdown-trigger').dispatchEvent(event);

ember-concurrency version brakes my app

ember-concurrency version 7.11 is marked as "Latest release" which allows using tasks like this this.myTask.perform();.

ember-basic-dropdown uses version 7.19 which for some reason does not allow that functionality thus breaking my app.

MutationObserver issue around transition to or from route with ember-basic-dropdown

I am seeing this issue in v0.31.0-beta.1 via ember-power-select v1.6.0-beta.0 in Ember 2.10.

The routes I am switching between both return an RSVP.hash of 2-3 model types. I switched the queries for one model type in both hashes to a return peekAll request but then gets it's contents refreshed by a query in the background.

Since that change I now get a MutationObserver error similar to the one from this issue (#33) when I transition into and out of the route containing a basic-dropdown. Transitioning into the route, it reads (when evaluating ember-basic-dropdown/components/basic-dropdown/content.js):

Uncaught TypeError: Cannot read property 'MutationObserver' of undefined
    at Module.callback (content.js:7)
    at Module.exports (loader.js:123)
    at Module.reify (loader.js:144)
    at Module.exports (loader.js:121)
    at requireModule (loader.js:23)
    at Class._extractDefaultExport (resolver.js:368)
    at Class.resolveOther (resolver.js:81)
    at Class.superWrapper [as resolveOther] (ember.debug.js:39641)
    at Class.resolve (ember.debug.js:5050)
    at resolve (ember.debug.js:2445)

The statement on line 7 reads:

var MutObserver = self.window.MutationObserver || self.window.WebKitMutationObserver;

Here, self refers to one of the component instances on the page. Each component instance gets each model array from the hash, including the model that is peek'd, passed into it. One of those models in the array is used to populate the dropdown list on the page being transitioned into.

When I transition away from the route containing the dropdowns I see this (in /ember-basic-dropdown/components/basic-dropdown/trigger.js):

trigger.js:46 Uncaught TypeError: Cannot read property 'body' of undefined
    at Class.willDestroyElement (trigger.js:46)
    at Class.superWrapper [as willDestroyElement] (ember.debug.js:39641)
    at Class.trigger (ember.debug.js:41457)
    at Class.superWrapper [as trigger] (ember.debug.js:39641)
    at ComponentStateBucket.destroy (ember.debug.js:12005)
    at UpdatableBlockTracker.destroy (ember.debug.js:45050)
    at SimpleBlockTracker.destroy (ember.debug.js:45050)
    at SimpleBlockTracker.destroy (ember.debug.js:45050)
    at SimpleBlockTracker.destroy (ember.debug.js:45050)
    at UpdatableBlockTracker.destroy (ember.debug.js:45050)

the statement in question:

self.document.body.removeEventListener('touchmove', this._touchMoveHandler);

The model array used to populate the dropdown options on the first page is used to populate the primary content space on the second page, and while it is not the set of records that has been switched to the peekAll request method, the results of that peekAll request gets passed into the components populated by the model array used in the dropdowns, and I think that's where things are breaking down.

If I refresh the page with the dropdowns by itself, the page loads fine as do the dropdowns. If I comment out the dropdowns, the page transitions without issue.

Any thoughts on what might be happening here? Thanks in advance.

Could not find module `ember-get-config` imported from `ember-basic-dropdown/components/basic-dropdown/content`

Hey, with the latest release we are getting an error in our tests:

Could not find moduleember-get-configimported fromember-basic-dropdown/components/basic-dropdown/content`

Pinning ember-basic-dropdown to 0.16.0-beta.4 or ember-get-config to 0.1.7 works fine, so it seems something has broken in the latest release of either of those.

We are pulling this library in from `ember-power-select.

Stable Root Element (Glimmer 2)

This addon seems to be almost compatible with Glimmer 2. However the wormholed dropdown sometimes cauces an error in my app. I guess this is related to the need for a stable root element as mentioned in the latest ember-wormhole release.

This:

{{#ember-wormhole to=to renderInPlace=renderInPlace}}
  {{#if overlay}}
    <!-- ... -->
  {{/if}}
{{/ember-wormhole}}  

Needs to be changed to something like:

{{#ember-wormhole to=to renderInPlace=renderInPlace}}
  <div class="something">
    {{#if overlay}}
      <!-- ... -->
    {{/if}}
  </div>
{{/ember-wormhole}}  

Would that be feasible?

Unable to get property 'removeEventListenter' of undefined or null reference

Browser: IE10

In willDestroy when MutObserver is undefined, removeGlobalEvents is called, which attempts to grab the selector .ember-basic-dropdown-content (https://github.com/cibernox/ember-basic-dropdown/blob/master/addon/components/basic-dropdown.js#L181).

This causes the aforementioned error to be raised since .ember-basic-dropdown-content has already been removed from the DOM.

Does removeGlobalEvents even need to be called? Unless I'm mistaken, it seems like the events are added only when toggling the dropdown. Do you have any suggestions?

Modifying Ember 2.10 hPosition

Upgrading to 2.10 shows this error in an acceptance test run. In one of the tests, for ex//, I am using selectChoose to modify a few power selects on the page. Do you think this is this a symptom of my code/test or how basic-dropdown is used in power select? Looks like the hPosition is used in the trigger and content sections of ember-basic-dropdown. On beta.31 of power select btw.

screen shot 2016-11-30 at 1 42 06 pm

emberjs/ember.js#13948

Test helper `clickTrigger` does not work in IE

I wrote my tests using your helper clickTrigger. When testing in IE, I get the following error message:

TypeError: Object doesn't support this action
  at nativeClick (http://localhost:7357/assets/tests.js:1112:5)
  at clickTrigger (http://localhost:7357/assets/tests.js:1145:5)
  at Anonymous function (http://localhost:7357/assets/tests.js:1943:8)
  at invoke (http://localhost:7357/assets/test-support.js:13227:5)
  at method (http://localhost:7357/assets/test-support.js:13292:13)
  at callFnAsync (http://localhost:7357/assets/test-support.js:4500:5)
  at Runnable.prototype.run (http://localhost:7357/assets/test-support.js:4452:7)
  at Runner.prototype.runTest (http://localhost:7357/assets/test-support.js:4940:5)
  at Anonymous function (http://localhost:7357/assets/test-support.js:5046:7)
  at next (http://localhost:7357/assets/test-support.js:4860:7)

Tested in IE11 via SauceLabs

Body height is required to trigger close on click

I'm working on a new app with just a header, so the body has a height of just 40px. Clicking anywhere but on the header (aka body) would fail to close the modal.

A simple css line fixed that: body {min-height: 100vh;}.

It is unlikely to happen in an actual app and not a bug per say but I thought I would share anyway. Maybe it could be added to the docs.

Animating dropdown

Hi!

I'm not sure if this should be an issue or possibly a wiki article, or maybe in the readme. But, do you have any suggestions for animating the dropdown? I've tried a few approaches using css transitions and using liquid-fire, and I find it difficult to do well because it appears that everything I've tried is messing up the positioning on the drop until the drop is fully open. So for instance, it will animate, but the drop will open far to the right of where it should be on the page, and then jump to the correct location after the animation is done.

Thanks!

How can I close opened dropdown?

I have this code:

{{#basic-dropdown disabled=(not selectedEvents.length) isOpen=eventsActionsIsOpen as |dropdown|}}
      {{#dropdown.trigger}}
        <button class="button margin-bottom-0" disabled={{not selectedEvents.length}}><i class="mdi mdi-dots-vertical"></i> ะ”ะตะนัั‚ะฒะธั</button>
      {{/dropdown.trigger}}
      {{#dropdown.content}}
        <ul class="dropdown menu vertical">
          <li class="dropdown-item" {{action 'cancelEvents'}}>ะžั‚ะผะตะฝะธั‚ัŒ</li>
          <li class="dropdown-item" {{action 'deleteEvents'}}>ะฃะดะฐะปะธั‚ัŒ</li>
        </ul>
      {{/dropdown.content}}
    {{/basic-dropdown}}
  actions: {
    cancelEvents() {
      ...
      this.set('eventsActionsIsOpen', false);
    }
  }

But it don't work. How can I close opened dropdown?

Possible memory leak at 'triggerElement'

I've probably found a memory leak within your addon at the triggerElement property. It seems that it doesn't get cleared when the dropdown is destroyed. See emberjs/ember.js#14626 for a full description and the results found by @robbiepitts here

(In our web-application we use ember-power-select ๐Ÿ‘ )

Any way to do nested dropdown?

I'm trying to do nested dropdown with this plugin. For nesting, I need to load a dropdown to the side of an element of the parent dropdown. Any idea how can this be done?

[RFC] New (probably final) API for 1.0

This was on my plans for a long time but 1.13 compatibility was holding me back from implementing it.

Ember 2.0 was release the 13th of August of 2015, and it's time to move forward and take advantage of the new features introduced since then. This proposed new API makes usage of some of those new features like contextual components.

Rationale: A critic of the current API

First of all, let's audit the current API of this component and highlight what I think are it's mayor flaws.

{{#basic-dropdown verticalPosition="above" ariaLabel="Terms of use" renderInPlace=true as |dropdown|}}
  <p>Lorem ipsum dolor sit amet ...</p>
  <img src="gumpy-cat.png"/>
{{else}}
  <button>Click me</button>
{{/basic-dropdown}}

On the surface, this API is not very complicated, but there is a few limitations

The yielded public API is only available in one of the blocks

This is a limitation in ember itself. You can't yield arguments to the inverse block, so the end user cannot make use of the public API yielded in the first block, that corresponds to the content of the dropdown when opened.

{{#basic-dropdown verticalPosition="above" ariaLabel="Terms of use" renderInPlace=true as |dropdown|}}
 ...
{{else}}
  <!-- What if I want to use the current API here? I can't -->
{{/basic-dropdown}}

Unexpected order of blocks

It is, at the very least, surprising that the order of the blocks. The reason for that derives the the previous point. Since I the yielded public API is only available to one of the blocks, I had to choose which one was more likely to make use of it. I decided that the content had more chances of need access to it, and I made it the first block, despite of being unexpected.

LMAO component (large monolith with an army of options)

I coined this term in my talk in last Embercamp London about building composable component. I warned about the danger of this and yet, I found myself unable to prevent this from happening to me. There was just too much behaviour that this component had to support to be flexible to not end up being a LMAO.

Luckily, most of them are optional, but if you decided to override all defaults, you would end up with this:

{{#basic-dropdown
  animationEnabled=...
  disabled=...
  renderInPlace=...
  role=...
  destination=...
  initiallyOpened=...
  hasFocusInside=...
  verticalPosition=...
  horizontalPosition=...
  dir=...
  class=...
  triggerClass=...
  dropdownClass=...
  onOpen=...
  onClose=...
  onFocus=...
  onMouseEnter=...
  onMouseLeave=...
  ariaDescribedBy=...
  ariaInvalid=...
  ariaLabel=...
  ariaLabelledBy=...
  ariaRequired=...
}}

I counted 23 options and maybe there is even some more, I don't know. The fact that I can't even be sure is the scary part.

What can I say? I'm sorry.

Don't get me wrong, every one of this options fulfills a role and is necessary, but the fact that this component has to be aware of all of them is an antipattern.

As I see it, there is a clear culprit for this antipattern. Since the public API is composed of a single component, that component is the entry point of every single option. That is something I intend to fix with the proposal.

Constrained HTML markup

This is better explained with an example.

<div id="ember317" class="ember-view ember-basic-dropdown ember-basic-dropdown--below ember-basic-dropdown--left">
  <div aria-haspopup="true" class="ember-basic-dropdown-trigger " aria-controls="ember-basic-dropdown-content-ember317" aria-disabled="false" aria-expanded="true" aria-pressed="true" role="button" tabindex="0">
    <button>Click me</button>
  </div>
  <div id="ember338" class="ember-view"><!-- The content is rendered in the root of the body --></div>
</div>

Let's analyze the problem with this:

  1. Unnecessary wrapper div. The top-level div doesn't really fulfill any mission. Those cases are there but could have been in the trigger/content just as well.
  2. The .ember-basic-dropdown-trigger element wraps the user-provided html. This is a problem when the user wants the content to be opened by an element like button, an input. Currently the user's button is contained inside the trigger instead of being the trigger. Also, how does the trigger open and close the component if that button is not bound do any action? Magic? I know the answer, but you probably don't.
  3. The user can't introduce content before/after/between the trigger and the content parts. This prevent the user to leverage this component to create trigger where only part of the markup acts as trigger. Think by example in a datepicker that is opened when the use click in the calendar icon attached to a text input.

screen shot 0028-05-29 at 13 59 35

## New proposed API

The challenge of this task is to make the component more flexible and at the same time make the API easier to use and more natural. Sounds hard but I think that contextual components can to achieve this goal.

In summary, the new proposed API in it's simplest form is this one:

{{#basic-dropdown as |dropdown|}}
  {{#dropdown.trigger}}Click me{{/dropdown.trigger}}
  {{#dropdown.content}}This is the content{{/dropdown.content}}
{{/basic-dropdown}}

This is it. Now let me analyze point by point why I think this is a huge improvement

The yielded public API is only available in one of the blocks

While this is still technically true, now both the trigger and the content are independent components
renderes inside the main (and only) block, so the yielded attributes are available in both.

Unexpected order of blocks

Being free of the previous limitation also means that the order of the components is decided by the user.
Choose whatever you want.

LMAO component (large monolith with an army of options)

While the component will continue to be customizable in the same ways, now the internal components
are exposed to the end user, and therefore there is no longer a single point of entry for all
the configuration of the addon.

If we think about the options, some existes just because the internal components weren't public. The best
examples are triggerClass and dropdownClass. Why do we need this options at all if we have direct
access to trigger and content components?

{{#basic-dropdown as |dropdown|}}
  {{#dropdown.trigger class="my-trigger"}}Click me{{/dropdown.trigger}}
  {{#dropdown.content class="my-content"}}This is the content{{/dropdown.content}}
{{/basic-dropdown}}

About the rest of the options, now some of them belong to the parent component, some to the trigger
and some to the content. This is the worst posible scenario where you override every single option of the component.

{{#basic-dropdown 
  initiallyOpened=true 
  onOpen=...
  onClose=...
  as |dropdown|}}

  {{#dropdown.trigger 
    disabled=true
    role=...
    onFocus=...
    onMouseEnter=...
    onMouseLeave=... 
    ariaDescribedBy=...
    ariaInvalid=...
    ariaLabel=...
    ariaLabelledBy=...
    ariaRequired=...
    dir=...       
    class="my-trigger"}} Click me {{/dropdown.trigger}}

  {{#dropdown.content 
    animationEnabled=true
    renderInPlace=true
    destination="#placeholder"
    verticalPosition="above"
    horizontalPosition="right"
    onMouseEnter=...
    onMouseLeave=...    
    class="my-content"}} This is the content {{/dropdown.content}}
{{/basic-dropdown}}

In this example, the same 23 options are now passed only to the components that actually cares about them.
This even allow more granularity, because I might care about mouseEnter/mouseLeave events over
the dropdown.content component but not on the trigger. Or I might want to invoke a different action
on each.

The bottom line of this approach is that the LMAO (the M stands for monolith) is no longer a monolith,
and although the number of configuration options is not any smaller, each component cares just about a subset of them.

Constrained HTML markup

Let's go back to the datepicker component I mentioned before, that was impossible to build.

screen shot 0028-05-29 at 13 59 35

The desired markup can now be achieved this way:

{{#basic-dropdown as |dropdown|}}
  <div class="input-and-button">
    <input type="text">
    {{#dropdown.trigger tagName="button" class="input-affix"}}<i class="icon-calendar"></i>{{/dropdown.trigger}}
  </div>
  {{#dropdown.content}}This is the content{{/dropdown.content}}
{/basic-dropdown}}
  1. Unnecessary wrapper div. The top-level component is now tag less. No extra wrapper div required.
  2. The .ember-basic-dropdown-trigger element wraps the user-provided html. Not anymore.
    The user can now customize that component passing things like tagName, class and all the usual jazz.
    If the users wants to provide it's own triggerComponent there is nothing preventing them to do so. They
    could even opt out to using the trigger component at all:
{{#basic-dropdown as |dropdown|}}
  <button type="button" onclick={{dropdown.actions.toggle}}>Click me</button>
  {{#dropdown.content}}This is the content{{/dropdown.content}}
{/basic-dropdown}}

This might not be recommended because there is many things that this component does for the user for
free (accesibility, aria-* attributes, keyboard handling, ect...) but at the end is good to just let
the users choose their own path.

  1. The user can't introduce content before/after/between the trigger and the content parts. Also solved :)

Drawbacks

The obvious drawbacks are:

  1. Totally breaking change. There is no upgrade path other than other than manually update all usages
    of the component. The transformation will be relatively easy because no functionality will be removed,
    and I want to maintain the same names for the options, but the transformation will require some unavoidable work.
  2. It will require contextual component, which effectively means 2.3+

Extra

Although this component is not nearly as popular as other components built on top of it (Ember Power Select mainly), there is a substantial amount of people using it, so I believe that after this refactor, I will deserve a domain and a web page with documentation, live examples and cookbooks showing how to take full advantage of the available options to build other component on top of it.

How to show "run-loop" error caused by this library (when used by power-select)

The run-loop issue we talked about on power-select a week ago is still present in the latest beta 4 because of the way "repositionDropdown" is invoked without a run loop (tests show this in firefox and chrome in our app).

https://github.com/cibernox/ember-basic-dropdown/blob/master/addon/components/basic-dropdown.js#L167

The line above isn't under a "run" right now, but when I add a ember.run here all our tests suddenly pass (making me think we should protect the set that occurs here)

https://github.com/cibernox/ember-basic-dropdown/blob/master/addon/components/basic-dropdown.js#L136

I'm opening this in part to help us fix the run-loop issue we found, but also to ask "how can we /where should we" test it today as the bug only occurs because of an if statement we invalidate (causing a re-render in HTMLBars)

https://github.com/cibernox/ember-power-select/blob/master/addon/templates/components/power-select/single.hbs#L26

What happens in our search action is - we debounce like so

Ember.run.debounce(this, myFunction, search, 300);

This in turn changes a url query param that updates the underlying options and when those underlying options go from zero (empty array proxy in my case) to something else that if statement is evaluated and fired to render the options (and that render fires the repositionDropdown you see above causing run-loop issues in test code).

I'm planning to submit a PR here (to fix the root cause) if that's acceptable - any other info that may help me as I diagnose this ?

here is the stack showing that our HTMLBars if (in power-select) changes causing a render to run -thus invoking the reposition work

screen shot 2015-11-24 at 1 05 38 pm

Feature request: Add option to stop propagation of dropdown close click

Thanks for the library, this is such a nice component to build smart apps, awesome!

Your code handles closing the dropdown when clicking outside the dropdown. In my usage scenario I have clickable routes behind the dropdown. Once you click next to the dropdown, the dropdown will close as expected, but also the click event on the row is triggered.

It would be really nice to be able to stop propagation of the click event outside the popup. Maybe we can reuse the bubbles=false which is possible on link-to and action elements?

A helper named 'basic-dropdown' could not be found

Using ember install ember-basic-dropdown will add the addon as devDependencies. Build works with no issues in my addon (incl. test/dummy app). Adding my addon to a consuming app and using my component which uses ember-basic-dropdown gives me A helper named 'basic-dropdown' could not be found

Moving ember-basic-dropdown to dependencies and the helper is resolvable but then inside my addon the build breaks with

Merge error: file ember-basic-dropdown.scss exists in /home/mike/projects/remerge/ember-remerge/tmp/broccoli_merge_trees-input_base_path-0wiZhWI4.tmp/1 and /home/mike/projects/remerge/ember-remerge/tmp/broccoli_merge_trees-input_base_path-0wiZhWI4.tmp/30
Pass option { overwrite: true } to mergeTrees in order to have the latter file win.

Problem with ember-cli-compass-compiler and ember-basic-dropdown

If I'm starting a new ember-app with both I get an interesting error.

Serving on http://localhost:4200/
directory tmp/compass_compiler-output_path-sDmbDsmx.tmp/assets/
   create tmp/compass_compiler-output_path-sDmbDsmx.tmp/assets/app.css
    error tmp/compass_compiler-input_base_path-5niakTBe.tmp/0/app/styles/ember-basic-dropdown.scss (Line 16: Invalid CSS
 after "  &-": expected number or function, was "-left { left: 0; }")
   create tmp/compass_compiler-output_path-sDmbDsmx.tmp/assets/ember-basic-dropdown.css

The Broccoli Plugin: [CompassCompiler] failed with:
Error: Command failed: C:\Windows\system32\cmd.exe /s /c "compass compile --output-style=compressed --sass-dir=E:/GitHub
/dropdown-test/tmp/compass_compiler-input_base_path-5niakTBe.tmp/0/app/styles --css-dir="E:\GitHub\dropdown-test\tmp\com
pass_compiler-output_path-sDmbDsmx.tmp\assets""

    at ChildProcess.exithandler (child_process.js:213:12)
    at emitTwo (events.js:87:13)
    at ChildProcess.emit (events.js:172:7)
    at maybeClose (internal/child_process.js:827:16)
    at Socket.<anonymous> (internal/child_process.js:319:11)
    at emitOne (events.js:77:13)
    at Socket.emit (events.js:169:7)
    at Pipe._onclose (net.js:486:12)

The broccoli plugin was instantiated at:
    at CompassCompiler.Plugin (E:\GitHub\dropdown-test\node_modules\ember-cli-compass-compiler\node_modules\broccoli-com
pass-compiler\node_modules\broccoli-caching-writer\node_modules\broccoli-plugin\index.js:10:31)
    at CompassCompiler.CachingWriter [as constructor] (E:\GitHub\dropdown-test\node_modules\ember-cli-compass-compiler\n
ode_modules\broccoli-compass-compiler\node_modules\broccoli-caching-writer\index.js:21:10)
    at new CompassCompiler (E:\GitHub\dropdown-test\node_modules\ember-cli-compass-compiler\node_modules\broccoli-compas
s-compiler\lib\compiler.js:17:17)
    at CompassCompiler (E:\GitHub\dropdown-test\node_modules\ember-cli-compass-compiler\node_modules\broccoli-compass-co
mpiler\lib\compiler.js:10:52)
    at E:\GitHub\dropdown-test\node_modules\ember-cli-compass-compiler\index.js:67:23
    at Array.map (native)
    at CompassCompilerPlugin.toTree (E:\GitHub\dropdown-test\node_modules\ember-cli-compass-compiler\index.js:38:40)
    at E:\GitHub\dropdown-test\node_modules\ember-cli\node_modules\ember-cli-preprocess-registry\preprocessors.js:184:26

    at Array.forEach (native)
    at processPlugins (E:\GitHub\dropdown-test\node_modules\ember-cli\node_modules\ember-cli-preprocess-registry\preproc
essors.js:182:11)

Interestingly this isn't a problem with older versions of "ember-cli-compass-compiler".
To reproduce I set up a simple github repo (https://github.com/Haidy777/dropdown-test).

The master shows the problem with ember-power-select and the basic-dropdown-branch shows the error with ember-basic-dropdown.
Since it seems like something with ember-basic-dropdown I thought I open the issue here and not at ember-power-select.

Allow nesting dropdowns

I love this addon. One feature that appears to not work properly, however, is nesting
dropdowns. A simple example of where this is useful is for a submenu in a nav. Is there a way to do this already that I'm missing?

I was thinking about taking a crack at this by instead of rendering to a single static wormhole div, creating a new div under the body tag when clicking the trigger, and pointing wormhole to that div. That way, all drop-downs don't share the same wormhole destination. Does this approach seem practical?

Thanks!

[a11y] Refine aria implementation

These items will address some a11y issues outlined in ember-power-select issue #806 that are specifically related to the basic-dropdown component.

  • aria-owns/aria-controls must reference an element that exists in the dom.

    Currently the trigger's aria-controls attribute references an ID of a dom node that does not exist when the power-select is in the collapsed state. This is considered invalid markup and can break screen readers. The controlled element (the listbox) should be maintained in the dom even when collapsed, it can be an empty div/ul. - https://www.w3.org/TR/wai-aria/states_and_properties#aria-owns

`verticalPosition` does not work with `renderInPlace`

{{! has class ".ember-basic-dropdown-content--above" }}
{{#basic-dropdown verticalPosition="above" renderInPlace=false}}

{{! does not have any vertical position class }}
{{#basic-dropdown verticalPosition="above" renderInPlace=true}}

removeEventListener exception on IE 11

Get this error when navigating away from a route with a dropdown in IE 11 when i have the developer tools open and i run a production build. Can't be sure it happens when the tools are closed and it doesn't happen when using a development build.

Dropdown declaration.

{{#power-select-multiple renderInPlace=true options=assets selected=filteredAssets searchField="name" placeholder=(t 'track.trackEventsChooseAssetsToShow') onchange=(action "assetFilterChanged") as |asset|}}
              {{icon-asset asset=asset class="text-bottom" width="20px" height="20px"}}
              {{asset.name}}
              {{#if asset.registration}}<div class="description">({{asset.registration}})</div>{{/if}}
{{/power-select-multiple}}

Stack Trace

"TypeError: Unable to get property 'removeEventListener' of undefined or null reference
   at removeGlobalEvents (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:34:23452)
   at _teardown (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:34:24415)
   at willDestroyElement (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:34:20486)
   at r (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:11:30486)
   at trigger (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:16:814)
   at r (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:11:30486)
   at d.prototype.willDestroyElement (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:9:10395)
   at d.prototype.remove (https://myapp/assets/vendor-d42d5df2bd86ce26959f3ab73da5b5bd.js:"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.