Compare commits
1 commit
master
...
chore/add-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0cf0ce4fb3 |
36 changed files with 192 additions and 523 deletions
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
[validation-message] remove the letter T from IsDateDisabled in Italian message
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
fix(LionInputStepper): improve handling of decimal step values and alignment closes #2615
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
feat(LionInputStepper): implement self-destructing output content for value display
|
||||
|
||||
1. from <div class="input-stepper__value">${this.__valueText}</div> to <output class="input-stepper__value" for="..">${this.\_\_valueText}</output>
|
||||
2. remove the \_onEnterButton() and \_onLeaveButton() logic.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
[form-core] improve validation feedback message for screen readers to make it one sentence.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
'@lion/ui': patch
|
||||
---
|
||||
|
||||
fix(pagination): remove unnecessary ellipsis when count equals visiblePages + 1
|
||||
|
||||
Fixed issue where LionPagination component incorrectly displayed ellipsis when the total page count was exactly one more than the visible pages setting (e.g., showing [1, 2, 3, 4, 5, '...', 6] instead of [1, 2, 3, 4, 5, 6] when visiblePages=5 and count=6).
|
||||
29
README.md
29
README.md
|
|
@ -54,35 +54,6 @@ They provide an unopinionated, white-label layer that can be extended to your ow
|
|||
<a href="https://lion.js.org/guides/"><strong>Explore the Lion Guides ▶</strong></a>
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary><h2>Table of Contents</h2></summary>
|
||||
<ul>
|
||||
<li><a href="#astro-migration"> Astro migration </a>
|
||||
<ul>
|
||||
<li><a href="#astro-how-to"> Astro how to </a></li>
|
||||
<li><a href="#issues-which-are-not-caused-by-the-migration-not-to-be-fixed-now"> Issues which are not caused by migration </a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#how-to-install"> Installation </a></li>
|
||||
<li><a href="#how-to-use"> How to use </a>
|
||||
<ul>
|
||||
<li><a href="#extend-a-web-component"> Extend a web component </a></li>
|
||||
<li><a href="#use-a-javascript-system"> Use a JavaScript system </a></li>
|
||||
<li><a href="#use-a-web-component"> Use a Web Component </a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#issues"> Issues </a></li>
|
||||
<li><a href="#feature-requests"> Feature request </a></li>
|
||||
<li><a href="#content"> Content </a></li>
|
||||
<li><a href="#technologies"> Technologies </a></li>
|
||||
<li><a href="#rationale"> Rationale </a></li>
|
||||
<li><a href="#coding-guidelines"> Coding guidelines </a></li>
|
||||
<li><a href="#how-to-contribute"> How to contribute </a></li>
|
||||
<li><a href="#site-deployment"> Site deployment </a></li>
|
||||
<li><a href="#content"> Contact </a></li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
## Astro migration
|
||||
|
||||
- Keep using `/docs` on the root level as we used it in the `master` branch. The documentation is copied into Astro related directories on `npm run start` and when when anything in `/docs` is updated.
|
||||
|
|
|
|||
|
|
@ -130,13 +130,3 @@ rocket-navigation ul > li:nth-child(n + 6).active > ul {
|
|||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.announcement-bar {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
text-align: center;
|
||||
|
||||
& a {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<div class="announcement-bar">
|
||||
<p>A new portal is coming! Check out <a href="/about/astro">the technical release of our Astro portal</a></p>
|
||||
</div>
|
||||
|
||||
<header id="main-header">
|
||||
<div class="content-area">
|
||||
{% for blockName, blockPath in _joiningBlocks.header %}
|
||||
{% include blockPath %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
---
|
||||
title: Astro portal
|
||||
description: Technically live with Astro (while still fully compatible with Rocket content!)
|
||||
---
|
||||
|
||||
# Astro Portal Announcement
|
||||
|
||||
We are excited to introduce the new version of our portal, now built with the Astro framework! You can access it at <a href="https://lion.js.org/next" rel="noopener noreferrer">/next</a>.
|
||||
|
||||
## What’s New?
|
||||
|
||||
- **Modern Look & Feel:** The UI will be redesigned in close collaboration with our designers. Expect further improvements as the design team continues to enhance the user experience.
|
||||
- **Search Functionality:** Quickly find components and documentation with the new search feature.
|
||||
- **Upgraded Dependencies:** All major dependencies and approaches have been updated for better performance, security, and maintainability.
|
||||
|
||||
## Compatibility
|
||||
|
||||
The new Astro portal is fully compatible with the previous Rocket-based portal. Maintaining the same structure and functionality was a key challenge and priority, ensuring a seamless experience for all users. Both portals will run in parallel in the near term future. During the course of 2026 the Astro portal will become the default.
|
||||
|
||||
## For Developers
|
||||
|
||||
The technical release of the Astro portal is primarily aimed at developers. We encourage you to explore the new features and provide feedback.
|
||||
|
||||
**Found an issue or have a suggestion?**
|
||||
Please open an issue on [GitHub](https://github.com/ing-bank/lion/issues) and use the tag `#astro`.
|
||||
|
||||
Stay tuned for more updates as we continue to improve the portal!
|
||||
|
|
@ -81,13 +81,19 @@ You can use `slot="label"` instead of the `label` attribute for defining more co
|
|||
```html preview-story
|
||||
<lion-checkbox-group name="scientists[]" label="Favorite scientists">
|
||||
<lion-checkbox .choiceValue="${'Archimedes'}">
|
||||
<label slot="label"><strong>Archimedes</strong></label>
|
||||
<label slot="label"
|
||||
><a href="https://wikipedia.org/wiki/Archimedes" target="_blank">Archimedes</a></label
|
||||
>
|
||||
</lion-checkbox>
|
||||
<lion-checkbox .choiceValue="${'Francis Bacon'}">
|
||||
<label slot="label"><strong>Francis Bacon</strong></label>
|
||||
<label slot="label"
|
||||
><a href="https://wikipedia.org/wiki/Francis_Bacon" target="_blank">Francis Bacon</a></label
|
||||
>
|
||||
</lion-checkbox>
|
||||
<lion-checkbox .choiceValue="${'Marie Curie'}">
|
||||
<label slot="label"><strong>Marie Curie</strong></label>
|
||||
<label slot="label"
|
||||
><a href="https://wikipedia.org/wiki/Marie_Curie" target="_blank">Marie Curie</a></label
|
||||
>
|
||||
</lion-checkbox>
|
||||
</lion-checkbox-group>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -86,13 +86,19 @@ You can use `slot="label"` instead of the `label` attribute for defining more co
|
|||
export const label = () => html`
|
||||
<lion-radio-group name="dinos_7" label="Favourite dinosaur">
|
||||
<lion-radio .choiceValue="${'allosaurus'}">
|
||||
<label slot="label"><strong>allosaurus</strong></label>
|
||||
<label slot="label"
|
||||
><a href="https://wikipedia.org/wiki/allosaurus" target="_blank">allosaurus</a></label
|
||||
>
|
||||
</lion-radio>
|
||||
<lion-radio .choiceValue="${'brontosaurus'}">
|
||||
<label slot="label"><strong>brontosaurus</strong></label>
|
||||
<label slot="label"
|
||||
><a href="https://wikipedia.org/wiki/brontosaurus" target="_blank">brontosaurus</a></label
|
||||
>
|
||||
</lion-radio>
|
||||
<lion-radio .choiceValue="${'diplodocus'}">
|
||||
<label slot="label"><strong>diplodocus</strong></label>
|
||||
<label slot="label"
|
||||
><a href="https://wikipedia.org/wiki/diplodocus" target="_blank">diplodocus</a></label
|
||||
>
|
||||
</lion-radio>
|
||||
</lion-radio-group>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import '@lion/ui/define/lion-select.js';
|
|||
export const main = () => html`
|
||||
<lion-select name="favoriteColor" label="Favorite color">
|
||||
<select slot="input">
|
||||
<option selected value>Please select</option>
|
||||
<option selected hidden value>Please select</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="hotpink">Hotpink</option>
|
||||
<option value="teal">Teal</option>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ You can preselect an option by setting the property modelValue.
|
|||
```html preview-story
|
||||
<lion-select name="favoriteColor" label="Favorite color" .modelValue="${'hotpink'}">
|
||||
<select slot="input">
|
||||
<option selected value>Please select</option>
|
||||
<option selected hidden value>Please select</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="hotpink">Hotpink</option>
|
||||
<option value="teal">Teal</option>
|
||||
|
|
@ -42,7 +42,7 @@ You can disable an option by adding the `disabled` attribute to an option.
|
|||
```html preview-story
|
||||
<lion-select name="favoriteColor" label="Favorite color">
|
||||
<select slot="input">
|
||||
<option selected value>Please select</option>
|
||||
<option selected hidden value>Please select</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="hotpink" disabled>Hotpink</option>
|
||||
<option value="teal">Teal</option>
|
||||
|
|
@ -55,7 +55,7 @@ Or by setting the `disabled` attribute on the entire `lion-select` field.
|
|||
```html preview-story
|
||||
<lion-select name="favoriteColor" label="Favorite color" disabled>
|
||||
<select slot="input">
|
||||
<option selected value>Please select</option>
|
||||
<option selected hidden value>Please select</option>
|
||||
<option value="red">Red</option>
|
||||
<option value="hotpink">Hotpink</option>
|
||||
<option value="teal">Teal</option>
|
||||
|
|
@ -89,7 +89,7 @@ class MyOptions extends LitElement {
|
|||
return html`
|
||||
<lion-select name="favoriteColor" label="Favorite color" disabled>
|
||||
<select slot="input">
|
||||
<option selected value>Please select</option>
|
||||
<option selected hidden value>Please select</option>
|
||||
${this.colours.map(colour => html`<option value="${colour}">${colour}</option>`)}
|
||||
</select>
|
||||
</lion-select>
|
||||
|
|
@ -115,7 +115,7 @@ render() {
|
|||
return html`
|
||||
<lion-select name="favoriteColor" label="Favorite color" disabled>
|
||||
<select slot="input">
|
||||
<option selected value>Please select</option>
|
||||
<option selected hidden value>Please select</option>
|
||||
${repeat(
|
||||
this.colours,
|
||||
(colour) => colour,
|
||||
|
|
|
|||
|
|
@ -87,11 +87,11 @@ export class LionValidationFeedback extends LocalizeMixin(LitElement) {
|
|||
${this.feedbackData &&
|
||||
this.feedbackData.map(
|
||||
({ message, type, validator }) => html`
|
||||
<span class="validation-feedback__type">
|
||||
<div class="validation-feedback__type">
|
||||
${message && type
|
||||
? this._localizeManager.msg(`lion-form-core:validation${capitalize(type)}`)
|
||||
: nothing}
|
||||
</span>
|
||||
</div>
|
||||
${this._messageTemplate({ message, type, validator })}
|
||||
`,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -61,12 +61,12 @@ describe('lion-validation-feedback', () => {
|
|||
await el.updateComplete;
|
||||
|
||||
const validationFeedbackType = el.shadowRoot?.querySelector('.validation-feedback__type');
|
||||
expect(validationFeedbackType?.textContent?.trim()).to.equal('Error,');
|
||||
expect(validationFeedbackType?.textContent?.trim()).to.equal('Error');
|
||||
|
||||
el.feedbackData = [{ message: 'hello', type: 'info', validator: new AlwaysInvalid() }];
|
||||
await el.updateComplete;
|
||||
|
||||
expect(validationFeedbackType?.textContent?.trim()).to.equal('Info,');
|
||||
expect(validationFeedbackType?.textContent?.trim()).to.equal('Info');
|
||||
});
|
||||
|
||||
it('it does not share the type if there is no message', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Грешка,',
|
||||
validationWarning: 'Предупреждение,',
|
||||
validationSuccess: 'Успех,',
|
||||
validationInfo: 'Информация,',
|
||||
validationError: 'Грешка',
|
||||
validationWarning: 'Предупреждение',
|
||||
validationSuccess: 'Успех',
|
||||
validationInfo: 'Информация',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Chyba,',
|
||||
validationWarning: 'Varování,',
|
||||
validationSuccess: 'Úspěch,',
|
||||
validationInfo: 'Informace,',
|
||||
validationError: 'Chyba',
|
||||
validationWarning: 'Varování',
|
||||
validationSuccess: 'Úspěch',
|
||||
validationInfo: 'Informace',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Fehler,',
|
||||
validationWarning: 'Warnhinweis,',
|
||||
validationSuccess: 'Erfolgreich,',
|
||||
validationInfo: 'Info,',
|
||||
validationError: 'Fehler',
|
||||
validationWarning: 'Warnhinweis',
|
||||
validationSuccess: 'Erfolgreich',
|
||||
validationInfo: 'Info',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Error,',
|
||||
validationWarning: 'Warning,',
|
||||
validationSuccess: 'Success,',
|
||||
validationInfo: 'Info,',
|
||||
validationError: 'Error',
|
||||
validationWarning: 'Warning',
|
||||
validationSuccess: 'Success',
|
||||
validationInfo: 'Info',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Error,',
|
||||
validationWarning: 'Advertencia,',
|
||||
validationSuccess: 'Satisfactorio,',
|
||||
validationInfo: 'Información,',
|
||||
validationError: 'Error',
|
||||
validationWarning: 'Advertencia',
|
||||
validationSuccess: 'Satisfactorio',
|
||||
validationInfo: 'Información',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Erreur,',
|
||||
validationWarning: 'Avertissement,',
|
||||
validationSuccess: 'Succès,',
|
||||
validationInfo: 'Info,',
|
||||
validationError: 'Erreur',
|
||||
validationWarning: 'Avertissement',
|
||||
validationSuccess: 'Succès',
|
||||
validationInfo: 'Info',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Hiba,',
|
||||
validationWarning: 'Figyelmeztetés,',
|
||||
validationSuccess: 'Sikeres,',
|
||||
validationInfo: 'Információ,',
|
||||
validationError: 'Hiba',
|
||||
validationWarning: 'Figyelmeztetés',
|
||||
validationSuccess: 'Sikeres',
|
||||
validationInfo: 'Információ',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Errore,',
|
||||
validationWarning: 'Avvertenza,',
|
||||
validationSuccess: 'Operazione riuscita,',
|
||||
validationInfo: 'Info,',
|
||||
validationError: 'Errore',
|
||||
validationWarning: 'Avvertenza',
|
||||
validationSuccess: 'Operazione riuscita',
|
||||
validationInfo: 'Info',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Fout,',
|
||||
validationWarning: 'Waarschuwing,',
|
||||
validationSuccess: 'Succes,',
|
||||
validationInfo: 'Informatie,',
|
||||
validationError: 'Fout',
|
||||
validationWarning: 'Waarschuwing',
|
||||
validationSuccess: 'Succes',
|
||||
validationInfo: 'Informatie',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Błąd,',
|
||||
validationWarning: 'Ostrzeżenie,',
|
||||
validationSuccess: 'Zrealizowano pomyślnie,',
|
||||
validationInfo: 'Informacja,',
|
||||
validationError: 'Błąd',
|
||||
validationWarning: 'Ostrzeżenie',
|
||||
validationSuccess: 'Zrealizowano pomyślnie',
|
||||
validationInfo: 'Informacja',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Eroare,',
|
||||
validationWarning: 'Atenție,',
|
||||
validationSuccess: 'Succes,',
|
||||
validationInfo: 'Informații,',
|
||||
validationError: 'Eroare',
|
||||
validationWarning: 'Atenție',
|
||||
validationSuccess: 'Succes',
|
||||
validationInfo: 'Informații',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Ошибка,',
|
||||
validationWarning: 'Предупреждение,',
|
||||
validationSuccess: 'Успешно,',
|
||||
validationInfo: 'Информация,',
|
||||
validationError: 'Ошибка',
|
||||
validationWarning: 'Предупреждение',
|
||||
validationSuccess: 'Успешно',
|
||||
validationInfo: 'Информация',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Chyba,',
|
||||
validationWarning: 'Varovanie,',
|
||||
validationSuccess: 'Úspešné,',
|
||||
validationInfo: 'Info,',
|
||||
validationError: 'Chyba',
|
||||
validationWarning: 'Varovanie',
|
||||
validationSuccess: 'Úspešné',
|
||||
validationInfo: 'Info',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: 'Помилка,',
|
||||
validationWarning: 'Попередження,',
|
||||
validationSuccess: 'Успішно,',
|
||||
validationInfo: 'Інформація,',
|
||||
validationError: 'Помилка',
|
||||
validationWarning: 'Попередження',
|
||||
validationSuccess: 'Успішно',
|
||||
validationInfo: 'Інформація',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
validationError: '错误,',
|
||||
validationWarning: '警告,',
|
||||
validationSuccess: '成功,',
|
||||
validationInfo: '信息,',
|
||||
validationError: '错误',
|
||||
validationWarning: '警告',
|
||||
validationSuccess: '成功',
|
||||
validationInfo: '信息',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { html, css, render, nothing } from 'lit';
|
||||
import { html, css, render } from 'lit';
|
||||
import { formatNumber, LocalizeMixin, parseNumber } from '@lion/ui/localize-no-side-effects.js';
|
||||
import { LionInput } from '@lion/ui/input.js';
|
||||
import { IsNumber, MinNumber, MaxNumber } from '@lion/ui/form-core.js';
|
||||
|
|
@ -96,6 +96,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
|
||||
this._increment = this._increment.bind(this);
|
||||
this._decrement = this._decrement.bind(this);
|
||||
this._onEnterButton = this._onEnterButton.bind(this);
|
||||
this._onLeaveButton = this._onLeaveButton.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
|
|
@ -242,26 +244,6 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
this._inputNode.removeAttribute('aria-valuenow');
|
||||
this._inputNode.removeAttribute('aria-valuetext');
|
||||
}
|
||||
this._destroyOutputContent();
|
||||
}
|
||||
|
||||
_destroyOutputContent() {
|
||||
const outputElement = /** @type {HTMLElement} */ (
|
||||
this.shadowRoot?.querySelector('.input-stepper__value')
|
||||
);
|
||||
|
||||
const timeoutValue = outputElement?.dataset?.selfDestruct
|
||||
? Number(outputElement.dataset.selfDestruct)
|
||||
: 2000;
|
||||
clearTimeout(this.timer);
|
||||
if (outputElement) {
|
||||
this.timer = setTimeout(() => {
|
||||
if (outputElement.parentNode) {
|
||||
this.__valueText = nothing;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}, timeoutValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -285,19 +267,12 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
_increment() {
|
||||
const { step, min, max } = this.values;
|
||||
const stepMin = min !== Infinity ? min : 0;
|
||||
const epsilon = 1e-10; // Tolerance for floating-point comparison
|
||||
|
||||
let newValue;
|
||||
let newValue = this.currentValue + step;
|
||||
|
||||
const remainder = (this.currentValue - stepMin) % step;
|
||||
const isAligned = Math.abs(remainder) < epsilon || Math.abs(remainder - step) < epsilon;
|
||||
|
||||
if (!isAligned) {
|
||||
// If the value is not aligned to step, align it to the next valid step
|
||||
newValue = Math.ceil((this.currentValue - stepMin) / step) * step + stepMin;
|
||||
} else {
|
||||
// If the value is aligned, just add the step
|
||||
newValue = this.currentValue + step;
|
||||
if ((this.currentValue + stepMin) % step !== 0) {
|
||||
// If the value is not aligned to step, align it to the nearest step
|
||||
newValue = Math.floor(this.currentValue / step) * step + step + (stepMin % step);
|
||||
}
|
||||
|
||||
if (newValue <= max || max === Infinity) {
|
||||
|
|
@ -314,19 +289,12 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
_decrement() {
|
||||
const { step, max, min } = this.values;
|
||||
const stepMin = min !== Infinity ? min : 0;
|
||||
const epsilon = 1e-10; // Tolerance for floating-point comparison
|
||||
|
||||
let newValue;
|
||||
let newValue = this.currentValue - step;
|
||||
|
||||
const remainder = (this.currentValue - stepMin) % step;
|
||||
const isAligned = Math.abs(remainder) < epsilon || Math.abs(remainder - step) < epsilon;
|
||||
|
||||
if (!isAligned) {
|
||||
// If the value is not aligned to step, align it to the previous valid step
|
||||
newValue = Math.floor((this.currentValue - stepMin) / step) * step + stepMin;
|
||||
} else {
|
||||
// If the value is aligned, just subtract the step
|
||||
newValue = this.currentValue - step;
|
||||
if ((this.currentValue + stepMin) % step !== 0) {
|
||||
// If the value is not aligned to step, align it to the nearest step
|
||||
newValue = Math.floor(this.currentValue / step) * step + (stepMin % step);
|
||||
}
|
||||
|
||||
if (newValue >= min || min === Infinity) {
|
||||
|
|
@ -412,6 +380,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
<button
|
||||
?disabled=${this.disabled || this.readOnly}
|
||||
@click=${this._decrement}
|
||||
@focus=${this._onEnterButton}
|
||||
@blur=${this._onLeaveButton}
|
||||
type="button"
|
||||
aria-label="${this.msgLit('lion-input-stepper:decrease')} ${this.fieldName}"
|
||||
>
|
||||
|
|
@ -430,6 +400,8 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
<button
|
||||
?disabled=${this.disabled || this.readOnly}
|
||||
@click=${this._increment}
|
||||
@focus=${this._onEnterButton}
|
||||
@blur=${this._onLeaveButton}
|
||||
type="button"
|
||||
aria-label="${this.msgLit('lion-input-stepper:increase')} ${this.fieldName}"
|
||||
>
|
||||
|
|
@ -441,9 +413,7 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
/** @protected */
|
||||
_inputGroupTemplate() {
|
||||
return html`
|
||||
<output for="${this._inputId}" data-self-destruct="2000" class="input-stepper__value"
|
||||
>${this.__valueText}</output
|
||||
>
|
||||
<div class="input-stepper__value">${this.__valueText}</div>
|
||||
<div class="input-group">
|
||||
${this._inputGroupBeforeTemplate()}
|
||||
<div class="input-group__container">
|
||||
|
|
@ -454,4 +424,38 @@ export class LionInputStepper extends LocalizeMixin(LionInput) {
|
|||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {Event} ev
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_onEnterButton(ev) {
|
||||
const valueNode = /** @type {HTMLElement} */ (
|
||||
this.shadowRoot?.querySelector('.input-stepper__value')
|
||||
);
|
||||
valueNode.setAttribute('aria-live', 'assertive');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redispatch leave event on host when catching leave event
|
||||
* on the incrementor and decrementor button.
|
||||
*
|
||||
* This redispatched leave event will be caught by
|
||||
* InteractionStateMixin to set "touched" state to true.
|
||||
*
|
||||
* Interacting with the buttons is "user interactions"
|
||||
* the same way as focusing + blurring the field (native input)
|
||||
*
|
||||
* @protected
|
||||
* @param {Event} ev
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_onLeaveButton(ev) {
|
||||
const valueNode = /** @type {HTMLElement} */ (
|
||||
this.shadowRoot?.querySelector('.input-stepper__value')
|
||||
);
|
||||
valueNode.removeAttribute('aria-live');
|
||||
this.dispatchEvent(new Event(this._leaveEvent));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { expect, fixture as _fixture, nextFrame } from '@open-wc/testing';
|
||||
import { nothing } from 'lit';
|
||||
import { html } from 'lit/static-html.js';
|
||||
import sinon from 'sinon';
|
||||
import { formatNumber } from '@lion/ui/localize-no-side-effects.js';
|
||||
|
|
@ -179,6 +178,31 @@ describe('<lion-input-stepper>', () => {
|
|||
expect(counter).to.equal(1);
|
||||
});
|
||||
|
||||
it('fires a leave event ("blur") on button clicks', async () => {
|
||||
const blurSpy = sinon.spy();
|
||||
const el = await fixture(html`
|
||||
<lion-input-stepper @blur=${blurSpy} name="year" label="Years"></lion-input-stepper>
|
||||
`);
|
||||
|
||||
expect(el.value).to.equal('');
|
||||
const decrementButton = el.querySelector('[slot=prefix]');
|
||||
decrementButton?.dispatchEvent(new Event('focus'));
|
||||
decrementButton?.dispatchEvent(new Event('click'));
|
||||
decrementButton?.dispatchEvent(new Event('blur'));
|
||||
expect(el.value).to.equal('−1');
|
||||
expect(blurSpy.calledOnce).to.be.true;
|
||||
expect(el.touched).to.be.true;
|
||||
|
||||
el.touched = false;
|
||||
const incrementButton = el.querySelector('[slot=suffix]');
|
||||
incrementButton?.dispatchEvent(new Event('focus'));
|
||||
incrementButton?.dispatchEvent(new Event('click'));
|
||||
incrementButton?.dispatchEvent(new Event('blur'));
|
||||
expect(el.value).to.equal('0');
|
||||
expect(blurSpy.calledTwice).to.be.true;
|
||||
expect(el.touched).to.be.true;
|
||||
});
|
||||
|
||||
it('should update min and max attributes when min and max property change', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
el.min = 100;
|
||||
|
|
@ -335,251 +359,6 @@ describe('<lion-input-stepper>', () => {
|
|||
await el.updateComplete;
|
||||
expect(el.modelValue).to.equal(-13, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7
|
||||
});
|
||||
|
||||
it('handles decimal step values correctly', async () => {
|
||||
// Test with decimal step 0.1
|
||||
let el = await fixture(
|
||||
html`<lion-input-stepper step="0.1" min="0" max="9" value="5.55"></lion-input-stepper>`,
|
||||
);
|
||||
|
||||
// Test increment with decimal step
|
||||
let incrementButton = el.querySelector('[slot=suffix]');
|
||||
incrementButton?.dispatchEvent(new Event('click'));
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.equal(5.6, 'Fail + : (0 > 9 by 0.1; val 5.55)');
|
||||
|
||||
// Test decrement with decimal step
|
||||
let decrementButton = el.querySelector('[slot=prefix]');
|
||||
decrementButton?.dispatchEvent(new Event('click'));
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.equal(5.5, 'Fail - : (0 > 9 by 0.1; val 5.6)');
|
||||
|
||||
// Test with value that needs alignment
|
||||
el = await fixture(
|
||||
html`<lion-input-stepper step="0.1" min="0" max="9" value="3.27"></lion-input-stepper>`,
|
||||
);
|
||||
|
||||
// Should align to next step when incrementing
|
||||
incrementButton = el.querySelector('[slot=suffix]');
|
||||
incrementButton?.dispatchEvent(new Event('click'));
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.equal(3.3, 'Fail + alignment: (0 > 9 by 0.1; val 3.27)');
|
||||
|
||||
// Reset and test decrement alignment
|
||||
el = await fixture(
|
||||
html`<lion-input-stepper step="0.1" min="0" max="9" value="3.27"></lion-input-stepper>`,
|
||||
);
|
||||
|
||||
// Should align to previous step when decrementing
|
||||
decrementButton = el.querySelector('[slot=prefix]');
|
||||
decrementButton?.dispatchEvent(new Event('click'));
|
||||
await el.updateComplete;
|
||||
expect(el.modelValue).to.equal(3.2, 'Fail - alignment: (0 > 9 by 0.1; val 3.27)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('_destroyOutputContent method', () => {
|
||||
beforeEach(() => {
|
||||
// Ensure clean state for each test
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should clear existing timer before setting a new one', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const clearTimeoutSpy = sinon.spy(window, 'clearTimeout');
|
||||
|
||||
// Set an initial timer
|
||||
el.timer = setTimeout(() => {}, 1000);
|
||||
const initialTimer = el.timer;
|
||||
|
||||
// Call _destroyOutputContent
|
||||
el._destroyOutputContent();
|
||||
|
||||
expect(clearTimeoutSpy.calledWith(initialTimer)).to.be.true;
|
||||
clearTimeoutSpy.restore();
|
||||
});
|
||||
|
||||
it('should set __valueText to nothing and request update after timeout', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
||||
// Set initial value text
|
||||
el.__valueText = 'test value';
|
||||
|
||||
// Call _destroyOutputContent
|
||||
el._destroyOutputContent();
|
||||
|
||||
// Fast forward time by 2000ms (default timeout)
|
||||
clock.tick(2000);
|
||||
|
||||
await expect(el.__valueText).to.equal(nothing);
|
||||
expect(requestUpdateSpy.calledOnce).to.be.true;
|
||||
|
||||
clock.restore();
|
||||
requestUpdateSpy.restore();
|
||||
});
|
||||
|
||||
it('should use custom timeout from data-self-destruct attribute', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-input-stepper name="test" label="Test"> </lion-input-stepper>
|
||||
`);
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Get the output element and set custom self-destruct value
|
||||
const outputElement = /** @type {HTMLElement} */ (
|
||||
el.shadowRoot?.querySelector('.input-stepper__value')
|
||||
);
|
||||
if (outputElement) {
|
||||
outputElement.dataset.selfDestruct = '5000';
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers();
|
||||
const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
|
||||
|
||||
el.__valueText = 'test value';
|
||||
el._destroyOutputContent();
|
||||
|
||||
// Should not trigger after default 2000ms
|
||||
clock.tick(2000);
|
||||
await expect(el.__valueText).to.equal('test value');
|
||||
expect(requestUpdateSpy.called).to.be.false;
|
||||
|
||||
// Should trigger after custom 5000ms
|
||||
clock.tick(3000); // Total 5000ms
|
||||
await expect(el.__valueText).to.equal(nothing);
|
||||
expect(requestUpdateSpy.calledOnce).to.be.true;
|
||||
|
||||
clock.restore();
|
||||
requestUpdateSpy.restore();
|
||||
});
|
||||
|
||||
it('should handle invalid data-self-destruct value by using default timeout', async () => {
|
||||
const el = await fixture(html`
|
||||
<lion-input-stepper name="test" label="Test"> </lion-input-stepper>
|
||||
`);
|
||||
|
||||
await el.updateComplete;
|
||||
|
||||
// Get the output element and set invalid self-destruct value
|
||||
const outputElement = /** @type {HTMLElement} */ (
|
||||
el.shadowRoot?.querySelector('.input-stepper__value')
|
||||
);
|
||||
if (outputElement) {
|
||||
outputElement.dataset.selfDestruct = 'invalid';
|
||||
}
|
||||
|
||||
const clock = sinon.useFakeTimers();
|
||||
const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
|
||||
|
||||
el.__valueText = 'test value';
|
||||
el._destroyOutputContent();
|
||||
|
||||
// Should use default 2000ms when invalid value is provided
|
||||
clock.tick(2000);
|
||||
await expect(el.__valueText).to.equal(nothing);
|
||||
expect(requestUpdateSpy.calledOnce).to.be.true;
|
||||
|
||||
clock.restore();
|
||||
requestUpdateSpy.restore();
|
||||
});
|
||||
|
||||
it('should not set timeout if output element does not exist', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const setTimeoutSpy = sinon.spy(window, 'setTimeout');
|
||||
|
||||
// Mock shadowRoot to return null for querySelector
|
||||
const originalQuerySelector = el.shadowRoot?.querySelector;
|
||||
if (el.shadowRoot) {
|
||||
el.shadowRoot.querySelector = () => null;
|
||||
}
|
||||
|
||||
el._destroyOutputContent();
|
||||
|
||||
expect(setTimeoutSpy.called).to.be.false;
|
||||
|
||||
// Restore original querySelector
|
||||
if (el.shadowRoot && originalQuerySelector) {
|
||||
el.shadowRoot.querySelector = originalQuerySelector;
|
||||
}
|
||||
setTimeoutSpy.restore();
|
||||
});
|
||||
|
||||
it('should only execute timeout callback if output element still has parent', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const clock = sinon.useFakeTimers();
|
||||
const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
|
||||
|
||||
el.__valueText = 'test value';
|
||||
el._destroyOutputContent();
|
||||
|
||||
// Remove the output element from DOM before timeout
|
||||
const outputElement = el.shadowRoot?.querySelector('.input-stepper__value');
|
||||
if (outputElement && outputElement.parentNode) {
|
||||
outputElement.parentNode.removeChild(outputElement);
|
||||
}
|
||||
|
||||
// Fast forward time
|
||||
clock.tick(2000);
|
||||
|
||||
// Should not have updated since element was removed
|
||||
expect(requestUpdateSpy.called).to.be.false;
|
||||
expect(el.__valueText).to.equal('test value');
|
||||
|
||||
clock.restore();
|
||||
requestUpdateSpy.restore();
|
||||
});
|
||||
|
||||
it('should be called when increment button is clicked', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const destroyOutputSpy = sinon.spy(el, '_destroyOutputContent');
|
||||
|
||||
const incrementButton = el.querySelector('[slot=suffix]');
|
||||
incrementButton?.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(destroyOutputSpy.calledOnce).to.be.true;
|
||||
destroyOutputSpy.restore();
|
||||
});
|
||||
|
||||
it('should be called when decrement button is clicked', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const destroyOutputSpy = sinon.spy(el, '_destroyOutputContent');
|
||||
|
||||
const decrementButton = el.querySelector('[slot=prefix]');
|
||||
decrementButton?.dispatchEvent(new Event('click'));
|
||||
|
||||
expect(destroyOutputSpy.calledOnce).to.be.true;
|
||||
destroyOutputSpy.restore();
|
||||
});
|
||||
|
||||
it('should handle multiple rapid calls by clearing previous timers', async () => {
|
||||
const el = await fixture(defaultInputStepper);
|
||||
const clock = sinon.useFakeTimers();
|
||||
const requestUpdateSpy = sinon.spy(el, 'requestUpdate');
|
||||
|
||||
el.__valueText = 'test value';
|
||||
|
||||
// Call _destroyOutputContent multiple times rapidly
|
||||
el._destroyOutputContent();
|
||||
clock.tick(1000); // 1 second
|
||||
|
||||
el._destroyOutputContent(); // This should clear the previous timer
|
||||
clock.tick(1000); // Total 2 seconds from first call, 1 second from second call
|
||||
|
||||
// Should not have triggered yet since second call reset the timer
|
||||
expect(el.__valueText).to.equal('test value');
|
||||
expect(requestUpdateSpy.called).to.be.false;
|
||||
|
||||
clock.tick(1000); // Total 2 seconds from second call
|
||||
|
||||
// Should trigger now
|
||||
await expect(el.__valueText).to.equal(nothing);
|
||||
expect(requestUpdateSpy.calledOnce).to.be.true;
|
||||
|
||||
clock.restore();
|
||||
requestUpdateSpy.restore();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -670,6 +449,32 @@ describe('<lion-input-stepper>', () => {
|
|||
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
|
||||
});
|
||||
|
||||
it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
|
||||
const decrementButton = el.querySelector('[slot=prefix]');
|
||||
|
||||
decrementButton?.dispatchEvent(new Event('focus'));
|
||||
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
|
||||
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
|
||||
|
||||
decrementButton?.dispatchEvent(new Event('blur'));
|
||||
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
|
||||
});
|
||||
|
||||
it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
|
||||
const incrementButton = el.querySelector('[slot=suffix]');
|
||||
|
||||
incrementButton?.dispatchEvent(new Event('focus'));
|
||||
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
|
||||
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
|
||||
|
||||
incrementButton?.dispatchEvent(new Event('blur'));
|
||||
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
|
||||
});
|
||||
|
||||
it('decrease button should have aria-label with the component label', async () => {
|
||||
const el = await fixture(inputStepperWithAttrs);
|
||||
const decrementButton = el.querySelector('[slot=prefix]');
|
||||
|
|
|
|||
|
|
@ -196,8 +196,7 @@ export class LionPagination extends LocalizeMixin(LitElement) {
|
|||
const finish = this.count;
|
||||
// If there are more pages then we want to display we have to redo the list each time
|
||||
// Else we can just return the same list every time.
|
||||
// Allow up to 2 extra pages without ellipsis (e.g., count=6 or count=7 when visiblePages=5)
|
||||
if (this.count > this.__visiblePages + 2) {
|
||||
if (this.count > this.__visiblePages) {
|
||||
// Calculate left side of current page and right side
|
||||
const pos3 = this.current - 1;
|
||||
const pos4 = this.current;
|
||||
|
|
|
|||
|
|
@ -164,45 +164,4 @@ describe('Pagination', () => {
|
|||
expect(buttons[3].getAttribute('aria-current')).to.equal('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ellipsis display', () => {
|
||||
it('should not show ellipsis when count is visiblePages + 1 (count=6, visiblePages=5)', async () => {
|
||||
const el = await fixture(html` <lion-pagination count="6" current="1"></lion-pagination> `);
|
||||
const navItems = Array.from(/** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('li'));
|
||||
// Check that no ellipsis is rendered (no <span> elements with '...')
|
||||
const spans = Array.from(
|
||||
/** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('li span'),
|
||||
);
|
||||
expect(spans.length).to.equal(0);
|
||||
|
||||
// There should be 8 nav items: previous button + 6 page buttons + next button
|
||||
expect(navItems.length).to.equal(8);
|
||||
});
|
||||
|
||||
it('should not show ellipsis when count is visiblePages + 2 (count=7, visiblePages=5)', async () => {
|
||||
const el = await fixture(html` <lion-pagination count="7" current="1"></lion-pagination> `);
|
||||
// Check that no ellipsis is rendered (no <span> elements with '...')
|
||||
const spans = Array.from(
|
||||
/** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('li span'),
|
||||
);
|
||||
expect(spans.length).to.equal(0);
|
||||
|
||||
// There should be 9 nav items: previous button + 7 page buttons + next button
|
||||
const navItems = Array.from(/** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('li'));
|
||||
expect(navItems.length).to.equal(9);
|
||||
});
|
||||
|
||||
it('should show ellipsis when count is visiblePages + 3 (count=8, visiblePages=5)', async () => {
|
||||
const el = await fixture(html` <lion-pagination count="8" current="1"></lion-pagination> `);
|
||||
// Check that ellipsis is rendered (should have <span> elements with '...')
|
||||
const spans = Array.from(
|
||||
/** @type {ShadowRoot} */ (el.shadowRoot).querySelectorAll('li span'),
|
||||
);
|
||||
expect(spans.length).to.be.greaterThan(0);
|
||||
|
||||
// Verify the ellipsis contains '...'
|
||||
const ellipsisText = Array.from(spans).map(span => span.textContent);
|
||||
expect(ellipsisText).to.include('...');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export default {
|
|||
MaxDate: 'Inserire un(a) {fieldName} prima di {params, date, YYYYMMDD}.',
|
||||
MinMaxDate:
|
||||
'Inserire un(a) {fieldName} tra {params.min, date, YYYYMMDD} e {params.max, date, YYYYMMDD}.',
|
||||
IsDateDisabled: "Questa data non è disponibile, sceglierne un'altra.",
|
||||
IsDateDisabled: "ТQuesta data non è disponibile, sceglierne un'altra.",
|
||||
IsEmail: 'Inserire un valore valido per {fieldName} nel formato "name@example.com".',
|
||||
MatchesOption:
|
||||
'Nessun risultato corrispondente. Provare con una parola chiave o una categoria diversa.',
|
||||
|
|
@ -41,7 +41,7 @@ export default {
|
|||
MaxDate: 'Inserire un(a) {fieldName} prima di {params, date, YYYYMMDD}.',
|
||||
MinMaxDate:
|
||||
'Inserire un(a) {fieldName} tra {params.min, date, YYYYMMDD} e {params.max, date, YYYYMMDD}.',
|
||||
IsDateDisabled: "Questa data non è disponibile, sceglierne un'altra.",
|
||||
IsDateDisabled: "ТQuesta data non è disponibile, sceglierne un'altra.",
|
||||
IsEmail: 'Inserire un valore valido per {fieldName} nel formato "name@example.com".',
|
||||
MatchesOption:
|
||||
'Nessun risultato corrispondente. Provare con una parola chiave o una categoria diversa.',
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ export default {
|
|||
eleventyConfig.setUseGitIgnore(false);
|
||||
eleventyConfig.addPassthroughCopy('CNAME');
|
||||
},
|
||||
checkLinks: {
|
||||
ignoreLinkPatterns: ['**/astro'],
|
||||
},
|
||||
absoluteBaseUrl: absoluteBaseUrlNetlify('http://localhost:8080'),
|
||||
setupUnifiedPlugins: [
|
||||
adjustPluginOptions(mdjsSetupCode, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue