feat(button): split off separate buttons for reset & submit
Co-authored-by: Gerjan van Geest <Gerjan.van.Geest@ing.com>
This commit is contained in:
parent
2b73c50f6e
commit
57b2fb9ff8
23 changed files with 1115 additions and 838 deletions
14
.changeset/young-kangaroos-juggle.md
Normal file
14
.changeset/young-kangaroos-juggle.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
'@lion/button': minor
|
||||
'@lion/form-integrations': minor
|
||||
'@lion/select-rich': minor
|
||||
---
|
||||
|
||||
- BREAKING: In `lion-button` package split of separate buttons for reset & submit:
|
||||
|
||||
- LionButton (a clean fundament, **use outside forms**)
|
||||
- LionButtonReset (logic for. submit and reset events, but without implicit form submission logic: **use for reset buttons**)
|
||||
- LionButtonSubmit (full featured button: **use for submit buttons and buttons with dynamic types**)
|
||||
|
||||
- fixed axe criterium for LionButton (which contained a native button to support form submission)
|
||||
- removed `_beforeTemplate()` & `_afterTemplate()` from LionButton
|
||||
|
|
@ -47,4 +47,5 @@ npm i --save @lion/combobox
|
|||
|
||||
```js
|
||||
import '@lion/combobox/define';
|
||||
import '@lion/listbox/lion-option.js';
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import '@lion/form/define';
|
|||
|
||||
## Submit & Reset
|
||||
|
||||
To submit a form, use a regular button (or `LionButton`) with `type="submit"` (which is default) somewhere inside the native `<form>`.
|
||||
To submit a form, use a regular button (or `LionButtonSubmit`) somewhere inside the native `<form>`.
|
||||
|
||||
Then, add a `submit` handler on the `lion-form`.
|
||||
|
||||
|
|
|
|||
|
|
@ -38,12 +38,12 @@ export const minimumClickArea = () => html` <style>
|
|||
|
||||
## Usage with native form
|
||||
|
||||
Supports the following use cases:
|
||||
`<lion-button-reset>` and `<lion-button-submit>` are especially created to supports the following use cases:
|
||||
|
||||
- Submit on button click
|
||||
- Reset native form fields when using type="reset"
|
||||
- Submit on button enter or space keypress
|
||||
- Submit on enter keypress inside an input
|
||||
- Reset native form fields when using type="reset"
|
||||
|
||||
```js preview-story
|
||||
export const withinForm = () => html`
|
||||
|
|
@ -57,16 +57,21 @@ export const withinForm = () => html`
|
|||
<input id="firstNameId" name="firstName" />
|
||||
<label for="lastNameId">Last name</label>
|
||||
<input id="lastNameId" name="lastName" />
|
||||
<lion-button @click=${ev => console.log('click handler', ev.target)}>Submit</lion-button>
|
||||
<lion-button-submit @click=${ev => console.log('click submit handler', ev.target)}
|
||||
>Submit</lion-button-submit
|
||||
>
|
||||
<lion-button-reset @click=${ev => console.log('click reset handler', ev.target)}
|
||||
>Reset</lion-button-reset
|
||||
>
|
||||
</form>
|
||||
`;
|
||||
```
|
||||
|
||||
Important notes:
|
||||
|
||||
- A (lion)-button of type submit is mandatory for the last use case, if you have multiple inputs. This is native behavior.
|
||||
- `@click` on `<lion-button>` and `@submit` on `<form>` are triggered by these use cases. We strongly encourage you to listen to the submit handler if your goal is to do something on form-submit.
|
||||
- To prevent form submission full page reloads, add a **submit handler on the form** `@submit` with `event.preventDefault()`. Adding it on the `<lion-button>` is not enough.
|
||||
- A `<lion-button-submit>` is mandatory for the last use case, if you have multiple inputs. This is native behaviour.
|
||||
- `@click` on `<lion-button-submit>` and `@submit` on `<form>` are triggered by these use cases. We strongly encourage you to listen to the submit handler if your goal is to do something on form-submit.
|
||||
- To prevent form submission full page reloads, add a **submit handler on the form** `@submit` with `event.preventDefault()`. Adding it on the `<lion-button-submit>` is not enough.
|
||||
|
||||
## Considerations
|
||||
|
||||
|
|
@ -76,15 +81,15 @@ There are multiple reasons why we used a Web Component as opposed to a CSS compo
|
|||
|
||||
- **Target size**: The minimum target size is 40 pixels, which makes even the small buttons easy to activate. A container element was needed to make this size possible.
|
||||
- **Advanced styling**: There are advanced styling options regarding icons in buttons, where it is a lot more maintainable to handle icons in our button using slots. An example is that a sticky icon-only buttons may looks different from buttons which have both icons and text.
|
||||
- **Native form integration**: The lion button works with native `<form>` submission, and even implicit form submission on-enter. A lot of delegation logic had to be created for this to work.
|
||||
- **Native form integration**: The `<lion-button-submit>` works with native `<form>` submission, and even implicit form submission on-enter. A lot of delegation logic had to be created for this to work.
|
||||
|
||||
### Event target
|
||||
|
||||
We want to ensure that the event target returned to the user is `<lion-button>`, not `button`. Therefore, simply delegating the click to the native button immediately, is not desired. Instead, we catch the click event in the `<lion-button>`, and ensure delegation inside of there.
|
||||
We want to ensure that the event target returned to the user is `<lion-button>`, not `<button>`. Therefore, simply delegating the click to the native button immediately, is not desired. Instead, we catch the click event in the `<lion-button>`, and ensure delegation inside of there.
|
||||
|
||||
### Flashing a native button click as a direct child of form
|
||||
|
||||
By delegating the `click()` to the native button, it will bubble back up to `<lion-button>` which would cause duplicate actions. We have to simulate the full `.click()` however, otherwise form submission is not triggered. So this bubbling cannot be prevented.
|
||||
By delegating the `click()` to the native button, it will bubble back up to `<lion-button-reset>` and `<lion-button-submit>` which would cause duplicate actions. We have to simulate the full `.click()` however, otherwise form submission is not triggered. So this bubbling cannot be prevented.
|
||||
Therefore, on click, we flash a `<button>` to the form as a direct child and fire the click on that button. We then immediately remove that button. This is a fully synchronous process; users or developers will not notice this, it should not cause problems.
|
||||
|
||||
### Native button & implicit form submission
|
||||
|
|
@ -93,7 +98,7 @@ Flashing the button in the way we do solves almost all issues except for one.
|
|||
One of the specs of W3C is that when you have a form with multiple inputs,
|
||||
pressing enter while inside one of the inputs only triggers a form submit if that form has a button of type submit.
|
||||
|
||||
To get this particular implicit form submission to work, having a native button in our `<lion-button>` is a hard requirement.
|
||||
Therefore, not only do we flash a native button on the form to delegate `<lion-button>` trigger to `<button>`
|
||||
and thereby trigger form submission, we **also** add a native `button` inside the `<lion-button>`
|
||||
whose `type` property is synchronized with the type of the `<lion-button>`.
|
||||
To get this particular implicit form submission to work, having a native button in our `<lion-button-submit>` is a hard requirement.
|
||||
Therefore, not only do we flash a native button on the form to delegate `<lion-button-submit>` trigger to `<button>`
|
||||
and thereby trigger form submission, we **also** add a native `button` inside the `<lion-button-submit>`
|
||||
whose `type` property is synchronized with the type of the `<lion-button-submit>`.
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ import '@lion/button/define';
|
|||
```
|
||||
|
||||
```js preview-story
|
||||
export const main = () => html`<lion-button>Default</lion-button>`;
|
||||
export const main = () => html` <lion-button>Default</lion-button> `;
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Clickable area that is bigger than visual size
|
||||
- Works with native form / inputs
|
||||
- Has integration for implicit form submission similar to how native `<form>`, `<input>` and `<button>` work together.
|
||||
- A special `button-reset` and `button-submit` works with native form / inputs
|
||||
- `button-submit` has integration for implicit form submission similar to how native `<form>`, `<input>` and `<button>` work together.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ npm i --save @lion/button
|
|||
```
|
||||
|
||||
```js
|
||||
import { LionButton } from '@lion/button';
|
||||
import { LionButton, LionButtonReset, LionButtonSubmit } from '@lion/button';
|
||||
// or
|
||||
import '@lion/button/define';
|
||||
```
|
||||
|
|
|
|||
|
|
@ -141,12 +141,10 @@ export const main = () => {
|
|||
</lion-input-stepper>
|
||||
<lion-textarea name="comments" label="Comments"></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button raised>Submit</lion-button>
|
||||
<lion-button
|
||||
type="button"
|
||||
raised
|
||||
<lion-button-submit>Submit</lion-button-submit>
|
||||
<lion-button-reset
|
||||
@click=${ev => ev.currentTarget.parentElement.parentElement.parentElement.resetGroup()}
|
||||
>Reset</lion-button
|
||||
>Reset</lion-button-reset
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ Due to our custom inputs being Web Components, it is possible to put HTML conten
|
|||
```js preview-story
|
||||
export const ButtonsWithFields = () => html`
|
||||
<lion-input label="Prefix and suffix">
|
||||
<lion-button slot="prefix" type="button">prefix</lion-button>
|
||||
<lion-button slot="suffix" type="button">suffix</lion-button>
|
||||
<lion-button slot="prefix">prefix</lion-button>
|
||||
<lion-button slot="suffix">suffix</lion-button>
|
||||
</lion-input>
|
||||
`;
|
||||
```
|
||||
|
|
|
|||
|
|
@ -800,7 +800,7 @@ export const backendValidation = () => {
|
|||
<lion-checkbox-group name="simulateError">
|
||||
<lion-checkbox label="Check to simulate a backend error"></lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
<lion-button raised>Submit</lion-button>
|
||||
<lion-button-submit>Submit</lion-button-submit>
|
||||
</form>
|
||||
</lion-form>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"@web/dev-server-legacy": "^0.1.7",
|
||||
"@web/test-runner": "^0.13.4",
|
||||
"@web/test-runner-browserstack": "^0.4.2",
|
||||
"@web/test-runner-commands": "^0.4.5",
|
||||
"@web/test-runner-playwright": "^0.8.6",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bundlesize": "^1.0.0-beta.2",
|
||||
|
|
@ -107,7 +108,7 @@
|
|||
},
|
||||
{
|
||||
"path": "./bundlesize/dist/all/*.js",
|
||||
"maxSize": "45 kB"
|
||||
"maxSize": "46 kB"
|
||||
}
|
||||
],
|
||||
"prettier": {
|
||||
|
|
|
|||
3
packages/button/define.js
Normal file
3
packages/button/define.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import '@lion/button/define-button';
|
||||
import '@lion/button/define-button-reset';
|
||||
import '@lion/button/define-button-submit';
|
||||
|
|
@ -1 +1,3 @@
|
|||
export { LionButton } from './src/LionButton.js';
|
||||
export { LionButtonReset } from './src/LionButtonReset.js';
|
||||
export { LionButtonSubmit } from './src/LionButtonSubmit.js';
|
||||
|
|
|
|||
3
packages/button/lion-button-reset.js
Normal file
3
packages/button/lion-button-reset.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionButtonReset } from './src/LionButtonReset.js';
|
||||
|
||||
customElements.define('lion-button-reset', LionButtonReset);
|
||||
3
packages/button/lion-button-submit.js
Normal file
3
packages/button/lion-button-submit.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { LionButtonSubmit } from './src/LionButtonSubmit.js';
|
||||
|
||||
customElements.define('lion-button-submit', LionButtonSubmit);
|
||||
|
|
@ -49,7 +49,10 @@
|
|||
"customElementsManifest": "custom-elements.json",
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./define": "./lion-button.js",
|
||||
"./define-button": "./lion-button.js",
|
||||
"./define-button-reset": "./lion-button-reset.js",
|
||||
"./define-button-submit": "./lion-button-submit.js",
|
||||
"./define": "./define.js",
|
||||
"./docs/": "./docs/"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
import {
|
||||
browserDetection,
|
||||
css,
|
||||
DisabledWithTabIndexMixin,
|
||||
html,
|
||||
LitElement,
|
||||
SlotMixin,
|
||||
} from '@lion/core';
|
||||
import { browserDetection, css, DisabledWithTabIndexMixin, html, LitElement } from '@lion/core';
|
||||
import '@lion/core/differentKeyEventNamesShimIE';
|
||||
|
||||
const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter';
|
||||
|
|
@ -15,51 +8,34 @@ const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key ===
|
|||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
*/
|
||||
|
||||
export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) {
|
||||
/**
|
||||
* Use LionButton (or LionButtonReset|LionButtonSubmit) when there is a need to extend HTMLButtonElement.
|
||||
* It allows to create complex shadow DOM for buttons needing this. Think of:
|
||||
* - a material Design button that needs a JS controlled ripple
|
||||
* - a LionSelectRich invoker that needs a complex shadow DOM structure
|
||||
* (for styling/maintainability purposes)
|
||||
* - a specialized button (for instance a primary button or icon button in a Design System) that
|
||||
* needs a simple api: `<my-button>text</my-button>` is always better than
|
||||
* `<button class="my-button"><div class="my-button__container">text</div><button>`
|
||||
*
|
||||
* In other cases, whenever you can, still use native HTMLButtonElement (`<button>`).
|
||||
*
|
||||
* Note that LionButton is meant for buttons with type="button". It's cleaner and more
|
||||
* lightweight than LionButtonReset and LionButtonSubmit, which should only be considered when native
|
||||
* `<form>` support is needed:
|
||||
* - When type="reset|submit" should be supported, use LionButtonReset.
|
||||
* - When implicit form submission should be supported on top, use LionButtonSubmit.
|
||||
*/
|
||||
export class LionButton extends DisabledWithTabIndexMixin(LitElement) {
|
||||
static get properties() {
|
||||
return {
|
||||
role: {
|
||||
type: String,
|
||||
reflect: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
reflect: true,
|
||||
},
|
||||
active: { type: Boolean, reflect: true },
|
||||
type: { type: String, reflect: true },
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
${this._beforeTemplate()}
|
||||
<div class="button-content" id="${this._buttonId}"><slot></slot></div>
|
||||
${this._afterTemplate()}
|
||||
<slot name="_button"></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {TemplateResult} button template
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_beforeTemplate() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {TemplateResult} button template
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_afterTemplate() {
|
||||
return html``;
|
||||
return html` <div class="button-content" id="${this._buttonId}"><slot></slot></div> `;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
|
|
@ -74,7 +50,8 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
background: #eee; /* minimal styling to make it recognizable as btn */
|
||||
padding: 8px; /* padding to fix with min-height */
|
||||
outline: none; /* focus style handled below */
|
||||
cursor: default; /* /* we should always see the default arrow, never a caret */
|
||||
cursor: default; /* we should always see the default arrow, never a caret */
|
||||
/* TODO: remove, native button also allows selection. Could be usability concern... */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
|
@ -104,20 +81,6 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
:host ::slotted(button) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
padding: 0; /* reset default agent styles */
|
||||
border: 0; /* reset default agent styles */
|
||||
}
|
||||
|
||||
/* Show focus styles on keyboard focus. */
|
||||
:host(:focus:not([disabled])),
|
||||
:host(:focus-visible) {
|
||||
|
|
@ -157,35 +120,10 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {HTMLButtonElement}
|
||||
* @protected
|
||||
*/
|
||||
get _nativeButtonNode() {
|
||||
return /** @type {HTMLButtonElement} */ (
|
||||
Array.from(this.children).find(child => child.slot === '_button')
|
||||
);
|
||||
}
|
||||
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
_button: () => {
|
||||
/** @type {HTMLButtonElement} */
|
||||
const buttonEl = document.createElement('button');
|
||||
buttonEl.setAttribute('tabindex', '-1');
|
||||
buttonEl.setAttribute('aria-hidden', 'true');
|
||||
return buttonEl;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.role = 'button';
|
||||
this.type = 'submit';
|
||||
this.type = 'button';
|
||||
this.active = false;
|
||||
this.__setupDelegationInConstructor();
|
||||
|
||||
this._buttonId = `button-${Math.random().toString(36).substr(2, 10)}`;
|
||||
if (browserDetection.isIE11) {
|
||||
|
|
@ -195,28 +133,14 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @type {HTMLButtonElement} */
|
||||
this.__submitAndResetHelperButton = document.createElement('button');
|
||||
|
||||
/** @type {EventListener} */
|
||||
this.__preventEventLeakage = this.__preventEventLeakage.bind(this);
|
||||
this.__setupEvents();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.__setupEvents();
|
||||
// Old browsers (IE11, Old Edge, Firefox ESR 60) don't have the `.form`
|
||||
// property defined immediately on the native button, so do this after first render on connected.
|
||||
this.updateComplete.then(() => {
|
||||
this.__setupSubmitAndResetHelperOnConnected();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.__teardownEvents();
|
||||
this.__teardownSubmitAndResetHelperOnDisconnected();
|
||||
if (!this.hasAttribute('role')) {
|
||||
this.setAttribute('role', 'button');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -224,59 +148,12 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
*/
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has('type')) {
|
||||
const native = this._nativeButtonNode;
|
||||
if (native) {
|
||||
native.type = this.type;
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProperties.has('disabled')) {
|
||||
this.setAttribute('aria-disabled', `${this.disabled}`); // create mixin if we need it in more places
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate click, by flashing a native button as a direct child
|
||||
* of the form, and firing click on this button. This will fire the form submit
|
||||
* without side effects caused by the click bubbling back up to lion-button.
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
// TODO: rename to _clickDelegationHandler in v1
|
||||
async __clickDelegationHandler(ev) {
|
||||
// Wait for updateComplete if form is not yet available
|
||||
if (!this._form) {
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
if ((this.type === 'submit' || this.type === 'reset') && ev.target === this && this._form) {
|
||||
/**
|
||||
* Here, we make sure our button is compatible with a native form, by firing a click
|
||||
* from a native button that our form responds to. The native button we spawn will be a direct
|
||||
* child of the form, plus the click event that will be sent will be prevented from
|
||||
* propagating outside of the form. This will keep the amount of 'noise' (click events
|
||||
* from 'ghost elements' that can be intercepted by listeners in the bubble chain) to an
|
||||
* absolute minimum.
|
||||
*/
|
||||
this.__submitAndResetHelperButton.type = this.type;
|
||||
|
||||
this._form.appendChild(this.__submitAndResetHelperButton);
|
||||
// Form submission or reset will happen
|
||||
this.__submitAndResetHelperButton.click();
|
||||
this._form.removeChild(this.__submitAndResetHelperButton);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupDelegationInConstructor() {
|
||||
// do not move to connectedCallback, otherwise IE11 breaks.
|
||||
// more info: https://github.com/ing-bank/lion/issues/179#issuecomment-511763835
|
||||
this.addEventListener('click', this.__clickDelegationHandler, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
|
@ -286,16 +163,6 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
this.addEventListener('keyup', this.__keyupHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__teardownEvents() {
|
||||
this.removeEventListener('mousedown', this.__mousedownHandler);
|
||||
this.removeEventListener('keydown', this.__keydownHandler);
|
||||
this.removeEventListener('keyup', this.__keyupHandler);
|
||||
this.removeEventListener('click', this.__clickDelegationHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
|
@ -311,19 +178,19 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
* @param {KeyboardEvent} event
|
||||
* @private
|
||||
*/
|
||||
__keydownHandler(e) {
|
||||
if (this.active || !isKeyboardClickEvent(e)) {
|
||||
if (isSpaceKeyboardClickEvent(e)) {
|
||||
e.preventDefault();
|
||||
__keydownHandler(event) {
|
||||
if (this.active || !isKeyboardClickEvent(event)) {
|
||||
if (isSpaceKeyboardClickEvent(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSpaceKeyboardClickEvent(e)) {
|
||||
e.preventDefault();
|
||||
if (isSpaceKeyboardClickEvent(event)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this.active = true;
|
||||
|
|
@ -340,48 +207,17 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
* @param {KeyboardEvent} event
|
||||
* @private
|
||||
*/
|
||||
__keyupHandler(e) {
|
||||
if (isKeyboardClickEvent(e)) {
|
||||
__keyupHandler(event) {
|
||||
if (isKeyboardClickEvent(event)) {
|
||||
// Fixes IE11 double submit/click. Enter keypress somehow triggers the __keyUpHandler on the native <button>
|
||||
if (e.srcElement && e.srcElement !== this) {
|
||||
if (event.target && event.target !== this) {
|
||||
return;
|
||||
}
|
||||
// dispatch click
|
||||
this.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents that someone who listens outside or on form catches the click event
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
__preventEventLeakage(e) {
|
||||
if (e.target === this.__submitAndResetHelperButton) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupSubmitAndResetHelperOnConnected() {
|
||||
this._form = this._nativeButtonNode.form;
|
||||
|
||||
if (this._form) {
|
||||
this._form.addEventListener('click', this.__preventEventLeakage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__teardownSubmitAndResetHelperOnDisconnected() {
|
||||
if (this._form) {
|
||||
this._form.removeEventListener('click', this.__preventEventLeakage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
124
packages/button/src/LionButtonReset.js
Normal file
124
packages/button/src/LionButtonReset.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { LionButton } from './LionButton.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* This adds functionality for form buttons (type 'submit' and 'reset').
|
||||
* It allows to submit or reset a <form> by spawning a click on a temporrary native button inside
|
||||
* the form.
|
||||
* Use LionButtonSubmit when implicit form submission should be supported as well.
|
||||
*
|
||||
* Functionality in this button is not purely for type="reset", also for type="submit".
|
||||
* For mainainability purposes the submit functionality is part of LionButtonReset
|
||||
* (it needs the same logic).
|
||||
* LionButtonReset could therefore actually be considered as 'LionButtonForm' (without the
|
||||
* implicit form submission logic), but LionButtonReset is an easier to grasp name for
|
||||
* Application Developers: for reset buttons, always use LionButtonReset, for submit
|
||||
* buttons always use LionButtonSubmit.
|
||||
*/
|
||||
export class LionButtonReset extends LionButton {
|
||||
constructor() {
|
||||
super();
|
||||
this.type = 'reset';
|
||||
this.__setupDelegationInConstructor();
|
||||
|
||||
/** @type {HTMLButtonElement} */
|
||||
this.__submitAndResetHelperButton = document.createElement('button');
|
||||
|
||||
/** @type {EventListener} */
|
||||
this.__preventEventLeakage = this.__preventEventLeakage.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Old browsers (IE11, Old Edge, Firefox ESR 60) don't have the `.form`
|
||||
// property defined immediately on the native button, so do this after first render on connected.
|
||||
this.updateComplete.then(() => {
|
||||
this._setupSubmitAndResetHelperOnConnected();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._teardownSubmitAndResetHelperOnDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents that someone who listens outside or on form catches the click event
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
__preventEventLeakage(e) {
|
||||
if (e.target === this.__submitAndResetHelperButton) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_setupSubmitAndResetHelperOnConnected() {
|
||||
// Get form
|
||||
this.appendChild(this.__submitAndResetHelperButton);
|
||||
/** @type {HTMLFormElement|null} */
|
||||
this._form = this.__submitAndResetHelperButton.form;
|
||||
this.removeChild(this.__submitAndResetHelperButton);
|
||||
|
||||
if (this._form) {
|
||||
this._form.addEventListener('click', this.__preventEventLeakage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_teardownSubmitAndResetHelperOnDisconnected() {
|
||||
if (this._form) {
|
||||
this._form.removeEventListener('click', this.__preventEventLeakage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate click, by flashing a native button as a direct child
|
||||
* of the form, and firing click on this button. This will fire the form submit
|
||||
* without side effects caused by the click bubbling back up to lion-button.
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
// TODO: rename to _clickDelegationHandler in v1
|
||||
async __clickDelegationHandler(ev) {
|
||||
// Wait for updateComplete if form is not yet available
|
||||
if (!this._form) {
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
if ((this.type === 'submit' || this.type === 'reset') && ev.target === this && this._form) {
|
||||
/**
|
||||
* Here, we make sure our button is compatible with a native form, by firing a click
|
||||
* from a native button that our form responds to. The native button we spawn will be a direct
|
||||
* child of the form, plus the click event that will be sent will be prevented from
|
||||
* propagating outside of the form. This will keep the amount of 'noise' (click events
|
||||
* from 'ghost elements' that can be intercepted by listeners in the bubble chain) to an
|
||||
* absolute minimum.
|
||||
*/
|
||||
this.__submitAndResetHelperButton.type = this.type;
|
||||
|
||||
this._form.appendChild(this.__submitAndResetHelperButton);
|
||||
// Form submission or reset will happen
|
||||
this.__submitAndResetHelperButton.click();
|
||||
this._form.removeChild(this.__submitAndResetHelperButton);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupDelegationInConstructor() {
|
||||
// do not move to connectedCallback, otherwise IE11 breaks.
|
||||
// more info: https://github.com/ing-bank/lion/issues/179#issuecomment-511763835
|
||||
this.addEventListener('click', this.__clickDelegationHandler, true);
|
||||
}
|
||||
}
|
||||
109
packages/button/src/LionButtonSubmit.js
Normal file
109
packages/button/src/LionButtonSubmit.js
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { LionButtonReset } from './LionButtonReset.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
* @typedef {{lionButtons: Set<LionButtonSubmit>, helper:HTMLButtonElement, observer:MutationObserver}} HelperRegistration
|
||||
*/
|
||||
|
||||
/** @type {WeakMap<HTMLFormElement, HelperRegistration>} */
|
||||
const implicitHelperMap = new WeakMap();
|
||||
|
||||
function createImplicitSubmitHelperButton() {
|
||||
const buttonEl = document.createElement('button');
|
||||
buttonEl.tabIndex = -1;
|
||||
buttonEl.type = 'submit';
|
||||
buttonEl.setAttribute('aria-hidden', 'true');
|
||||
|
||||
// Make it sr-only
|
||||
buttonEl.style.cssText = `
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
padding: 0; /* reset default agent styles */
|
||||
border: 0; /* reset default agent styles */
|
||||
`;
|
||||
return buttonEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains all the funcionaility of LionButton and LionButtonReset. On top of that it
|
||||
* supports implicit form submission.
|
||||
*
|
||||
* Use when:
|
||||
* - the Application Developer should be able to switch types between 'submit'|'reset'|'button'
|
||||
* (this is similar to how native HTMLButtonElement works)
|
||||
* - a submit button with native form support is needed
|
||||
*/
|
||||
export class LionButtonSubmit extends LionButtonReset {
|
||||
/**
|
||||
* @type {HTMLButtonElement|null}
|
||||
* @protected
|
||||
*/
|
||||
get _nativeButtonNode() {
|
||||
return implicitHelperMap.get(/** @type {HTMLFormElement} */ (this._form))?.helper || null;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.type = 'submit';
|
||||
|
||||
/** @type {HTMLButtonElement|null} */
|
||||
this.__implicitSubmitHelperButton = null;
|
||||
}
|
||||
|
||||
_setupSubmitAndResetHelperOnConnected() {
|
||||
super._setupSubmitAndResetHelperOnConnected();
|
||||
|
||||
if (!this._form || this.type !== 'submit') {
|
||||
return;
|
||||
}
|
||||
/** @type {HTMLFormElement} */
|
||||
const form = this._form;
|
||||
const registrationForCurForm = implicitHelperMap.get(this._form);
|
||||
|
||||
if (!registrationForCurForm) {
|
||||
const buttonEl = createImplicitSubmitHelperButton();
|
||||
const wrapperEl = document.createElement('div');
|
||||
wrapperEl.appendChild(buttonEl);
|
||||
implicitHelperMap.set(this._form, {
|
||||
lionButtons: new Set(),
|
||||
helper: buttonEl,
|
||||
observer: new MutationObserver(() => {
|
||||
form.appendChild(wrapperEl);
|
||||
}),
|
||||
});
|
||||
form.appendChild(wrapperEl);
|
||||
|
||||
// Prevent that the just created button gets lost during rerender
|
||||
implicitHelperMap.get(form)?.observer.observe(wrapperEl, { childList: true });
|
||||
}
|
||||
implicitHelperMap.get(form)?.lionButtons.add(this);
|
||||
}
|
||||
|
||||
_teardownSubmitAndResetHelperOnDisconnected() {
|
||||
super._teardownSubmitAndResetHelperOnDisconnected();
|
||||
|
||||
if (this._form) {
|
||||
// If we are the last button to leave the form, clean up the
|
||||
const registrationForCurForm = /** @type {HelperRegistration} */ (
|
||||
implicitHelperMap.get(/** @type {HTMLFormElement} */ (this._form))
|
||||
);
|
||||
if (registrationForCurForm) {
|
||||
registrationForCurForm.lionButtons.delete(this);
|
||||
if (!registrationForCurForm.lionButtons.size) {
|
||||
if (this._form.contains(registrationForCurForm.helper)) {
|
||||
registrationForCurForm.helper.remove();
|
||||
}
|
||||
implicitHelperMap.get(this._form)?.observer.disconnect();
|
||||
implicitHelperMap.delete(this._form);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
378
packages/button/test-suites/LionButton.suite.js
Normal file
378
packages/button/test-suites/LionButton.suite.js
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
/* eslint-disable lit-a11y/click-events-have-key-events */
|
||||
import { browserDetection } from '@lion/core';
|
||||
import { aTimeout, expect, fixture, oneEvent, defineCE } from '@open-wc/testing';
|
||||
import { html, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { LionButton } from '@lion/button';
|
||||
import sinon from 'sinon';
|
||||
import '@lion/core/differentKeyEventNamesShimIE';
|
||||
|
||||
export function LionButtonSuite({ klass = LionButton } = {}) {
|
||||
const tagStringButton = defineCE(class extends klass {});
|
||||
const tagButton = unsafeStatic(tagStringButton);
|
||||
|
||||
describe('LionButton', () => {
|
||||
it('has .type="button" and type="button" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(html`<${tagButton}>foo</${tagButton}>`));
|
||||
expect(el.type).to.equal('button');
|
||||
expect(el.getAttribute('type')).to.be.equal('button');
|
||||
});
|
||||
|
||||
it('is hidden when attribute hidden is true', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} hidden>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el).not.to.be.displayed;
|
||||
});
|
||||
|
||||
it('can be disabled imperatively', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} disabled>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
expect(el.hasAttribute('disabled')).to.equal(false);
|
||||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
expect(el.hasAttribute('disabled')).to.equal(true);
|
||||
});
|
||||
|
||||
describe('Active', () => {
|
||||
it('updates "active" attribute on host when mousedown/mouseup on button', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(html`<${tagButton}>foo</${tagButton}`));
|
||||
el.dispatchEvent(new Event('mousedown'));
|
||||
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new Event('mouseup'));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when mousedown on button and mouseup anywhere else', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
|
||||
el.dispatchEvent(new Event('mousedown'));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
document.dispatchEvent(new Event('mouseup'));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when space keydown/keyup on button', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when space keydown on button and space keyup anywhere else', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when enter keydown/keyup on button', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when enter keydown on button and space keyup anywhere else', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
document.body.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('has a role="button" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('role')).to.equal('button');
|
||||
el.setAttribute('role', 'foo');
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('role')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('does not override user provided role', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} role="foo">foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('role')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('has a tabindex="0" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
});
|
||||
|
||||
it('has a tabindex="-1" when disabled', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} disabled>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
});
|
||||
|
||||
it('does not override user provided tabindex', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} tabindex="5">foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('5');
|
||||
});
|
||||
|
||||
it('disabled does not override user provided tabindex', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} tabindex="5" disabled>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('5');
|
||||
});
|
||||
|
||||
it('has an aria-labelledby and wrapper element in IE11', async () => {
|
||||
const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true);
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.hasAttribute('aria-labelledby')).to.be.true;
|
||||
const wrapperId = el.getAttribute('aria-labelledby');
|
||||
expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`#${wrapperId}`)).to.exist;
|
||||
expect(
|
||||
/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`#${wrapperId}`),
|
||||
).dom.to.equal(`<div class="button-content" id="${wrapperId}"><slot></slot></div>`);
|
||||
browserDetectionStub.restore();
|
||||
});
|
||||
|
||||
it('does not override aria-labelledby when provided by user', async () => {
|
||||
const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true);
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} aria-labelledby="some-id another-id">foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.getAttribute('aria-labelledby')).to.equal('some-id another-id');
|
||||
browserDetectionStub.restore();
|
||||
});
|
||||
|
||||
it('[axe] is accessible', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('[axe] is accessible when disabled', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} disabled>foo</${tagButton}>`)
|
||||
);
|
||||
await expect(el).to.be.accessible({ ignoredRules: ['color-contrast'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Click event', () => {
|
||||
/**
|
||||
* @param {HTMLButtonElement | LionButton} el
|
||||
*/
|
||||
async function prepareClickEvent(el) {
|
||||
setTimeout(() => {
|
||||
el.click();
|
||||
});
|
||||
return oneEvent(el, 'click');
|
||||
}
|
||||
|
||||
it('is fired once', async () => {
|
||||
const clickSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html` <${tagButton} @click="${clickSpy}">foo</${tagButton}> `)
|
||||
);
|
||||
|
||||
el.click();
|
||||
|
||||
// trying to wait for other possible redispatched events
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(clickSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('Native button behavior', async () => {
|
||||
/** @type {Event} */
|
||||
let nativeButtonEvent;
|
||||
/** @type {Event} */
|
||||
let lionButtonEvent;
|
||||
|
||||
before(async () => {
|
||||
const nativeButtonEl = /** @type {LionButton} */ (await fixture('<button>foo</button>'));
|
||||
const lionButtonEl = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
nativeButtonEvent = await prepareClickEvent(nativeButtonEl);
|
||||
lionButtonEvent = await prepareClickEvent(lionButtonEl);
|
||||
});
|
||||
|
||||
const sameProperties = [
|
||||
'constructor',
|
||||
'composed',
|
||||
'bubbles',
|
||||
'cancelable',
|
||||
'clientX',
|
||||
'clientY',
|
||||
];
|
||||
|
||||
sameProperties.forEach(property => {
|
||||
it(`has same value of the property "${property}" as in native button event`, () => {
|
||||
expect(lionButtonEvent[property]).to.equal(nativeButtonEvent[property]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event target', async () => {
|
||||
it('is host by default', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
const event = await prepareClickEvent(el);
|
||||
expect(event.target).to.equal(el);
|
||||
});
|
||||
|
||||
const useCases = [
|
||||
{ container: 'div', type: 'submit' },
|
||||
{ container: 'div', type: 'reset' },
|
||||
{ container: 'div', type: 'button' },
|
||||
{ container: 'form', type: 'submit' },
|
||||
{ container: 'form', type: 'reset' },
|
||||
{ container: 'form', type: 'button' },
|
||||
];
|
||||
|
||||
useCases.forEach(useCase => {
|
||||
const { container, type } = useCase;
|
||||
const targetName = 'host';
|
||||
it(`is ${targetName} with type ${type} and it is inside a ${container}`, async () => {
|
||||
const clickSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html`<${tagButton} type="${type}">foo</${tagButton}>`)
|
||||
);
|
||||
const tag = unsafeStatic(container);
|
||||
await fixture(html`<${tag} @click="${clickSpy}">${el}</${tag}>`);
|
||||
const event = await prepareClickEvent(el);
|
||||
|
||||
expect(event.target).to.equal(el);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With click event', () => {
|
||||
it('behaves like native `button` when clicked', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<${tagButton} @click="${formButtonClickedSpy}" type="submit">foo</${tagButton}>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const button = /** @type {LionButton} */ (form.querySelector(tagStringButton));
|
||||
button.click();
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard space', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<${tagButton} @click="${formButtonClickedSpy}" type="submit">foo</${tagButton}>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const lionButton = /** @type {LionButton} */ (form.querySelector(tagStringButton));
|
||||
lionButton.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard enter', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<${tagButton} @click="${formButtonClickedSpy}" type="submit">foo</${tagButton}>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const button = /** @type {LionButton} */ (form.querySelector(tagStringButton));
|
||||
button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
197
packages/button/test-suites/LionButtonReset.suite.js
Normal file
197
packages/button/test-suites/LionButtonReset.suite.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/* eslint-disable lit-a11y/click-events-have-key-events */
|
||||
import { aTimeout, expect, fixture, defineCE } from '@open-wc/testing';
|
||||
import { html, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { LionButtonReset } from '@lion/button';
|
||||
import sinon from 'sinon';
|
||||
import '@lion/core/differentKeyEventNamesShimIE';
|
||||
|
||||
export function LionButtonResetSuite({ klass = LionButtonReset } = {}) {
|
||||
const tagStringButtonReset = defineCE(class extends klass {});
|
||||
const tagButtonReset = unsafeStatic(tagStringButtonReset);
|
||||
|
||||
describe('LionButtonReset', () => {
|
||||
it('has .type="reset" and type="reset" by default', async () => {
|
||||
const el = /** @type {LionButtonReset} */ (
|
||||
await fixture(html`<${tagButtonReset}>foo</${tagButtonReset}>`)
|
||||
);
|
||||
expect(el.type).to.equal('reset');
|
||||
expect(el.getAttribute('type')).to.be.equal('reset');
|
||||
});
|
||||
|
||||
/**
|
||||
* Notice functionality below is not purely for type="reset", also for type="submit".
|
||||
* For mainainability purposes the submit functionality is part of LionButtonReset.
|
||||
* (it needs the same logic)
|
||||
* LionButtonReset could therefore actually be considered as 'LionButtonForm' (without the
|
||||
* implicit form submission logic), but LionButtonReset is an easier to grasp name for
|
||||
* Application Developers: for reset buttons, always use LionButtonReset, for submit
|
||||
* buttons always use LionButton.
|
||||
* For buttons that should support all three types (like native <button>); use LionButton.
|
||||
*/
|
||||
describe('Form integration', () => {
|
||||
describe('With submit event', () => {
|
||||
it('behaves like native `button` when clicked', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<${tagButtonReset} type="submit">foo</${tagButtonReset}>
|
||||
</form>
|
||||
`);
|
||||
const button /** @type {LionButtonReset} */ = /** @type {LionButtonReset} */ (
|
||||
form.querySelector(tagStringButtonReset)
|
||||
);
|
||||
button.click();
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard space', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<${tagButtonReset} type="submit">foo</${tagButtonReset}>
|
||||
</form>
|
||||
`);
|
||||
const button /** @type {LionButtonReset} */ = /** @type {LionButtonReset} */ (
|
||||
form.querySelector(tagStringButtonReset)
|
||||
);
|
||||
button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard enter', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<${tagButtonReset} type="submit">foo</${tagButtonReset}>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const button = /** @type {LionButtonReset} */ (form.querySelector(tagStringButtonReset));
|
||||
button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('supports resetting form inputs in a native form', async () => {
|
||||
const form = await fixture(html`
|
||||
<form>
|
||||
<input name="firstName" />
|
||||
<input name="lastName" />
|
||||
<${tagButtonReset} type="reset">reset</${tagButtonReset}>
|
||||
</form>
|
||||
`);
|
||||
const btn /** @type {LionButtonReset} */ = /** @type {LionButtonReset} */ (
|
||||
form.querySelector(tagStringButtonReset)
|
||||
);
|
||||
const firstName = /** @type {HTMLInputElement} */ (
|
||||
form.querySelector('input[name=firstName]')
|
||||
);
|
||||
const lastName = /** @type {HTMLInputElement} */ (
|
||||
form.querySelector('input[name=lastName]')
|
||||
);
|
||||
firstName.value = 'Foo';
|
||||
lastName.value = 'Bar';
|
||||
|
||||
expect(firstName.value).to.equal('Foo');
|
||||
expect(lastName.value).to.equal('Bar');
|
||||
|
||||
btn.click();
|
||||
|
||||
expect(firstName.value).to.be.empty;
|
||||
expect(lastName.value).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('is fired once outside and inside the form', async () => {
|
||||
const outsideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const insideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
|
||||
const el = /** @type {HTMLDivElement} */ (
|
||||
await fixture(
|
||||
html`
|
||||
<div @click="${outsideSpy}">
|
||||
<form @click="${formSpyEarly}">
|
||||
<div @click="${insideSpy}">
|
||||
<${tagButtonReset}>foo</${tagButtonReset}>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
);
|
||||
const lionButton = /** @type {LionButtonReset} */ (el.querySelector(tagStringButtonReset));
|
||||
const form = /** @type {HTMLFormElement} */ (el.querySelector('form'));
|
||||
form.addEventListener('click', formSpyLater);
|
||||
|
||||
lionButton.click();
|
||||
// trying to wait for other possible redispatched events
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(insideSpy).to.have.been.calledOnce;
|
||||
expect(outsideSpy).to.have.been.calledOnce;
|
||||
// A small sacrifice for event listeners registered early: we get the native button evt.
|
||||
expect(formSpyEarly).to.have.been.calledTwice;
|
||||
expect(formSpyLater).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('works when connected to different form', async () => {
|
||||
const form1El = /** @type {HTMLFormElement} */ (
|
||||
await fixture(
|
||||
html`
|
||||
<form>
|
||||
<${tagButtonReset}>foo</${tagButtonReset}>
|
||||
</form>
|
||||
`,
|
||||
)
|
||||
);
|
||||
const lionButton = /** @type {LionButtonReset} */ (
|
||||
form1El.querySelector(tagStringButtonReset)
|
||||
);
|
||||
|
||||
expect(lionButton._form).to.equal(form1El);
|
||||
|
||||
// Now we add the lionButton to a different form.
|
||||
// We disconnect and connect and check if everything still works as expected
|
||||
const outsideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const insideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
|
||||
const form2El = /** @type {HTMLFormElement} */ (
|
||||
await fixture(
|
||||
html`
|
||||
<div @click="${outsideSpy}">
|
||||
<form @click="${formSpyEarly}">
|
||||
<div @click="${insideSpy}">${lionButton}</div>
|
||||
</form>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
);
|
||||
const form2Node = /** @type {HTMLFormElement} */ (form2El.querySelector('form'));
|
||||
|
||||
expect(lionButton._form).to.equal(form2Node);
|
||||
|
||||
form2Node.addEventListener('click', formSpyLater);
|
||||
lionButton.click();
|
||||
// trying to wait for other possible redispatched events
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(insideSpy).to.have.been.calledOnce;
|
||||
expect(outsideSpy).to.have.been.calledOnce;
|
||||
// A small sacrifice for event listeners registered early: we get the native button evt.
|
||||
expect(formSpyEarly).to.have.been.calledTwice;
|
||||
expect(formSpyLater).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
}
|
||||
189
packages/button/test-suites/LionButtonSubmit.suite.js
Normal file
189
packages/button/test-suites/LionButtonSubmit.suite.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
/* eslint-disable lit-a11y/click-events-have-key-events */
|
||||
import { aTimeout, expect, fixture, defineCE } from '@open-wc/testing';
|
||||
import { html, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { LionButtonSubmit } from '@lion/button';
|
||||
import sinon from 'sinon';
|
||||
import '@lion/core/differentKeyEventNamesShimIE';
|
||||
|
||||
export function LionButtonSubmitSuite({ klass = LionButtonSubmit } = {}) {
|
||||
const tagStringButton = defineCE(class extends klass {});
|
||||
const tagButton = unsafeStatic(tagStringButton);
|
||||
|
||||
describe('LionButtonSubmit', () => {
|
||||
it('has .type="submit" and type="submit" by default', async () => {
|
||||
const el = /** @type {LionButtonSubmit} */ (
|
||||
await fixture(html`<${tagButton}>foo</${tagButton}>`)
|
||||
);
|
||||
expect(el.type).to.equal('submit');
|
||||
expect(el.getAttribute('type')).to.be.equal('submit');
|
||||
});
|
||||
|
||||
describe('Implicit form submission', () => {
|
||||
describe('Helper submit button', () => {
|
||||
it('creates a helper submit button when type is "submit"', async () => {
|
||||
let lionBtnEl;
|
||||
const elTypeSubmit = /** @type {HTMLFormElement} */ (
|
||||
await fixture(html`<form><${tagButton} type="submit">foo</${tagButton}></form>`)
|
||||
);
|
||||
lionBtnEl = /** @type {LionButtonSubmit} */ (elTypeSubmit.querySelector('[type=submit]'));
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl._nativeButtonNode instanceof HTMLButtonElement).to.be.true;
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl._nativeButtonNode.type).to.equal('submit');
|
||||
|
||||
const elTypeReset = /** @type {LionButtonSubmit} */ (
|
||||
await fixture(html`<form><${tagButton} type="reset">foo</${tagButton}></form>`)
|
||||
);
|
||||
lionBtnEl = /** @type {LionButtonSubmit} */ (elTypeReset.querySelector('[type=reset]'));
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl._nativeButtonNode).to.be.null;
|
||||
|
||||
const elTypeButton = /** @type {LionButtonSubmit} */ (
|
||||
await fixture(html`<form><${tagButton} type="button">foo</${tagButton}></form>`)
|
||||
);
|
||||
|
||||
lionBtnEl = /** @type {LionButtonSubmit} */ (elTypeButton.querySelector('[type=button]'));
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl._nativeButtonNode).to.be.null;
|
||||
});
|
||||
|
||||
it('only creates a helper submit button when LionButtonSubmit is inside a form', async () => {
|
||||
const elForm = /** @type {HTMLFormElement} */ (await fixture(html`<form></form>`));
|
||||
const el = /** @type {LionButtonSubmit} */ (
|
||||
await fixture(html`<${tagButton} type="submit">foo</${tagButton}>`)
|
||||
);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(el._nativeButtonNode).to.be.null;
|
||||
|
||||
elForm.appendChild(el);
|
||||
await el.updateComplete;
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(el._nativeButtonNode).to.be.not.null;
|
||||
|
||||
elForm.removeChild(el);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(el._nativeButtonNode).to.be.null;
|
||||
});
|
||||
|
||||
it('puts helper submit button at the bottom of a form', async () => {
|
||||
const elForm = /** @type {HTMLFormElement} */ (
|
||||
await fixture(
|
||||
html`<form><input /><${tagButton} type="submit">foo</${tagButton}><input /></form>`,
|
||||
)
|
||||
);
|
||||
const lionBtnEl = /** @type {LionButtonSubmit} */ (elForm.querySelector('[type=submit]'));
|
||||
expect(elForm.children.length).to.equal(4); // 3 + 1
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl._nativeButtonNode).to.be.not.null;
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(elForm.children[3].firstChild).to.equal(lionBtnEl._nativeButtonNode);
|
||||
});
|
||||
|
||||
it('creates max one helper submit button per form', async () => {
|
||||
const elForm = /** @type {HTMLFormElement} */ (
|
||||
await fixture(html`
|
||||
<form>
|
||||
<input />
|
||||
<${tagButton} type="submit">foo</${tagButton}>
|
||||
<${tagButton} type="submit">foo</${tagButton}>
|
||||
<input />
|
||||
</form>
|
||||
`)
|
||||
);
|
||||
const [lionBtnEl1, lionBtnEl2] = /** @type {LionButtonSubmit[]} */ (
|
||||
Array.from(elForm.querySelectorAll('[type=submit]'))
|
||||
);
|
||||
const { children } = elForm;
|
||||
expect(children.length).to.equal(5); // 4 + 1
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl1._nativeButtonNode).to.be.not.null;
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(lionBtnEl2._nativeButtonNode).to.be.not.null;
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(children[children.length - 1].firstChild).to.equal(lionBtnEl1._nativeButtonNode);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(children[children.length - 1].firstChild).to.equal(lionBtnEl2._nativeButtonNode);
|
||||
});
|
||||
|
||||
it('helper submit button gets reconnected when external context changes (rerenders)', async () => {
|
||||
const elForm = /** @type {HTMLFormElement} */ (
|
||||
await fixture(html`<form><${tagButton} type="submit">foo</${tagButton}></form>`)
|
||||
);
|
||||
const helperBtnEl = /** @type {HTMLButtonElement} */ (
|
||||
elForm.querySelector('button[type=submit]')
|
||||
);
|
||||
helperBtnEl.remove();
|
||||
expect(elForm).to.not.include(helperBtnEl);
|
||||
await aTimeout(0);
|
||||
expect(elForm).to.include(helperBtnEl);
|
||||
});
|
||||
|
||||
it('helper submit button gets removed when last LionbuttonSubmit gets disconnected from form', async () => {
|
||||
const elForm = /** @type {HTMLFormElement} */ (
|
||||
await fixture(
|
||||
html`<form><${tagButton} type="submit">foo</${tagButton}><${tagButton} type="submit">foo</${tagButton}></form>`,
|
||||
)
|
||||
);
|
||||
const [lionBtnEl1, lionBtnEl2] = /** @type {LionButtonSubmit[]} */ (
|
||||
Array.from(elForm.querySelectorAll('[type=submit]'))
|
||||
);
|
||||
const helperBtnEl = elForm.children[elForm.children.length - 1].firstChild;
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(helperBtnEl).to.equal(lionBtnEl1._nativeButtonNode);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(helperBtnEl).to.equal(lionBtnEl2._nativeButtonNode);
|
||||
|
||||
elForm.removeChild(lionBtnEl1);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(helperBtnEl).to.equal(lionBtnEl2._nativeButtonNode);
|
||||
|
||||
elForm.removeChild(lionBtnEl2);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
expect(helperBtnEl).to.not.equal(lionBtnEl2._nativeButtonNode);
|
||||
expect(Array.from(elForm.children)).to.not.include(helperBtnEl);
|
||||
});
|
||||
|
||||
it('hides the helper submit button in the UI', async () => {
|
||||
const el = /** @type {LionButtonSubmit} */ (
|
||||
await fixture(html`<form><${tagButton}>foo</${tagButton}></form>`)
|
||||
);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
const helperButtonEl = el.querySelector(tagStringButton)._nativeButtonNode;
|
||||
expect(helperButtonEl.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(window.getComputedStyle(helperButtonEl).clip).to.equal('rect(0px, 0px, 0px, 0px)');
|
||||
});
|
||||
});
|
||||
|
||||
it('works with implicit form submission on-enter inside an input', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<input name="foo" />
|
||||
<input name="foo2" />
|
||||
<${tagButton} type="submit">foo</${tagButton}>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const input2 = /** @type {HTMLInputElement} */ (form.querySelector('input[name="foo2"]'));
|
||||
input2.focus();
|
||||
await sendKeys({
|
||||
press: 'Enter',
|
||||
});
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('the helper button has aria-hidden set to true', async () => {
|
||||
const el = /** @type {LionButtonSubmit} */ (
|
||||
await fixture(html`<form><${tagButton}></${tagButton}></form>`)
|
||||
);
|
||||
// @ts-ignore [allow-protected] in test
|
||||
const helperButtonEl = el.querySelector(tagStringButton)._nativeButtonNode;
|
||||
expect(helperButtonEl.getAttribute('aria-hidden')).to.equal('true');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1,602 +1,9 @@
|
|||
/* eslint-disable lit-a11y/click-events-have-key-events */
|
||||
import { browserDetection } from '@lion/core';
|
||||
import { aTimeout, expect, fixture, oneEvent } from '@open-wc/testing';
|
||||
import { unsafeStatic, html } from 'lit/static-html.js';
|
||||
import sinon from 'sinon';
|
||||
import '@lion/core/differentKeyEventNamesShimIE';
|
||||
import '@lion/button/define';
|
||||
|
||||
/**
|
||||
* @typedef {import('@lion/button/src/LionButton').LionButton} LionButton
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LionButton} el
|
||||
*/
|
||||
function getProtectedMembers(el) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
nativeButtonNode: el._nativeButtonNode,
|
||||
};
|
||||
}
|
||||
import { LionButtonSuite } from '../test-suites/LionButton.suite.js';
|
||||
import { LionButtonResetSuite } from '../test-suites/LionButtonReset.suite.js';
|
||||
import { LionButtonSubmitSuite } from '../test-suites/LionButtonSubmit.suite.js';
|
||||
|
||||
describe('lion-button', () => {
|
||||
it('behaves like native `button` in terms of a11y', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
expect(el.getAttribute('role')).to.equal('button');
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
});
|
||||
|
||||
it('has .type="submit" and type="submit" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.type).to.equal('submit');
|
||||
expect(el.getAttribute('type')).to.be.equal('submit');
|
||||
expect(nativeButtonNode.type).to.equal('submit');
|
||||
expect(nativeButtonNode.getAttribute('type')).to.be.equal('submit');
|
||||
});
|
||||
|
||||
it('sync type down to the native button', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button type="button">foo</lion-button>`)
|
||||
);
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.type).to.equal('button');
|
||||
expect(el.getAttribute('type')).to.be.equal('button');
|
||||
expect(nativeButtonNode.type).to.equal('button');
|
||||
expect(nativeButtonNode.getAttribute('type')).to.be.equal('button');
|
||||
});
|
||||
|
||||
it('hides the native button in the UI', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(nativeButtonNode.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(window.getComputedStyle(nativeButtonNode).clip).to.equal('rect(0px, 0px, 0px, 0px)');
|
||||
});
|
||||
|
||||
it('is hidden when attribute hidden is true', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button hidden>foo</lion-button>`));
|
||||
expect(el).not.to.be.displayed;
|
||||
});
|
||||
|
||||
it('can be disabled imperatively', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button disabled>foo</lion-button>`));
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
expect(el.hasAttribute('disabled')).to.equal(false);
|
||||
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
expect(el.hasAttribute('disabled')).to.equal(true);
|
||||
});
|
||||
|
||||
describe('active', () => {
|
||||
it('updates "active" attribute on host when mousedown/mouseup on button', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
el.dispatchEvent(new Event('mousedown'));
|
||||
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new Event('mouseup'));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when mousedown on button and mouseup anywhere else', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
|
||||
el.dispatchEvent(new Event('mousedown'));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
document.dispatchEvent(new Event('mouseup'));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when space keydown/keyup on button', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when space keydown on button and space keyup anywhere else', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when enter keydown/keyup on button', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
|
||||
it('updates "active" attribute on host when enter keydown on button and space keyup anywhere else', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
|
||||
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
|
||||
expect(el.active).to.be.true;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.true;
|
||||
|
||||
document.body.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
expect(el.active).to.be.false;
|
||||
await el.updateComplete;
|
||||
expect(el.hasAttribute('active')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('a11y', () => {
|
||||
it('has a role="button" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
expect(el.getAttribute('role')).to.equal('button');
|
||||
el.role = 'foo';
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('role')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('does not override user provided role', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button role="foo">foo</lion-button>`)
|
||||
);
|
||||
expect(el.getAttribute('role')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('has a tabindex="0" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
});
|
||||
|
||||
it('has a tabindex="-1" when disabled', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button disabled>foo</lion-button>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('0');
|
||||
el.disabled = true;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
});
|
||||
|
||||
it('does not override user provided tabindex', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button tabindex="5">foo</lion-button>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('5');
|
||||
});
|
||||
|
||||
it('disabled does not override user provided tabindex', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button tabindex="5" disabled>foo</lion-button>`)
|
||||
);
|
||||
expect(el.getAttribute('tabindex')).to.equal('-1');
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('tabindex')).to.equal('5');
|
||||
});
|
||||
|
||||
it('has an aria-labelledby and wrapper element in IE11', async () => {
|
||||
const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true);
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
expect(el.hasAttribute('aria-labelledby')).to.be.true;
|
||||
const wrapperId = el.getAttribute('aria-labelledby');
|
||||
expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`#${wrapperId}`)).to.exist;
|
||||
expect(/** @type {ShadowRoot} */ (el.shadowRoot).querySelector(`#${wrapperId}`)).dom.to.equal(
|
||||
`<div class="button-content" id="${wrapperId}"><slot></slot></div>`,
|
||||
);
|
||||
browserDetectionStub.restore();
|
||||
});
|
||||
|
||||
it('does not override aria-labelledby when provided by user', async () => {
|
||||
const browserDetectionStub = sinon.stub(browserDetection, 'isIE11').value(true);
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button aria-labelledby="some-id another-id">foo</lion-button>`)
|
||||
);
|
||||
expect(el.getAttribute('aria-labelledby')).to.equal('some-id another-id');
|
||||
browserDetectionStub.restore();
|
||||
});
|
||||
|
||||
it('has a native button node with aria-hidden set to true', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture('<lion-button></lion-button>'));
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(nativeButtonNode.getAttribute('aria-hidden')).to.equal('true');
|
||||
});
|
||||
|
||||
// TODO: enable when native button is not a child anymore
|
||||
it.skip('is accessible', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
// TODO: enable when native button is not a child anymore
|
||||
it.skip('is accessible when disabled', async () => {
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button disabled>foo</lion-button>`)
|
||||
);
|
||||
await expect(el).to.be.accessible({ ignoredRules: ['color-contrast'] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('form integration', () => {
|
||||
describe('with submit event', () => {
|
||||
it('behaves like native `button` when clicked', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<lion-button type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
const button /** @type {LionButton} */ = /** @type {LionButton} */ (
|
||||
form.querySelector('lion-button')
|
||||
);
|
||||
button.click();
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard space', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<lion-button type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
const button /** @type {LionButton} */ = /** @type {LionButton} */ (
|
||||
form.querySelector('lion-button')
|
||||
);
|
||||
button.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard enter', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<lion-button type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const button = /** @type {LionButton} */ (form.querySelector('lion-button'));
|
||||
button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('supports resetting form inputs in a native form', async () => {
|
||||
const form = await fixture(html`
|
||||
<form>
|
||||
<input name="firstName" />
|
||||
<input name="lastName" />
|
||||
<lion-button type="reset">reset</lion-button>
|
||||
</form>
|
||||
`);
|
||||
const btn /** @type {LionButton} */ = /** @type {LionButton} */ (
|
||||
form.querySelector('lion-button')
|
||||
);
|
||||
const firstName = /** @type {HTMLInputElement} */ (
|
||||
form.querySelector('input[name=firstName]')
|
||||
);
|
||||
const lastName = /** @type {HTMLInputElement} */ (
|
||||
form.querySelector('input[name=lastName]')
|
||||
);
|
||||
firstName.value = 'Foo';
|
||||
lastName.value = 'Bar';
|
||||
|
||||
expect(firstName.value).to.equal('Foo');
|
||||
expect(lastName.value).to.equal('Bar');
|
||||
|
||||
btn.click();
|
||||
|
||||
expect(firstName.value).to.be.empty;
|
||||
expect(lastName.value).to.be.empty;
|
||||
});
|
||||
|
||||
// input "enter" keypress mock doesn't seem to work right now, but should be tested in the future (maybe with Selenium)
|
||||
it.skip('works with implicit form submission on-enter inside an input', async () => {
|
||||
const formSubmitSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const form = await fixture(html`
|
||||
<form @submit="${formSubmitSpy}">
|
||||
<input name="foo" />
|
||||
<input name="foo2" />
|
||||
<lion-button type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const input2 = /** @type {HTMLInputElement} */ (form.querySelector('input[name="foo2"]'));
|
||||
input2.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formSubmitSpy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
describe('with click event', () => {
|
||||
it('behaves like native `button` when clicked', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const button = /** @type {LionButton} */ (form.querySelector('lion-button'));
|
||||
button.click();
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard space', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const lionButton = /** @type {LionButton} */ (form.querySelector('lion-button'));
|
||||
lionButton.dispatchEvent(new KeyboardEvent('keyup', { key: ' ' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('behaves like native `button` when interacted with keyboard enter', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const button = /** @type {LionButton} */ (form.querySelector('lion-button'));
|
||||
button.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
// input "enter" keypress mock doesn't seem to work right now, but should be tested in the future (maybe with Selenium)
|
||||
it.skip('works with implicit form submission on-enter inside an input', async () => {
|
||||
const formButtonClickedSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const form = await fixture(html`
|
||||
<form @submit=${/** @type {EventListener} */ ev => ev.preventDefault()}>
|
||||
<input name="foo" />
|
||||
<input name="foo2" />
|
||||
<lion-button @click="${formButtonClickedSpy}" type="submit">foo</lion-button>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const input2 = /** @type {HTMLInputElement} */ (form.querySelector('input[name="foo2"]'));
|
||||
input2.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' }));
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(formButtonClickedSpy).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('click event', () => {
|
||||
/**
|
||||
* @param {HTMLButtonElement | LionButton} el
|
||||
*/
|
||||
async function prepareClickEvent(el) {
|
||||
setTimeout(() => {
|
||||
el.click();
|
||||
});
|
||||
return oneEvent(el, 'click');
|
||||
}
|
||||
|
||||
it('is fired once', async () => {
|
||||
const clickSpy = /** @type {EventListener} */ (sinon.spy());
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(html` <lion-button @click="${clickSpy}">foo</lion-button> `)
|
||||
);
|
||||
|
||||
el.click();
|
||||
|
||||
// trying to wait for other possible redispatched events
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(clickSpy).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('is fired once outside and inside the form', async () => {
|
||||
const outsideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const insideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
|
||||
const el = /** @type {HTMLDivElement} */ (
|
||||
await fixture(
|
||||
html`
|
||||
<div @click="${outsideSpy}">
|
||||
<form @click="${formSpyEarly}">
|
||||
<div @click="${insideSpy}">
|
||||
<lion-button>foo</lion-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
);
|
||||
const lionButton = /** @type {LionButton} */ (el.querySelector('lion-button'));
|
||||
const form = /** @type {HTMLFormElement} */ (el.querySelector('form'));
|
||||
form.addEventListener('click', formSpyLater);
|
||||
|
||||
lionButton.click();
|
||||
// trying to wait for other possible redispatched events
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(insideSpy).to.have.been.calledOnce;
|
||||
expect(outsideSpy).to.have.been.calledOnce;
|
||||
// A small sacrifice for event listeners registered early: we get the native button evt.
|
||||
expect(formSpyEarly).to.have.been.calledTwice;
|
||||
expect(formSpyLater).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('works when connected to different form', async () => {
|
||||
const form1El = /** @type {HTMLFormElement} */ (
|
||||
await fixture(
|
||||
html`
|
||||
<form>
|
||||
<lion-button>foo</lion-button>
|
||||
</form>
|
||||
`,
|
||||
)
|
||||
);
|
||||
const lionButton = /** @type {LionButton} */ (form1El.querySelector('lion-button'));
|
||||
|
||||
expect(lionButton._form).to.equal(form1El);
|
||||
|
||||
// Now we add the lionButton to a different form.
|
||||
// We disconnect and connect and check if everything still works as expected
|
||||
const outsideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const insideSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyEarly = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const formSpyLater = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
|
||||
const form2El = /** @type {HTMLFormElement} */ (
|
||||
await fixture(
|
||||
html`
|
||||
<div @click="${outsideSpy}">
|
||||
<form @click="${formSpyEarly}">
|
||||
<div @click="${insideSpy}">${lionButton}</div>
|
||||
</form>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
);
|
||||
const form2Node = /** @type {HTMLFormElement} */ (form2El.querySelector('form'));
|
||||
|
||||
expect(lionButton._form).to.equal(form2Node);
|
||||
|
||||
form2Node.addEventListener('click', formSpyLater);
|
||||
lionButton.click();
|
||||
// trying to wait for other possible redispatched events
|
||||
await aTimeout(0);
|
||||
await aTimeout(0);
|
||||
|
||||
expect(insideSpy).to.have.been.calledOnce;
|
||||
expect(outsideSpy).to.have.been.calledOnce;
|
||||
// A small sacrifice for event listeners registered early: we get the native button evt.
|
||||
expect(formSpyEarly).to.have.been.calledTwice;
|
||||
expect(formSpyLater).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('native button behavior', async () => {
|
||||
/** @type {Event} */
|
||||
let nativeButtonEvent;
|
||||
/** @type {Event} */
|
||||
let lionButtonEvent;
|
||||
|
||||
before(async () => {
|
||||
const nativeButtonEl = /** @type {LionButton} */ (await fixture('<button>foo</button>'));
|
||||
const lionButtonEl = /** @type {LionButton} */ (
|
||||
await fixture('<lion-button>foo</lion-button>')
|
||||
);
|
||||
nativeButtonEvent = await prepareClickEvent(nativeButtonEl);
|
||||
lionButtonEvent = await prepareClickEvent(lionButtonEl);
|
||||
});
|
||||
|
||||
const sameProperties = [
|
||||
'constructor',
|
||||
'composed',
|
||||
'bubbles',
|
||||
'cancelable',
|
||||
'clientX',
|
||||
'clientY',
|
||||
];
|
||||
|
||||
sameProperties.forEach(property => {
|
||||
it(`has same value of the property "${property}" as in native button event`, () => {
|
||||
expect(lionButtonEvent[property]).to.equal(nativeButtonEvent[property]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('event target', async () => {
|
||||
it('is host by default', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture('<lion-button>foo</lion-button>'));
|
||||
const event = await prepareClickEvent(el);
|
||||
expect(event.target).to.equal(el);
|
||||
});
|
||||
|
||||
const useCases = [
|
||||
{ container: 'div', type: 'submit' },
|
||||
{ container: 'div', type: 'reset' },
|
||||
{ container: 'div', type: 'button' },
|
||||
{ container: 'form', type: 'submit' },
|
||||
{ container: 'form', type: 'reset' },
|
||||
{ container: 'form', type: 'button' },
|
||||
];
|
||||
|
||||
useCases.forEach(useCase => {
|
||||
const { container, type } = useCase;
|
||||
const targetName = 'host';
|
||||
it(`is ${targetName} with type ${type} and it is inside a ${container}`, async () => {
|
||||
const clickSpy = /** @type {EventListener} */ (sinon.spy(e => e.preventDefault()));
|
||||
const el = /** @type {LionButton} */ (
|
||||
await fixture(`<lion-button type="${type}">foo</lion-button>`)
|
||||
);
|
||||
const tag = unsafeStatic(container);
|
||||
await fixture(html`<${tag} @click="${clickSpy}">${el}</${tag}>`);
|
||||
const event = await prepareClickEvent(el);
|
||||
|
||||
expect(event.target).to.equal(el);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
LionButtonSuite();
|
||||
LionButtonResetSuite();
|
||||
LionButtonSubmitSuite();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -145,15 +145,15 @@ export class UmbrellaForm extends LitElement {
|
|||
</lion-input-stepper>
|
||||
<lion-textarea name="comments" label="Comments"></lion-textarea>
|
||||
<div class="buttons">
|
||||
<lion-button id="submit_button" raised>Submit</lion-button>
|
||||
<lion-button
|
||||
<lion-button-submit id="submit_button" raised>Submit</lion-button-submit>
|
||||
<lion-button-reset
|
||||
id="reset_button"
|
||||
type="button"
|
||||
raised
|
||||
@click="${() => {
|
||||
this._lionFormNode.resetGroup();
|
||||
}}"
|
||||
>Reset</lion-button
|
||||
>Reset</lion-button-reset
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { LionButton } from '@lion/button';
|
||||
import { css, html } from '@lion/core';
|
||||
import { css, html, SlotMixin } from '@lion/core';
|
||||
|
||||
/**
|
||||
* @typedef {import('@lion/core').CSSResult} CSSResult
|
||||
|
|
@ -10,7 +10,7 @@ import { css, html } from '@lion/core';
|
|||
/**
|
||||
* LionSelectInvoker: invoker button consuming a selected element
|
||||
*/
|
||||
export class LionSelectInvoker extends LionButton {
|
||||
export class LionSelectInvoker extends SlotMixin(LionButton) {
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
|
|
@ -121,6 +121,10 @@ export class LionSelectInvoker extends LionButton {
|
|||
return this._noSelectionTemplate();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${this._beforeTemplate()} ${super.render()} ${this._afterTemplate()} `;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be overriden for a placeholder, used when `hasNoDefaultSelected` is true on the select rich
|
||||
* @protected
|
||||
|
|
|
|||
Loading…
Reference in a new issue