Compare commits

..

1 commit

Author SHA1 Message Date
Ayo
0cf0ce4fb3 chore: add CNAME 2025-11-17 10:43:10 +01:00
36 changed files with 192 additions and 523 deletions

View file

@ -1,5 +0,0 @@
---
'@lion/ui': patch
---
[validation-message] remove the letter T from IsDateDisabled in Italian message

View file

@ -1,5 +0,0 @@
---
'@lion/ui': patch
---
fix(LionInputStepper): improve handling of decimal step values and alignment closes #2615

View file

@ -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.

View file

@ -1,5 +0,0 @@
---
'@lion/ui': patch
---
[form-core] improve validation feedback message for screen readers to make it one sentence.

View file

@ -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).

View file

@ -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&nbsp;&nbsp;</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.

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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>.
## Whats 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!

View file

@ -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>
```

View file

@ -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>
`;

View file

@ -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>

View file

@ -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,

View file

@ -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 })}
`,
)}

View file

@ -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 () => {

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Грешка,',
validationWarning: 'Предупреждение,',
validationSuccess: 'Успех,',
validationInfo: 'Информация,',
validationError: 'Грешка',
validationWarning: 'Предупреждение',
validationSuccess: 'Успех',
validationInfo: 'Информация',
};

View file

@ -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',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Fehler,',
validationWarning: 'Warnhinweis,',
validationSuccess: 'Erfolgreich,',
validationInfo: 'Info,',
validationError: 'Fehler',
validationWarning: 'Warnhinweis',
validationSuccess: 'Erfolgreich',
validationInfo: 'Info',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Error,',
validationWarning: 'Warning,',
validationSuccess: 'Success,',
validationInfo: 'Info,',
validationError: 'Error',
validationWarning: 'Warning',
validationSuccess: 'Success',
validationInfo: 'Info',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Error,',
validationWarning: 'Advertencia,',
validationSuccess: 'Satisfactorio,',
validationInfo: 'Información,',
validationError: 'Error',
validationWarning: 'Advertencia',
validationSuccess: 'Satisfactorio',
validationInfo: 'Información',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Erreur,',
validationWarning: 'Avertissement,',
validationSuccess: 'Succès,',
validationInfo: 'Info,',
validationError: 'Erreur',
validationWarning: 'Avertissement',
validationSuccess: 'Succès',
validationInfo: 'Info',
};

View file

@ -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ó',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Errore,',
validationWarning: 'Avvertenza,',
validationSuccess: 'Operazione riuscita,',
validationInfo: 'Info,',
validationError: 'Errore',
validationWarning: 'Avvertenza',
validationSuccess: 'Operazione riuscita',
validationInfo: 'Info',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Fout,',
validationWarning: 'Waarschuwing,',
validationSuccess: 'Succes,',
validationInfo: 'Informatie,',
validationError: 'Fout',
validationWarning: 'Waarschuwing',
validationSuccess: 'Succes',
validationInfo: 'Informatie',
};

View file

@ -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',
};

View file

@ -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',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Ошибка,',
validationWarning: 'Предупреждение,',
validationSuccess: 'Успешно,',
validationInfo: 'Информация,',
validationError: 'Ошибка',
validationWarning: 'Предупреждение',
validationSuccess: 'Успешно',
validationInfo: 'Информация',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Chyba,',
validationWarning: 'Varovanie,',
validationSuccess: 'Úspešné,',
validationInfo: 'Info,',
validationError: 'Chyba',
validationWarning: 'Varovanie',
validationSuccess: 'Úspešné',
validationInfo: 'Info',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: 'Помилка,',
validationWarning: 'Попередження,',
validationSuccess: 'Успішно,',
validationInfo: 'Інформація,',
validationError: 'Помилка',
validationWarning: 'Попередження',
validationSuccess: 'Успішно',
validationInfo: 'Інформація',
};

View file

@ -1,6 +1,6 @@
export default {
validationError: '错误',
validationWarning: '警告',
validationSuccess: '成功',
validationInfo: '信息',
validationError: '错误',
validationWarning: '警告',
validationSuccess: '成功',
validationInfo: '信息',
};

View file

@ -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));
}
}

View file

@ -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]');

View file

@ -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;

View file

@ -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('...');
});
});
});

View file

@ -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.',

View file

@ -34,9 +34,6 @@ export default {
eleventyConfig.setUseGitIgnore(false);
eleventyConfig.addPassthroughCopy('CNAME');
},
checkLinks: {
ignoreLinkPatterns: ['**/astro'],
},
absoluteBaseUrl: absoluteBaseUrlNetlify('http://localhost:8080'),
setupUnifiedPlugins: [
adjustPluginOptions(mdjsSetupCode, {