ui: Exposes the <ToggleButton /> 'click' action (#7479)

This exposes an api for <ToggleButton /> so you can call it from
elsewhere, specifically here we use the api.click to close the dropdown
menus which is required is the DOM containing the toggle button isn't
redrawn as is the case with external links in a dropdown menu
This commit is contained in:
John Cowen 2020-03-24 10:55:45 +00:00 committed by John Cowen
parent 0a47a95588
commit f0062f5cbe
5 changed files with 103 additions and 31 deletions

View File

@ -94,16 +94,16 @@
<BlockSlot @name="trigger">
Help
</BlockSlot>
<BlockSlot @name="menu">
<BlockSlot @name="menu" as |id send keypressClick change|>
<li role="none" class="docs-link">
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_DOCS_URL'}} rel="noopener noreferrer" target="_blank">Documentation</a>
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_DOCS_URL'}} rel="noopener noreferrer" target="_blank" onclick={{change}}>Documentation</a>
</li>
<li role="none" class="learn-link">
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_DOCS_LEARN_URL'}} rel="noopener noreferrer" target="_blank">HashiCorp Learn</a>
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_DOCS_LEARN_URL'}} rel="noopener noreferrer" target="_blank" onclick={{change}}>HashiCorp Learn</a>
</li>
<li role="separator"></li>
<li role="none" class="feedback-link">
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_REPO_ISSUES_URL'}} target="_blank" rel="noopener noreferrer">Provide Feedback</a>
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_REPO_ISSUES_URL'}} target="_blank" rel="noopener noreferrer" onclick={{change}}>Provide Feedback</a>
</li>
</BlockSlot>
</PopoverMenu>

View File

@ -1,7 +1,11 @@
{{yield (concat 'popover-menu-' guid)}}
<AriaMenu @keyboardAccess={{keyboardAccess}} as |change keypress ariaLabelledBy ariaControls ariaExpanded keypressClick|>
<ToggleButton @checked={{ariaExpanded}} @onchange={{queue change (action "change")}} as |click|>
<button type="button" aria-haspopup="menu" onkeydown={{keypress}} onclick={{click}} id={{ariaLabelledBy}} aria-controls={{ariaControls}}>
<ToggleButton
@checked={{if keyboardAccess ariaExpanded expanded}}
@onchange={{queue change (action "change")}}
as |api|>
<Ref @target={{this}} @name="toggle" @value={{api}} />
<button type="button" aria-haspopup="menu" onkeydown={{keypress}} onclick={{this.toggle.click}} id={{ariaLabelledBy}} aria-controls={{ariaControls}}>
<YieldSlot @name="trigger">
{{yield}}
</YieldSlot>
@ -19,7 +23,7 @@
{{else}}
{{/yield-slot}}
<ul role="menu" id={{ariaControls}} aria-labelledby={{ariaLabelledBy}} aria-expanded={{ariaExpanded}}>
<YieldSlot @name="menu" @params={{block-params (concat "popover-menu-" guid "-") send keypressClick}}>
<YieldSlot @name="menu" @params={{block-params (concat "popover-menu-" guid "-") send keypressClick this.toggle.click}}>
{{yield}}
</YieldSlot>
</ul>

View File

@ -0,0 +1,54 @@
## ToggleButton
`<ToggleButton checked="checked" @onchange={{action 'change'}} as |api|>Toggle</ToggleButton>`
`<ToggleButton />` is a straightforward combination of a `<label>` and `<input type="checkbox" />` to allow you to easily setup CSS based (`input:checked ~ *`) visual toggling. The body of the component ends up inside the `<label>`.
Additionally, a `clickoutside` is currently included, so if the toggle is in an 'on' state, clicking outside the `<ToggleButton>` itself will un-toggle the component. This could be changed in a future version for this to be configurable/preventable and/or use/rely on a modifer instead.
### Arguments
| Argument/Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `Boolean` | false | The default value of the toggle on/off (true/false) |
| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `target` that is a reference to the underlying standard DOM input element. |
### Methods/Actions/api
| Method/Action | Description |
| --- | --- | --- | --- |
| `click` | Fire a click event on the ToggleButton/input which reverse the state of the toggle. |
### Example
Here is an example of a simple CSS based dropdown menu. Note: The general sibling selector (`~`) should be used as the label/button itself is the adjacent sibling (`+`).
```handlebars
<div class="menu">
<ToggleButton>
Open Menu
</ToggleButton>
<ul>
<li><a href="">Link 1</a></li>
<li><a href="">Link 2</a></li>
</ul>
</div>
```
```css
.menu input ~ ul {
display: none;
}
.menu input:checked ~ ul {
display: block;
}
```
### See
- [Component Source Code](./index.js)
- [TemplateSource Code](./index.hbs)
---

View File

@ -1,4 +1,13 @@
<input {{ref this "input"}} type="checkbox" checked={{if checked 'checked' undefined}} id={{concat 'toggle-button-' guid}} onchange={{action 'change'}} />
<input
...attributes
{{ref this "input"}}
type="checkbox"
checked={{if checked 'checked' undefined}}
id={{concat 'toggle-button-' guid}}
onchange={{action 'change'}}
/>
<label {{ref this "label"}} for={{concat 'toggle-button-' guid}}>
{{yield (action 'click')}}
{{yield (hash
click=(action 'click')
)}}
</label>

View File

@ -3,16 +3,11 @@ import { inject as service } from '@ember/service';
export default Component.extend({
dom: service('dom'),
// TODO(octane): Remove when we can move to glimmer components
// so we aren't using ember-test-selectors
// supportsDataTestProperties: true,
// the above doesn't seem to do anything so still need to find a way
// to pass through data-test-properties
tagName: '',
// TODO: reserved for the moment but we don't need it yet
onblur: null,
checked: false,
onchange: function() {},
// TODO: reserved for the moment but we don't need it yet
onblur: function() {},
init: function() {
this._super(...arguments);
this.guid = this.dom.guid(this);
@ -22,9 +17,32 @@ export default Component.extend({
this._super(...arguments);
this._listeners.remove();
},
didReceiveAttrs: function() {
this._super(...arguments);
if (this.checked) {
this.addClickOutsideListener();
} else {
this._listeners.remove();
}
},
addClickOutsideListener: function() {
// default onblur event
this._listeners.remove();
this._listeners.add(this.dom.document(), 'click', e => {
if (this.dom.isOutside(this.label, e.target)) {
if (this.dom.isOutside(this.label.nextElementSibling, e.target)) {
if (this.input.checked) {
this.input.checked = false;
// TODO: This should be an event
this.onchange({ target: this.input });
}
this._listeners.remove();
}
}
});
},
actions: {
click: function(e) {
e.preventDefault();
this.input.checked = !this.input.checked;
// manually dispatched mouse events have a detail = 0
// real mouse events have the number of click counts
@ -35,20 +53,7 @@ export default Component.extend({
},
change: function(e) {
if (this.input.checked) {
// default onblur event
this._listeners.remove();
this._listeners.add(this.dom.document(), 'click', e => {
if (this.dom.isOutside(this.label, e.target)) {
if (this.dom.isOutside(this.label.nextElementSibling, e.target)) {
if (this.input.checked) {
this.input.checked = false;
// TODO: This should be an event
this.onchange({ target: this.input });
}
this._listeners.remove();
}
}
});
this.addClickOutsideListener();
}
// TODO: This should be an event
this.onchange({ target: this.input });