Merge pull request #1263 from ing-bank/chore/add-private-protected-types

Chore/add private protected types
This commit is contained in:
Thijs Louisse 2021-04-01 19:49:43 +02:00 committed by GitHub
commit 2ac36c7f2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 1430 additions and 297 deletions

View file

@ -0,0 +1,36 @@
---
'@lion/accordion': patch
'@lion/ajax': patch
'@lion/button': patch
'@lion/calendar': patch
'@lion/checkbox-group': patch
'@lion/collapsible': patch
'@lion/combobox': patch
'@lion/core': patch
'@lion/dialog': patch
'@lion/fieldset': patch
'@lion/form': patch
'@lion/form-core': patch
'@lion/form-integrations': patch
'@lion/icon': patch
'@lion/input': patch
'@lion/input-amount': patch
'@lion/input-datepicker': patch
'@lion/input-range': patch
'@lion/input-stepper': patch
'@lion/listbox': patch
'@lion/localize': patch
'@lion/overlays': patch
'@lion/pagination': patch
'@lion/progress-indicator': patch
'@lion/select': patch
'@lion/select-rich': patch
'singleton-manager': patch
'@lion/switch': patch
'@lion/tabs': patch
'@lion/textarea': patch
'@lion/tooltip': patch
'@lion/validate-messages': patch
---
add protected and private type info

View file

@ -184,10 +184,16 @@ export class LionAccordion extends LitElement {
super();
this.styles = {};
/** @type {StoreEntry[]} */
/**
* @type {StoreEntry[]}
* @private
*/
this.__store = [];
/** @type {number} */
/**
* @type {number}
* @private
*/
this.__focusedIndex = -1;
/** @type {number[]} */
@ -200,6 +206,9 @@ export class LionAccordion extends LitElement {
this.__setupSlots();
}
/**
* @private
*/
__setupSlots() {
const invokerSlot = this.shadowRoot?.querySelector('slot[name=invoker]');
const handleSlotChange = () => {
@ -213,6 +222,9 @@ export class LionAccordion extends LitElement {
}
}
/**
* @private
*/
__setupStore() {
const invokers = /** @type {HTMLElement[]} */ (Array.from(
this.querySelectorAll('[slot="invoker"]'),
@ -248,6 +260,9 @@ export class LionAccordion extends LitElement {
});
}
/**
* @private
*/
__cleanStore() {
if (!this.__store) {
return;
@ -261,6 +276,7 @@ export class LionAccordion extends LitElement {
/**
*
* @param {number} index
* @private
*/
__createInvokerClickHandler(index) {
return () => {
@ -271,6 +287,7 @@ export class LionAccordion extends LitElement {
/**
* @param {Event} e
* @private
*/
__handleInvokerKeydown(e) {
const _e = /** @type {KeyboardEvent} */ (e);
@ -313,6 +330,9 @@ export class LionAccordion extends LitElement {
return this.__focusedIndex;
}
/**
* @private
*/
get _pairCount() {
return this.__store.length;
}
@ -329,6 +349,9 @@ export class LionAccordion extends LitElement {
return this.__expanded;
}
/**
* @private
*/
__updateFocused() {
if (!(this.__store && this.__store[this.focusedIndex])) {
return;
@ -345,6 +368,9 @@ export class LionAccordion extends LitElement {
}
}
/**
* @private
*/
__updateExpanded() {
if (!this.__store) {
return;
@ -364,6 +390,7 @@ export class LionAccordion extends LitElement {
/**
* @param {number} value
* @private
*/
__toggleExpanded(value) {
const { expanded } = this;

View file

@ -18,7 +18,10 @@ export class AjaxClient {
* @param {Partial<AjaxClientConfig>} config
*/
constructor(config = {}) {
/** @type {Partial<AjaxClientConfig>} */
/**
* @type {Partial<AjaxClientConfig>}
* @private
*/
this.__config = {
addAcceptLanguage: true,
xsrfCookieName: 'XSRF-TOKEN',

View file

@ -17,6 +17,7 @@ class Cache {
/**
* @type {{[url: string]: {expires: number, data: object} }}
* @protected
*/
this._cacheObject = {};
}
@ -92,6 +93,7 @@ class Cache {
* Validate cache on each call to the Cache
* When the expiration date has passed, the _cacheObject will be replaced by an
* empty object
* @protected
*/
_validateCache() {
if (new Date().getTime() > this.expiration) {

View file

@ -11,6 +11,10 @@ import '@lion/core/differentKeyEventNamesShimIE';
const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter';
const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ';
/**
* @typedef {import('@lion/core').TemplateResult} TemplateResult
*/
export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) {
static get properties() {
return {
@ -38,11 +42,21 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
`;
}
/**
*
* @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``;
@ -143,7 +157,10 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
];
}
/** @type {HTMLButtonElement} */
/**
* @type {HTMLButtonElement}
* @protected
*/
get _nativeButtonNode() {
return /** @type {HTMLButtonElement} */ (Array.from(this.children).find(
child => child.slot === '_button',
@ -224,6 +241,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
* 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
* @private
*/
async __clickDelegationHandler(ev) {
// Wait for updateComplete if form is not yet available
@ -249,18 +267,27 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
}
}
/**
* @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
*/
__setupEvents() {
this.addEventListener('mousedown', this.__mousedownHandler);
this.addEventListener('keydown', this.__keydownHandler);
this.addEventListener('keyup', this.__keyupHandler);
}
/**
* @private
*/
__teardownEvents() {
this.removeEventListener('mousedown', this.__mousedownHandler);
this.removeEventListener('keydown', this.__keydownHandler);
@ -268,6 +295,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
this.removeEventListener('click', this.__clickDelegationHandler);
}
/**
* @private
*/
__mousedownHandler() {
this.active = true;
const mouseupHandler = () => {
@ -281,6 +311,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
/**
* @param {KeyboardEvent} e
* @private
*/
__keydownHandler(e) {
if (this.active || !isKeyboardClickEvent(e)) {
@ -309,6 +340,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
/**
* @param {KeyboardEvent} e
* @private
*/
__keyupHandler(e) {
if (isKeyboardClickEvent(e)) {
@ -324,6 +356,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
/**
* Prevents that someone who listens outside or on form catches the click event
* @param {Event} e
* @private
*/
__preventEventLeakage(e) {
if (e.target === this.__submitAndResetHelperButton) {
@ -331,6 +364,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
}
}
/**
* @private
*/
__setupSubmitAndResetHelperOnConnected() {
this._form = this._nativeButtonNode.form;
@ -339,6 +375,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
}
}
/**
* @private
*/
__teardownSubmitAndResetHelperOnDisconnected() {
if (this._form) {
this._form.removeEventListener('click', this.__preventEventLeakage);

View file

@ -9,6 +9,16 @@ import '@lion/button/define';
* @typedef {import('@lion/button/src/LionButton').LionButton} LionButton
*/
/**
* @param {LionButton} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
nativeButtonNode: el._nativeButtonNode,
};
}
describe('lion-button', () => {
it('behaves like native `button` in terms of a11y', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
@ -18,26 +28,32 @@ describe('lion-button', () => {
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(el._nativeButtonNode.type).to.equal('submit');
expect(el._nativeButtonNode.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(el._nativeButtonNode.type).to.equal('button');
expect(el._nativeButtonNode.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>`));
expect(el._nativeButtonNode.getAttribute('tabindex')).to.equal('-1');
expect(window.getComputedStyle(el._nativeButtonNode).clip).to.equal('rect(0px, 0px, 0px, 0px)');
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 () => {
@ -223,8 +239,9 @@ describe('lion-button', () => {
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(el._nativeButtonNode.getAttribute('aria-hidden')).to.equal('true');
expect(nativeButtonNode.getAttribute('aria-hidden')).to.equal('true');
});
it('is accessible', async () => {

View file

@ -142,7 +142,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
constructor() {
super();
/** @type {{months: Month[]}} */
/** @type {{months: Month[]}}
* @private
*/
this.__data = { months: [] };
this.minDate = new Date(0);
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
@ -156,17 +158,27 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
this.firstDayOfWeek = 0;
this.weekdayHeaderNotation = 'short';
/** @private */
this.__today = normalizeDateTime(new Date());
/** @type {Date} */
this.centralDate = this.__today;
/** @type {Date | null} */
/**
* @type {Date | null}
* @private
*/
this.__focusedDate = null;
/** @private */
this.__connectedCallbackDone = false;
/** @private */
this.__eventsAdded = false;
this.locale = '';
/** @private */
this.__boundKeyboardNavigationEvent = this.__keyboardNavigationEvent.bind(this);
/** @private */
this.__boundClickDateDelegation = this.__clickDateDelegation.bind(this);
/** @private */
this.__boundFocusDateDelegation = this.__focusDateDelegation.bind(this);
/** @private */
this.__boundBlurDateDelegation = this.__focusDateDelegation.bind(this);
}
@ -312,6 +324,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
}
/**
* @private
*/
__calculateInitialCentralDate() {
if (this.centralDate === this.__today && this.selectedDate) {
// initialised with selectedDate only if user didn't provide another one
@ -324,6 +339,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {string} month
* @param {number} year
* @private
*/
__renderMonthNavigation(month, year) {
const nextMonth =
@ -348,6 +364,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {string} month
* @param {number} year
* @private
*/
__renderYearNavigation(month, year) {
const nextYear = year + 1;
@ -362,6 +379,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
`;
}
/**
* @private
*/
__renderNavigation() {
const month = getMonthNames({ locale: this.__getLocale() })[this.centralDate.getMonth()];
const year = this.centralDate.getFullYear();
@ -372,6 +392,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
`;
}
/**
* @private
*/
__renderData() {
return dataTemplate(this.__data, {
monthsLabels: getMonthNames({ locale: this.__getLocale() }),
@ -393,6 +416,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} type
* @param {string} previousMonth
* @param {number} previousYear
* @private
*/
__getPreviousDisabled(type, previousMonth, previousYear) {
let disabled;
@ -417,6 +441,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} type
* @param {string} nextMonth
* @param {number} nextYear
* @private
*/
__getNextDisabled(type, nextMonth, nextYear) {
let disabled;
@ -441,6 +466,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} type
* @param {string} previousMonth
* @param {number} previousYear
* @private
*/
__renderPreviousButton(type, previousMonth, previousYear) {
const { disabled, month } = this.__getPreviousDisabled(type, previousMonth, previousYear);
@ -470,6 +496,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} type
* @param {string} nextMonth
* @param {number} nextYear
* @private
*/
__renderNextButton(type, nextMonth, nextYear) {
const { disabled, month } = this.__getNextDisabled(type, nextMonth, nextYear);
@ -501,6 +528,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} type
* @param {string} month
* @param {number} year
* @private
*/
__getNavigationLabel(mode, type, month, year) {
return `${this.msgLit(`lion-calendar:${mode}${type}`)}, ${month} ${year}`;
@ -510,6 +538,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
*
* @param {Day} _day
* @param {*} param1
* @private
*/
__coreDayPreprocessor(_day, { currentMonth = false } = {}) {
const day = createDay(new Date(_day.date), _day);
@ -543,6 +572,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {Day} [options]
* @private
*/
__createData(options) {
const data = createMultipleMonth(this.centralDate, {
@ -564,6 +594,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return data;
}
/**
* @private
*/
__disableDatesChanged() {
if (this.__connectedCallbackDone) {
this.__ensureValidCentralDate();
@ -572,6 +605,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {Date} selectedDate
* @private
*/
__dateSelectedByUser(selectedDate) {
this.selectedDate = selectedDate;
@ -585,18 +619,27 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
);
}
/**
* @private
*/
__centralDateChanged() {
if (this.__connectedCallbackDone) {
this.__ensureValidCentralDate();
}
}
/**
* @private
*/
__focusedDateChanged() {
if (this.__focusedDate) {
this.centralDate = this.__focusedDate;
}
}
/**
* @private
*/
__ensureValidCentralDate() {
if (!this.__isEnabledDate(this.centralDate)) {
this.centralDate = this.__findBestEnabledDateFor(this.centralDate);
@ -605,6 +648,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {Date} date
* @private
*/
__isEnabledDate(date) {
const processedDay = this.__coreDayPreprocessor({ date });
@ -615,6 +659,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {Date} date
* @param {Object} opts
* @param {String} [opts.mode] Find best date in `future/past/both`
* @private
*/
__findBestEnabledDateFor(date, { mode = 'both' } = {}) {
const futureDate =
@ -655,6 +700,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {Event} ev
* @private
*/
__clickDateDelegation(ev) {
const isDayButton = /** @param {HTMLElement} el */ el =>
@ -666,6 +712,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
}
/**
* @private
*/
__focusDateDelegation() {
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
@ -679,6 +728,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}
}
/**
* @private
*/
__blurDateDelegation() {
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
@ -695,6 +747,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
/**
* @param {KeyboardEvent} ev
* @private
*/
__keyboardNavigationEvent(ev) {
const preventedKeys = ['ArrowUp', 'ArrowDown', 'PageDown', 'PageUp'];
@ -744,6 +797,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {string} opts.dateType
* @param {string} opts.type
* @param {string} opts.mode
* @private
*/
__modifyDate(modify, { dateType, type, mode }) {
let tmpDate = new Date(this.centralDate);
@ -765,6 +819,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
this[dateType] = tmpDate;
}
/**
* @private
*/
__getLocale() {
return this.locale || localize.locale;
}

View file

@ -34,10 +34,16 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
};
}
/**
* @protected
*/
get _checkboxGroupNode() {
return /** @type LionCheckboxGroup */ (this._parentFormGroup);
}
/**
* @protected
*/
get _subCheckboxes() {
let checkboxes = [];
if (
@ -52,6 +58,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
return /** @type LionCheckbox[] */ (checkboxes);
}
/**
* @protected
*/
_setOwnCheckedState() {
const subCheckboxes = this._subCheckboxes;
if (!subCheckboxes.length) {
@ -82,6 +91,7 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
/**
* @param {Event} ev
* @private
*/
__onModelValueChanged(ev) {
if (this.disabled) {
@ -98,6 +108,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
this._setOwnCheckedState();
}
/**
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_afterTemplate() {
return html`
@ -107,6 +120,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
`;
}
/**
* @protected
*/
_onRequestToAddFormElement() {
this._setOwnCheckedState();
}

View file

@ -6,6 +6,16 @@ import '@lion/checkbox-group/define';
* @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
*/
/**
* @param {LionCheckboxIndeterminate} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
subCheckboxes: el._subCheckboxes,
};
}
describe('<lion-checkbox-indeterminate>', () => {
it('should have type = checkbox', async () => {
// Arrange
@ -93,8 +103,10 @@ describe('<lion-checkbox-indeterminate>', () => {
'lion-checkbox-indeterminate',
));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Act
elIndeterminate._subCheckboxes[0].checked = true;
subCheckboxes[0].checked = true;
await el.updateComplete;
// Assert
@ -115,11 +127,12 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Act
elIndeterminate._subCheckboxes[0].checked = true;
elIndeterminate._subCheckboxes[1].checked = true;
elIndeterminate._subCheckboxes[2].checked = true;
subCheckboxes[0].checked = true;
subCheckboxes[1].checked = true;
subCheckboxes[2].checked = true;
await el.updateComplete;
// Assert
@ -145,12 +158,13 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act
elIndeterminate._inputNode.click();
await elIndeterminate.updateComplete;
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
});
it('should sync all children when parent is checked (from unchecked to checked)', async () => {
@ -171,12 +185,13 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act
elIndeterminate._inputNode.click();
await elIndeterminate.updateComplete;
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
});
it('should sync all children when parent is checked (from checked to unchecked)', async () => {
@ -197,12 +212,13 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act
elIndeterminate._inputNode.click();
await elIndeterminate.updateComplete;
const elProts = getProtectedMembers(elIndeterminate);
// Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.false;
expect(elProts.subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elProts.subCheckboxes[1].hasAttribute('checked')).to.be.false;
expect(elProts.subCheckboxes[2].hasAttribute('checked')).to.be.false;
});
it('should work as expected with siblings checkbox-indeterminate', async () => {
@ -251,13 +267,18 @@ describe('<lion-checkbox-indeterminate>', () => {
await elFirstIndeterminate.updateComplete;
await elSecondIndeterminate.updateComplete;
const elFirstSubCheckboxes = getProtectedMembers(elFirstIndeterminate);
const elSecondSubCheckboxes = getProtectedMembers(elSecondIndeterminate);
// Assert - the second sibling should not be affected
expect(elFirstIndeterminate?.hasAttribute('indeterminate')).to.be.false;
expect(elFirstIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elFirstIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elFirstIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(elSecondIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elSecondIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
expect(elFirstIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(elFirstSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elFirstSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elFirstSubCheckboxes.subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(elSecondSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elSecondSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.false;
});
it('should work as expected with nested indeterminate checkboxes', async () => {
@ -301,9 +322,13 @@ describe('<lion-checkbox-indeterminate>', () => {
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#parent-checkbox-indeterminate',
));
const elNestedSubCheckboxes = getProtectedMembers(elNestedIndeterminate);
const elParentSubCheckboxes = getProtectedMembers(elParentIndeterminate);
// Act - check a nested checkbox
elNestedIndeterminate?._subCheckboxes[0]._inputNode.click();
if (elNestedIndeterminate) {
elNestedSubCheckboxes.subCheckboxes[0]._inputNode.click();
}
await el.updateComplete;
// Assert
@ -311,8 +336,8 @@ describe('<lion-checkbox-indeterminate>', () => {
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
// Act - check all nested checkbox
elNestedIndeterminate?._subCheckboxes[1]._inputNode.click();
elNestedIndeterminate?._subCheckboxes[2]._inputNode.click();
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[1]._inputNode.click();
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[2]._inputNode.click();
await el.updateComplete;
// Assert
@ -322,8 +347,12 @@ describe('<lion-checkbox-indeterminate>', () => {
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
// Act - finally check all remaining checkbox
elParentIndeterminate?._subCheckboxes[0]._inputNode.click();
elParentIndeterminate?._subCheckboxes[1]._inputNode.click();
if (elParentIndeterminate) {
elParentSubCheckboxes.subCheckboxes[0]._inputNode.click();
}
if (elParentIndeterminate) {
elParentSubCheckboxes.subCheckboxes[1]._inputNode.click();
}
await el.updateComplete;
// Assert
@ -354,11 +383,12 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate',
));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Act
elIndeterminate._subCheckboxes[0].checked = true;
elIndeterminate._subCheckboxes[1].checked = true;
elIndeterminate._subCheckboxes[2].checked = true;
subCheckboxes[0].checked = true;
subCheckboxes[1].checked = true;
subCheckboxes[2].checked = true;
await el.updateComplete;
// Assert

View file

@ -127,18 +127,27 @@ export class LionCollapsible extends LitElement {
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async _hideAnimation(opts) {}
/**
* @protected
*/
get _invokerNode() {
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
child => child.slot === 'invoker',
);
}
/**
* @protected
*/
get _contentNode() {
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
child => child.slot === 'content',
);
}
/**
* @protected
*/
get _contentHeight() {
const size = this._contentNode?.getBoundingClientRect().height || 0;
return `${size}px`;

View file

@ -33,6 +33,16 @@ const collapsibleWithEvents = html`
</lion-collapsible>
`;
/**
* @param {LionCollapsible} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
contentHeight: el._contentHeight,
};
}
describe('<lion-collapsible>', () => {
describe('Collapsible', () => {
it('sets opened to false by default', async () => {
@ -49,10 +59,12 @@ describe('<lion-collapsible>', () => {
it('should return content node height before and after collapsing', async () => {
const collapsible = await fixture(defaultCollapsible);
expect(collapsible._contentHeight).to.equal('0px');
const collHeight1 = getProtectedMembers(collapsible);
expect(collHeight1.contentHeight).to.equal('0px');
collapsible.show();
await collapsible.requestUpdate();
expect(collapsible._contentHeight).to.equal('32px');
const collHeight2 = getProtectedMembers(collapsible);
expect(collHeight2.contentHeight).to.equal('32px');
});
});

View file

@ -71,6 +71,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance FormControlMixin - add slot[name=selection-display]
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate() {
@ -83,6 +84,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
`;
}
/**
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_overlayListboxTemplate() {
// TODO: Localize the aria-label
@ -96,6 +100,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance FormControlMixin - add overlay
* @protected
*/
_groupTwoTemplate() {
return html` ${super._groupTwoTemplate()} ${this._overlayListboxTemplate()}`;
@ -157,6 +162,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* Wrapper with combobox role for the text input that the end user controls the listbox with.
* @type {HTMLElement}
* @protected
*/
get _comboboxNode() {
return /** @type {HTMLElement} */ (this.querySelector('[slot="input"]'));
@ -164,6 +170,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @type {SelectionDisplay | null}
* @protected
*/
get _selectionDisplayNode() {
return this.querySelector('[slot="selection-display"]');
@ -173,6 +180,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @configure FormControlMixin
* Will tell FormControlMixin that a11y wrt labels / descriptions / feedback
* should be applied here.
* @protected
*/
get _inputNode() {
if (this._ariaVersion === '1.1') {
@ -183,6 +191,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @configure OverlayMixin
* @protected
*/
get _overlayContentNode() {
return this._listboxNode;
@ -190,6 +199,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @configure OverlayMixin
* @protected
*/
get _overlayReferenceNode() {
return /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('.input-group__container');
@ -197,6 +207,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @configure OverlayMixin
* @protected
*/
get _overlayInvokerNode() {
return this._inputNode;
@ -204,6 +215,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @configure ListboxMixin
* @protected
*/
get _listboxNode() {
return /** @type {LionOptions} */ ((this._overlayCtrl && this._overlayCtrl.contentNode) ||
@ -212,6 +224,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @configure ListboxMixin
* @protected
*/
get _activeDescendantOwnerNode() {
return this._inputNode;
@ -253,22 +266,36 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* For optimal support, we allow aria v1.1 on newer browsers
* @type {'1.1'|'1.0'}
* @protected
*/
this._ariaVersion = browserDetection.isChromium ? '1.1' : '1.0';
/**
* @configure ListboxMixin
* @protected
*/
this._listboxReceivesNoFocus = true;
/**
* @private
*/
this.__prevCboxValueNonSelected = '';
/**
* @private
*/
this.__prevCboxValue = '';
/** @type {EventListener} */
/** @type {EventListener}
* @private
*/
this.__requestShowOverlay = this.__requestShowOverlay.bind(this);
/** @type {EventListener} */
/** @type {EventListener}
* @protected
*/
this._textboxOnInput = this._textboxOnInput.bind(this);
/** @type {EventListener} */
/** @type {EventListener}
* @protected
*/
this._textboxOnKeydown = this._textboxOnKeydown.bind(this);
}
@ -387,6 +414,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* }
*
* @param {{ currentValue: string, lastKey:string }} options
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_showOverlayCondition({ lastKey }) {
@ -403,6 +431,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @param {Event} ev
* @protected
*/
// eslint-disable-next-line no-unused-vars
_textboxOnInput(ev) {
@ -412,6 +441,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @param {KeyboardEvent} ev
* @protected
*/
_textboxOnKeydown(ev) {
if (ev.key === 'Tab') {
@ -421,6 +451,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @param {MouseEvent} ev
* @protected
*/
_listboxOnClick(ev) {
super._listboxOnClick(ev);
@ -433,6 +464,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @param {string} v
* @protected
*/
_setTextboxValue(v) {
// Make sure that we don't loose inputNode.selectionStart and inputNode.selectionEnd
@ -441,6 +473,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
}
/**
* @private
*/
__onOverlayClose() {
if (!this.multipleChoice) {
if (
@ -471,6 +506,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* model-value-changed event that gets received, and we should repropagate it.
*
* @param {EventTarget & import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} target
* @protected
*/
_repropagationCondition(target) {
return super._repropagationCondition(target) || this.formElements.every(el => !el.checked);
@ -481,6 +517,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @overridable
* @param {LionOption & {__originalInnerHTML?:string}} option
* @param {string} matchingString
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_onFilterMatch(option, matchingString) {
@ -498,6 +535,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @param {LionOption & {__originalInnerHTML?:string}} option
* @param {string} [curValue]
* @param {string} [prevValue]
* @protected
*/
// eslint-disable-next-line no-unused-vars, class-methods-use-this
_onFilterUnmatch(option, curValue, prevValue) {
@ -512,6 +550,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* Computes whether a user intends to autofill (inline autocomplete textbox)
* @param {{ prevValue:string, curValue:string }} config
* @private
*/
// eslint-disable-next-line class-methods-use-this
__computeUserIntendsAutoFill({ prevValue, curValue }) {
@ -535,6 +574,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* - complete: completes the textbox value inline (the 'missing characters' will be added as
* selected text)
*
* @protected
*/
_handleAutocompletion() {
// TODO: this is captured by 'noFilter'
@ -660,6 +700,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
}
/**
* @private
*/
__textboxInlineComplete(option = this.formElements[this.activeIndex]) {
const prevLen = this._inputNode.value.length;
this._inputNode.value = option.choiceValue;
@ -672,6 +715,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* option from the list (by default when autocomplete is 'none' or 'list').
* For autocomplete 'both' or 'inline', it will automatically select on a match.
* @overridable
* @protected
*/
_autoSelectCondition() {
return this.autocomplete === 'both' || this.autocomplete === 'inline';
@ -679,6 +723,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance ListboxMixin
* @protected
*/
_setupListboxNode() {
super._setupListboxNode();
@ -688,6 +733,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @configure OverlayMixin
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
@ -699,6 +745,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance OverlayMixin
* @protected
*/
_setupOverlayCtrl() {
super._setupOverlayCtrl();
@ -708,6 +755,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance OverlayMixin
* @protected
*/
_setupOpenCloseListeners() {
super._setupOpenCloseListeners();
@ -716,6 +764,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance OverlayMixin
* @protected
*/
_teardownOpenCloseListeners() {
super._teardownOpenCloseListeners();
@ -725,6 +774,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @enhance ListboxMixin
* @param {KeyboardEvent} ev
* @protected
*/
_listboxOnKeyDown(ev) {
super._listboxOnKeyDown(ev);
@ -750,6 +800,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @overridable
* @param {string|string[]} modelValue
* @param {string|string[]} oldModelValue
* @protected
*/
// eslint-disable-next-line no-unused-vars
_syncToTextboxCondition(modelValue, oldModelValue, { phase } = {}) {
@ -763,6 +814,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* Allows to control what happens when checkedIndexes change
* @param {string[]} modelValue
* @param {string[]} oldModelValue
* @protected
*/
_syncToTextboxMultiple(modelValue, oldModelValue = []) {
const diff = modelValue.filter(x => !oldModelValue.includes(x));
@ -771,6 +823,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @override FormControlMixin - add form-control to [slot=input] instead of _inputNode
* @protected
*/
_enhanceLightDomClasses() {
if (this.querySelector('[slot=input]')) {
@ -778,10 +831,16 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
}
/**
* @private
*/
__initFilterListbox() {
this._handleAutocompletion();
}
/**
* @private
*/
__setComboboxDisabledAndReadOnly() {
if (this._comboboxNode) {
this._comboboxNode.setAttribute('disabled', `${this.disabled}`);
@ -789,6 +848,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
}
}
/**
* @private
*/
__setupCombobox() {
// With regard to accessibility: aria-expanded and -labelledby will
// be handled by OverlayMixin and FormControlMixin respectively.
@ -811,6 +873,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
this._inputNode.addEventListener('keydown', this._textboxOnKeydown);
}
/**
* @private
*/
__teardownCombobox() {
this._inputNode.removeEventListener('keydown', this._listboxOnKeyDown);
this._inputNode.removeEventListener('input', this._textboxOnInput);
@ -819,6 +884,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
/**
* @param {KeyboardEvent} [ev]
* @private
*/
__requestShowOverlay(ev) {
this.opened = this._showOverlayCondition({

View file

@ -11,17 +11,42 @@ import { LionCombobox } from '../src/LionCombobox.js';
* @typedef {import('../types/SelectionDisplay').SelectionDisplay} SelectionDisplay
*/
/**
* @param {LionCombobox} el
*/
function getProtectedMembers(el) {
// @ts-ignore
const {
_comboboxNode: comboboxNode,
_inputNode: inputNode,
_listboxNode: listboxNode,
_selectionDisplayNode: selectionDisplayNode,
_activeDescendantOwnerNode: activeDescendantOwnerNode,
_ariaVersion: ariaVersion,
} = el;
return {
comboboxNode,
inputNode,
listboxNode,
selectionDisplayNode,
activeDescendantOwnerNode,
ariaVersion,
};
}
/**
* @param {LionCombobox} el
* @param {string} value
*/
function mimicUserTyping(el, value) {
el._inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
const { inputNode } = getProtectedMembers(el);
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
// eslint-disable-next-line no-param-reassign
el._inputNode.value = value;
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
el._inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
inputNode.value = value;
inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
}
/**
@ -38,35 +63,49 @@ function mimicKeyPress(el, key) {
* @param {string[]} values
*/
async function mimicUserTypingAdvanced(el, values) {
const inputNode = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (el._inputNode);
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
const { inputNode } = getProtectedMembers(el);
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (inputNode);
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
for (const key of values) {
// eslint-disable-next-line no-await-in-loop, no-loop-func
await new Promise(resolve => {
const hasSelection = inputNode.selectionStart !== inputNode.selectionEnd;
const hasSelection = inputNodeLoc.selectionStart !== inputNodeLoc.selectionEnd;
if (key === 'Backspace') {
if (hasSelection) {
inputNode.value =
inputNode.value.slice(0, inputNode.selectionStart) +
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
inputNodeLoc.value =
inputNodeLoc.value.slice(
0,
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
) +
inputNodeLoc.value.slice(
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
inputNodeLoc.value.length,
);
} else {
inputNode.value = inputNode.value.slice(0, -1);
inputNodeLoc.value = inputNodeLoc.value.slice(0, -1);
}
} else if (hasSelection) {
inputNode.value =
inputNode.value.slice(0, inputNode.selectionStart) +
inputNodeLoc.value =
inputNodeLoc.value.slice(
0,
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
) +
key +
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
inputNodeLoc.value.slice(
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
inputNodeLoc.value.length,
);
} else {
inputNode.value += key;
inputNodeLoc.value += key;
}
mimicKeyPress(inputNode, key);
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
mimicKeyPress(inputNodeLoc, key);
inputNodeLoc.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
el.updateComplete.then(() => {
// @ts-ignore
resolve();
});
});
@ -187,9 +226,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
expect(el.opened).to.be.false;
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
await el.updateComplete;
expect(el.opened).to.be.true;
});
@ -204,9 +244,11 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
expect(el._listboxNode).to.exist;
expect(el._listboxNode).to.be.instanceOf(LionOptions);
expect(el.querySelector('[role=listbox]')).to.equal(el._listboxNode);
const { listboxNode } = getProtectedMembers(el);
expect(listboxNode).to.exist;
expect(listboxNode).to.be.instanceOf(LionOptions);
expect(el.querySelector('[role=listbox]')).to.equal(listboxNode);
});
it('has a textbox element', async () => {
@ -216,8 +258,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
expect(el._comboboxNode).to.exist;
expect(el.querySelector('[role=combobox]')).to.equal(el._comboboxNode);
const { comboboxNode } = getProtectedMembers(el);
expect(comboboxNode).to.exist;
expect(el.querySelector('[role=combobox]')).to.equal(comboboxNode);
});
});
@ -229,11 +273,13 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
expect(el._inputNode.value).to.equal('10');
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('10');
el.modelValue = '20';
await el.updateComplete;
expect(el._inputNode.value).to.equal('20');
expect(inputNode.value).to.equal('20');
});
it('sets modelValue to empty string if no option is selected', async () => {
@ -283,9 +329,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
expect(el.opened).to.equal(false);
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
await el.updateComplete;
expect(el.opened).to.equal(false);
});
@ -308,10 +355,12 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const options = el.formElements;
const { inputNode } = getProtectedMembers(el);
expect(el.opened).to.equal(false);
// step [1]
el._inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
await el.updateComplete;
expect(el.opened).to.equal(false);
@ -324,7 +373,7 @@ describe('lion-combobox', () => {
options[0].click();
await el.updateComplete;
expect(el.opened).to.equal(false);
expect(document.activeElement).to.equal(el._inputNode);
expect(document.activeElement).to.equal(inputNode);
// step [4]
await el.updateComplete;
@ -343,17 +392,19 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode, inputNode } = getProtectedMembers(el);
// open
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
expect(el.opened).to.equal(true);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(el.opened).to.equal(false);
expect(el._inputNode.value).to.equal('');
expect(inputNode.value).to.equal('');
});
it('hides overlay on [Tab]', async () => {
@ -366,17 +417,19 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode, inputNode } = getProtectedMembers(el);
// open
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
expect(el.opened).to.equal(true);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
mimicKeyPress(el._inputNode, 'Tab');
mimicKeyPress(inputNode, 'Tab');
expect(el.opened).to.equal(false);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
});
it('clears checkedIndex on empty text', async () => {
@ -389,13 +442,15 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode, inputNode } = getProtectedMembers(el);
// open
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
expect(el.opened).to.equal(true);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
expect(el.checkedIndex).to.equal(0);
mimicUserTyping(el, '');
@ -425,9 +480,10 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</${tag}>
`));
const { comboboxNode } = getProtectedMembers(el);
expect(el.opened).to.equal(false);
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
await el.updateComplete;
expect(el.opened).to.equal(true);
});
@ -517,9 +573,10 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const options = el.formElements;
const { comboboxNode } = getProtectedMembers(el);
expect(el.opened).to.equal(false);
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
await el.updateComplete;
@ -557,9 +614,10 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const options = el.formElements;
const { comboboxNode } = getProtectedMembers(el);
expect(el.opened).to.equal(false);
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'art');
expect(el.opened).to.equal(true);
@ -584,14 +642,15 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
expect(el.checkedIndex).to.equal(0);
// Simulate backspace deleting the char at the end of the string
mimicKeyPress(el._inputNode, 'Backspace');
el._inputNode.dispatchEvent(new Event('input'));
const arr = el._inputNode.value.split('');
arr.splice(el._inputNode.value.length - 1, 1);
el._inputNode.value = arr.join('');
mimicKeyPress(inputNode, 'Backspace');
inputNode.dispatchEvent(new Event('input'));
const arr = inputNode.value.split('');
arr.splice(inputNode.value.length - 1, 1);
inputNode.value = arr.join('');
await el.updateComplete;
el.dispatchEvent(new Event('blur'));
@ -614,7 +673,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
expect(el._comboboxNode.getAttribute('role')).to.equal('combobox');
const { comboboxNode } = getProtectedMembers(el);
expect(comboboxNode.getAttribute('role')).to.equal('combobox');
});
it('makes sure listbox node is not focusable', async () => {
@ -624,7 +685,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
</lion-combobox>
`));
expect(el._listboxNode.hasAttribute('tabindex')).to.be.false;
const { listboxNode } = getProtectedMembers(el);
expect(listboxNode.hasAttribute('tabindex')).to.be.false;
});
});
});
@ -653,7 +716,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
expect(el._selectionDisplayNode).to.equal(el.querySelector('[slot=selection-display]'));
const { selectionDisplayNode } = getProtectedMembers(el);
expect(selectionDisplayNode).to.equal(el.querySelector('[slot=selection-display]'));
});
it('sets a reference to combobox element in _selectionDisplayNode', async () => {
@ -781,14 +846,16 @@ describe('lion-combobox', () => {
`));
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el._inputNode.value).to.equal('Chard');
expect(el._inputNode.selectionStart).to.equal(2);
expect(el._inputNode.selectionEnd).to.equal(el._inputNode.value.length);
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('Chard');
expect(inputNode.selectionStart).to.equal(2);
expect(inputNode.selectionEnd).to.equal(inputNode.value.length);
// We don't autocomplete when characters are removed
mimicUserTyping(el, 'c'); // The user pressed backspace (number of chars decreased)
expect(el._inputNode.value).to.equal('c');
expect(el._inputNode.selectionStart).to.equal(el._inputNode.value.length);
expect(inputNode.value).to.equal('c');
expect(inputNode.selectionStart).to.equal(inputNode.value.length);
});
it('filters options when autocomplete is "list"', async () => {
@ -800,10 +867,12 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(getFilteredOptionValues(el)).to.eql(['Artichoke', 'Chard', 'Chicory']);
expect(el._inputNode.value).to.equal('ch');
expect(inputNode.value).to.equal('ch');
});
it('does not filter options when autocomplete is "none"', async () => {
@ -891,25 +960,26 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el._inputNode.value).to.equal('Chard');
expect(el._inputNode.selectionStart).to.equal('ch'.length);
expect(el._inputNode.selectionEnd).to.equal('Chard'.length);
expect(inputNode.value).to.equal('Chard');
expect(inputNode.selectionStart).to.equal('ch'.length);
expect(inputNode.selectionEnd).to.equal('Chard'.length);
await mimicUserTypingAdvanced(el, ['i', 'c']);
await el.updateComplete;
expect(el._inputNode.value).to.equal('Chicory');
expect(el._inputNode.selectionStart).to.equal('chic'.length);
expect(el._inputNode.selectionEnd).to.equal('Chicory'.length);
expect(inputNode.value).to.equal('Chicory');
expect(inputNode.selectionStart).to.equal('chic'.length);
expect(inputNode.selectionEnd).to.equal('Chicory'.length);
// Diminishing chars, but autocompleting
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el._inputNode.value).to.equal('ch');
expect(el._inputNode.selectionStart).to.equal('ch'.length);
expect(el._inputNode.selectionEnd).to.equal('ch'.length);
expect(inputNode.value).to.equal('ch');
expect(inputNode.selectionStart).to.equal('ch'.length);
expect(inputNode.selectionEnd).to.equal('ch'.length);
});
it('synchronizes textbox on overlay close', async () => {
@ -921,7 +991,8 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
expect(el._inputNode.value).to.equal('');
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('');
/**
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
@ -937,7 +1008,7 @@ describe('lion-combobox', () => {
el.setCheckedIndex(index);
el.opened = false;
await el.updateComplete;
expect(el._inputNode.value).to.equal(valueOnClose);
expect(inputNode.value).to.equal(valueOnClose);
}
await performChecks('none', 0, 'Artichoke');
@ -961,7 +1032,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
expect(el._inputNode.value).to.equal('');
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('');
/**
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
@ -977,7 +1050,7 @@ describe('lion-combobox', () => {
el.setCheckedIndex(index);
el.opened = false;
await el.updateComplete;
expect(el._inputNode.value).to.equal(valueOnClose);
expect(inputNode.value).to.equal(valueOnClose);
}
await performChecks('none', 0, '');
@ -1054,20 +1127,21 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
mimicUserTyping(el, 'ch');
await el.updateComplete;
expect(el._inputNode.value).to.equal('Chard');
expect(el._inputNode.selectionStart).to.equal('Ch'.length);
expect(el._inputNode.selectionEnd).to.equal('Chard'.length);
expect(inputNode.value).to.equal('Chard');
expect(inputNode.selectionStart).to.equal('Ch'.length);
expect(inputNode.selectionEnd).to.equal('Chard'.length);
// Autocompletion happened. When we go backwards ('Ch[ard]' => 'Ch'), we should not
// autocomplete to 'Chard' anymore.
await mimicUserTypingAdvanced(el, ['Backspace']);
await el.updateComplete;
expect(el._inputNode.value).to.equal('Ch'); // so not 'Chard'
expect(el._inputNode.selectionStart).to.equal('Ch'.length);
expect(el._inputNode.selectionEnd).to.equal('Ch'.length);
expect(inputNode.value).to.equal('Ch'); // so not 'Chard'
expect(inputNode.selectionStart).to.equal('Ch'.length);
expect(inputNode.selectionEnd).to.equal('Ch'.length);
});
describe('Subclassers', () => {
@ -1133,27 +1207,29 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
expect(el._inputNode.value).to.equal('');
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'none';
el.setCheckedIndex(0);
expect(el._inputNode.value).to.equal('');
expect(inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'list';
el.setCheckedIndex(0);
expect(el._inputNode.value).to.equal('');
expect(inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'inline';
el.setCheckedIndex(0);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
el.setCheckedIndex(-1);
el.autocomplete = 'both';
el.setCheckedIndex(0);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
});
it('synchronizes last index to textbox when autocomplete is "inline" or "both" when multipleChoice', async () => {
@ -1165,33 +1241,35 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
expect(el._inputNode.value).to.eql('');
const { inputNode } = getProtectedMembers(el);
expect(inputNode.value).to.eql('');
el.setCheckedIndex(-1);
el.autocomplete = 'none';
el.setCheckedIndex([0]);
el.setCheckedIndex([1]);
expect(el._inputNode.value).to.equal('');
expect(inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'list';
el.setCheckedIndex([0]);
el.setCheckedIndex([1]);
expect(el._inputNode.value).to.equal('');
expect(inputNode.value).to.equal('');
el.setCheckedIndex(-1);
el.autocomplete = 'inline';
el.setCheckedIndex([0]);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
el.setCheckedIndex([1]);
expect(el._inputNode.value).to.equal('Chard');
expect(inputNode.value).to.equal('Chard');
el.setCheckedIndex(-1);
el.autocomplete = 'both';
el.setCheckedIndex([0]);
expect(el._inputNode.value).to.equal('Artichoke');
expect(inputNode.value).to.equal('Artichoke');
el.setCheckedIndex([1]);
expect(el._inputNode.value).to.equal('Chard');
expect(inputNode.value).to.equal('Chard');
});
describe('Subclassers', () => {
@ -1224,26 +1302,27 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</${tag}>
`));
const { inputNode } = getProtectedMembers(el);
el.setCheckedIndex(-1);
el.autocomplete = 'none';
el.setCheckedIndex([0]);
expect(el._inputNode.value).to.equal('Artichoke--multi');
expect(inputNode.value).to.equal('Artichoke--multi');
el.setCheckedIndex(-1);
el.autocomplete = 'list';
el.setCheckedIndex([0]);
expect(el._inputNode.value).to.equal('Artichoke--multi');
expect(inputNode.value).to.equal('Artichoke--multi');
el.setCheckedIndex(-1);
el.autocomplete = 'inline';
el.setCheckedIndex([0]);
expect(el._inputNode.value).to.equal('Artichoke--multi');
expect(inputNode.value).to.equal('Artichoke--multi');
el.setCheckedIndex(-1);
el.autocomplete = 'both';
el.setCheckedIndex([0]);
expect(el._inputNode.value).to.equal('Artichoke--multi');
expect(inputNode.value).to.equal('Artichoke--multi');
});
});
@ -1277,6 +1356,7 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
/**
* @param {LionCombobox} elm
@ -1304,7 +1384,7 @@ describe('lion-combobox', () => {
expect(el.activeIndex).to.equal(-1);
expect(el.opened).to.be.true;
mimicKeyPress(el._inputNode, 'Enter');
mimicKeyPress(inputNode, 'Enter');
expect(el.opened).to.be.false;
expect(el.activeIndex).to.equal(-1);
@ -1318,7 +1398,7 @@ describe('lion-combobox', () => {
expect(el.opened).to.be.true;
expect(el.activeIndex).to.equal(-1);
mimicKeyPress(el._inputNode, 'Enter');
mimicKeyPress(inputNode, 'Enter');
expect(el.activeIndex).to.equal(-1);
expect(el.opened).to.be.false;
@ -1335,7 +1415,7 @@ describe('lion-combobox', () => {
expect(el.activeIndex).to.equal(1);
mimicKeyPress(el._inputNode, 'Enter');
mimicKeyPress(inputNode, 'Enter');
await el.updateComplete;
await el.updateComplete;
@ -1349,7 +1429,7 @@ describe('lion-combobox', () => {
await el.updateComplete;
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
await el.updateComplete;
mimicKeyPress(el._inputNode, 'Enter');
mimicKeyPress(inputNode, 'Enter');
expect(el.activeIndex).to.equal(1);
expect(el.opened).to.be.false;
});
@ -1363,6 +1443,8 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
await el.updateComplete;
expect(el.activeIndex).to.equal(1);
@ -1378,7 +1460,7 @@ describe('lion-combobox', () => {
// select artichoke
mimicUserTyping(/** @type {LionCombobox} */ (el), 'artichoke');
await el.updateComplete;
mimicKeyPress(el._inputNode, 'Enter');
mimicKeyPress(inputNode, 'Enter');
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
await el.updateComplete;
@ -1397,15 +1479,17 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
const { inputNode } = getProtectedMembers(el);
// Select something
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
await el.updateComplete;
mimicKeyPress(el._inputNode, 'Enter');
mimicKeyPress(inputNode, 'Enter');
expect(el.activeIndex).to.equal(1);
mimicKeyPress(el._inputNode, 'Escape');
mimicKeyPress(inputNode, 'Escape');
await el.updateComplete;
expect(el._inputNode.textContent).to.equal('');
expect(inputNode.textContent).to.equal('');
el.formElements.forEach(option => expect(option.active).to.be.false);
@ -1419,19 +1503,21 @@ describe('lion-combobox', () => {
describe('Accessibility', () => {
it('synchronizes autocomplete option to textbox', async () => {
let el;
[el] = await fruitFixture({ autocomplete: 'both' });
// @ts-expect-error
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('both');
[el] = await fruitFixture({ autocomplete: 'list' });
// @ts-expect-error
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('list');
[el] = await fruitFixture({ autocomplete: 'none' });
// @ts-expect-error
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('none');
});
it('updates aria-activedescendant on textbox node', async () => {
let el = /** @type {LionCombobox} */ (await fixture(html`
const el = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" autocomplete="none">
<lion-option .choiceValue="${'Artichoke'}" id="artichoke-option">Artichoke</lion-option>
<lion-option .choiceValue="${'Chard'}" id="chard-option">Chard</lion-option>
@ -1440,20 +1526,26 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(null);
const elProts = getProtectedMembers(el);
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
null,
);
expect(el.formElements[1].active).to.equal(false);
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el.updateComplete;
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(null);
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
null,
);
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
mimicKeyPress(el._inputNode, 'ArrowDown');
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
mimicKeyPress(elProts.inputNode, 'ArrowDown');
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
'artichoke-option',
);
expect(el.formElements[1].active).to.equal(false);
el = /** @type {LionCombobox} */ (await fixture(html`
const el2 = /** @type {LionCombobox} */ (await fixture(html`
<lion-combobox name="foo" autocomplete="both" match-mode="begin">
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
@ -1461,20 +1553,23 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
</lion-combobox>
`));
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el.updateComplete;
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
el.formElements[1].id,
);
expect(el.formElements[1].active).to.equal(true);
el.autocomplete = 'list';
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el.updateComplete;
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
el.formElements[1].id,
const el2Prots = getProtectedMembers(el2);
mimicUserTyping(/** @type {LionCombobox} */ (el2), 'ch');
await el2.updateComplete;
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
el2.formElements[1].id,
);
expect(el.formElements[1].active).to.equal(true);
expect(el2.formElements[1].active).to.equal(true);
el2.autocomplete = 'list';
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
await el2.updateComplete;
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
el2.formElements[1].id,
);
expect(el2.formElements[1].active).to.equal(true);
});
it('adds aria-label to highlighted options', async () => {
@ -1496,7 +1591,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
expect(el._comboboxNode.contains(el._inputNode)).to.be.true;
const { comboboxNode, inputNode } = getProtectedMembers(el);
expect(comboboxNode.contains(inputNode)).to.be.true;
});
it('has one input node with [role=combobox] in v1.0', async () => {
@ -1505,7 +1602,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
expect(el._comboboxNode).to.equal(el._inputNode);
const { comboboxNode, inputNode } = getProtectedMembers(el);
expect(comboboxNode).to.equal(inputNode);
});
it('autodetects aria version and sets it to 1.1 on Chromium browsers', async () => {
@ -1517,7 +1616,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
expect(el._ariaVersion).to.equal('1.1');
const elProts = getProtectedMembers(el);
expect(elProts.ariaVersion).to.equal('1.1');
browserDetection.isChromium = false;
const el2 = /** @type {LionCombobox} */ (await fixture(html`
@ -1525,7 +1626,9 @@ describe('lion-combobox', () => {
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
</lion-combobox>
`));
expect(el2._ariaVersion).to.equal('1.0');
const el2Prots = getProtectedMembers(el2);
expect(el2Prots.ariaVersion).to.equal('1.0');
// restore...
browserDetection.isChromium = browserDetectionIsChromiumOriginal;
@ -1545,8 +1648,10 @@ describe('lion-combobox', () => {
</lion-combobox>
`));
const { comboboxNode } = getProtectedMembers(el);
// activate opened listbox
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
mimicUserTyping(el, 'ch');
await el.updateComplete;

View file

@ -22,11 +22,18 @@ const DelegateMixinImplementation = superclass =>
constructor() {
super();
/** @type {DelegateEvent[]} */
/**
* @type {DelegateEvent[]}
* @private
*/
this.__eventsQueue = [];
/** @type {Object.<string,?>} */
/**
* @type {Object.<string,?>}
* @private
*/
this.__propertiesQueue = {};
/** @private */
this.__setupPropertyDelegation();
}
@ -101,6 +108,9 @@ const DelegateMixinImplementation = superclass =>
super.removeAttribute(name);
}
/**
* @protected
*/
_connectDelegateMixin() {
if (this.__connectedDelegateMixin) return;
@ -117,6 +127,9 @@ const DelegateMixinImplementation = superclass =>
}
}
/**
* @private
*/
__setupPropertyDelegation() {
const propertyNames = this.delegations.properties.concat(this.delegations.methods);
propertyNames.forEach(propertyName => {
@ -153,6 +166,9 @@ const DelegateMixinImplementation = superclass =>
});
}
/**
* @private
*/
__initialAttributeDelegation() {
const attributeNames = this.delegations.attributes;
attributeNames.forEach(attributeName => {
@ -164,12 +180,18 @@ const DelegateMixinImplementation = superclass =>
});
}
/**
* @private
*/
__emptyEventListenerQueue() {
this.__eventsQueue.forEach(ev => {
this.delegationTarget.addEventListener(ev.type, ev.handler, ev.opts);
});
}
/**
* @private
*/
__emptyPropertiesQueue() {
Object.keys(this.__propertiesQueue).forEach(propName => {
this.delegationTarget[propName] = this.__propertiesQueue[propName];

View file

@ -22,8 +22,11 @@ const DisabledMixinImplementation = superclass =>
constructor() {
super();
/** @protected */
this._requestedToBeDisabled = false;
/** @private */
this.__isUserSettingDisabled = true;
/** @private */
this.__restoreDisabledTo = false;
this.disabled = false;
}
@ -43,7 +46,10 @@ const DisabledMixinImplementation = superclass =>
}
}
/** @param {boolean} value */
/**
* @param {boolean} value
* @private
*/
__internalSetDisabled(value) {
this.__isUserSettingDisabled = false;
this.disabled = value;

View file

@ -27,7 +27,9 @@ const DisabledWithTabIndexMixinImplementation = superclass =>
constructor() {
super();
/** @private */
this.__isUserSettingTabIndex = true;
/** @private */
this.__restoreTabIndexTo = 0;
this.__internalSetTabIndex(0);
}
@ -48,6 +50,7 @@ const DisabledWithTabIndexMixinImplementation = superclass =>
/**
* @param {number} value
* @private
*/
__internalSetTabIndex(value) {
this.__isUserSettingTabIndex = false;

View file

@ -22,6 +22,7 @@ const SlotMixinImplementation = superclass =>
constructor() {
super();
/** @private */
this.__privateSlots = new Set(null);
}
@ -34,6 +35,9 @@ const SlotMixinImplementation = superclass =>
this._connectSlotMixin();
}
/**
* @protected
*/
_connectSlotMixin() {
if (!this.__isConnectedSlotMixin) {
Object.keys(this.slots).forEach(slotName => {
@ -55,6 +59,7 @@ const SlotMixinImplementation = superclass =>
/**
* @param {string} slotName Name of the slot
* @return {boolean} true if given slot name been created by SlotMixin
* @protected
*/
_isPrivateSlot(slotName) {
return this.__privateSlots.has(slotName);

View file

@ -4,11 +4,15 @@ import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
export class LionDialog extends OverlayMixin(LitElement) {
constructor() {
super();
/** @private */
this.__toggle = () => {
this.opened = !this.opened;
};
}
/**
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return {
@ -16,6 +20,9 @@ export class LionDialog extends OverlayMixin(LitElement) {
};
}
/**
* @protected
*/
_setupOpenCloseListeners() {
super._setupOpenCloseListeners();
if (this._overlayInvokerNode) {
@ -23,6 +30,9 @@ export class LionDialog extends OverlayMixin(LitElement) {
}
}
/**
* @protected
*/
_teardownOpenCloseListeners() {
super._teardownOpenCloseListeners();
if (this._overlayInvokerNode) {

View file

@ -22,11 +22,15 @@ import { FormGroupMixin } from '@lion/form-core';
export class LionFieldset extends FormGroupMixin(LitElement) {
constructor() {
super();
/** @override FormRegistrarMixin */
/**
* @override FormRegistrarMixin
* @protected
*/
this._isFormOrFieldset = true;
/**
* @type {'child' | 'choice-group' | 'fieldset'}
* @override FormControlMixin
* @protected
*/
this._repropagationRole = 'fieldset';
}

View file

@ -46,14 +46,23 @@ const FocusMixinImplementation = superclass =>
}
}
/**
* @private
*/
__onFocus() {
this.focused = true;
}
/**
* @private
*/
__onBlur() {
this.focused = false;
}
/**
* @private
*/
__registerEventsForFocusMixin() {
/**
* focus
@ -98,6 +107,9 @@ const FocusMixinImplementation = superclass =>
this._inputNode.addEventListener('focusout', this.__redispatchFocusout);
}
/**
* @private
*/
__teardownEventsForFocusMixin() {
this._inputNode.removeEventListener(
'focus',

View file

@ -274,16 +274,19 @@ const FormControlMixinImplementation = superclass =>
}
}
/** @protected */
_triggerInitialModelValueChangedEvent() {
this.__dispatchInitialModelValueChangedEvent();
}
/** @protected */
_enhanceLightDomClasses() {
if (this._inputNode) {
this._inputNode.classList.add('form-control');
}
}
/** @protected */
_enhanceLightDomA11y() {
const { _inputNode, _labelNode, _helpTextNode, _feedbackNode } = this;
@ -310,6 +313,7 @@ const FormControlMixinImplementation = superclass =>
* When boolean attribute data-label or data-description is found,
* the slot element will be connected to the input via aria-labelledby or aria-describedby
* @param {string[]} additionalSlots
* @protected
*/
_enhanceLightDomA11yForAdditionalSlots(
additionalSlots = ['prefix', 'suffix', 'before', 'after'],
@ -335,6 +339,7 @@ const FormControlMixinImplementation = superclass =>
* @param {string} attrName
* @param {HTMLElement[]} nodes
* @param {boolean|undefined} reorder
* @private
*/
__reflectAriaAttr(attrName, nodes, reorder) {
if (this._inputNode) {
@ -393,6 +398,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
_groupOneTemplate() {
return html` ${this._labelTemplate()} ${this._helpTextTemplate()} `;
@ -400,6 +406,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
_groupTwoTemplate() {
return html` ${this._inputGroupTemplate()} ${this._feedbackTemplate()} `;
@ -407,6 +414,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_labelTemplate() {
@ -419,6 +427,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_helpTextTemplate() {
@ -431,6 +440,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
_inputGroupTemplate() {
return html`
@ -447,6 +457,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupBeforeTemplate() {
@ -459,6 +470,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult | nothing}
* @protected
*/
_inputGroupPrefixTemplate() {
return !Array.from(this.children).find(child => child.slot === 'prefix')
@ -472,6 +484,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate() {
@ -484,6 +497,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult | nothing}
* @protected
*/
_inputGroupSuffixTemplate() {
return !Array.from(this.children).find(child => child.slot === 'suffix')
@ -497,6 +511,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupAfterTemplate() {
@ -509,6 +524,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_feedbackTemplate() {
@ -522,6 +538,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @param {?} modelValue
* @return {boolean}
* @protected
*/
// @ts-ignore FIXME: Move to FormatMixin? Since there we have access to modelValue prop
_isEmpty(modelValue = this.modelValue) {
@ -675,6 +692,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @return {Array.<HTMLElement|undefined>}
* @protected
*/
// Returns dom references to all elements that should be referred to by field(s)
_getAriaDescriptionElements() {
@ -727,6 +745,7 @@ const FormControlMixinImplementation = superclass =>
/**
* @param {string} slotName
* @return {HTMLElement | undefined}
* @private
*/
__getDirectSlotChild(slotName) {
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
@ -734,6 +753,7 @@ const FormControlMixinImplementation = superclass =>
);
}
/** @private */
__dispatchInitialModelValueChangedEvent() {
// When we are not a fieldset / choice-group, we don't need to wait for our children
// to send a unified event
@ -762,12 +782,14 @@ const FormControlMixinImplementation = superclass =>
/**
* @param {CustomEvent} ev
* @protected
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
_onBeforeRepropagateChildrenValues(ev) {}
/**
* @param {CustomEvent} ev
* @private
*/
__repropagateChildrenValues(ev) {
// Allows sub classes to internally listen to the children change events
@ -841,6 +863,7 @@ const FormControlMixinImplementation = superclass =>
* TODO: Extend this in choice group so that target is always a choice input and multipleChoice exists.
* This will fix the types and reduce the need for ignores/expect-errors
* @param {EventTarget & import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} target
* @protected
*/
_repropagationCondition(target) {
return !(
@ -861,6 +884,7 @@ const FormControlMixinImplementation = superclass =>
* _onLabelClick() {
* this._invokerNode.focus();
* }
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_onLabelClick() {}

View file

@ -207,6 +207,7 @@ const FormatMixinImplementation = superclass =>
* @param {{source:'model'|'serialized'|'formatted'|null}} config - the type of value that triggered this method. It should not be
* set again, so that its observer won't be triggered. Can be:
* 'model'|'formatted'|'serialized'.
* @protected
*/
_calculateValues({ source } = { source: null }) {
if (this.__preventRecursiveTrigger) return; // prevent infinite loops
@ -236,6 +237,7 @@ const FormatMixinImplementation = superclass =>
/**
* @param {string|undefined} value
* @return {?}
* @private
*/
__callParser(value = this.formattedValue) {
// A) check if we need to parse at all
@ -273,6 +275,7 @@ const FormatMixinImplementation = superclass =>
/**
* @returns {string|undefined}
* @private
*/
__callFormatter() {
// - Why check for this.hasError?
@ -309,6 +312,7 @@ const FormatMixinImplementation = superclass =>
/**
* Observer Handlers
* @param {{ modelValue: unknown; }[]} args
* @protected
*/
_onModelValueChanged(...args) {
this._calculateValues({ source: 'model' });
@ -319,6 +323,7 @@ const FormatMixinImplementation = superclass =>
* @param {{ modelValue: unknown; }[]} args
* This is wrapped in a distinct method, so that parents can control when the changed event
* is fired. For objects, a deep comparison might be needed.
* @protected
*/
// eslint-disable-next-line no-unused-vars
_dispatchModelValueChangedEvent(...args) {
@ -339,6 +344,7 @@ const FormatMixinImplementation = superclass =>
* Downwards syncing should only happen for `LionField`.value changes from 'above'.
* This triggers _onModelValueChanged and connects user input
* to the parsing/formatting/serializing loop.
* @protected
*/
_syncValueUpwards() {
if (!this.__isHandlingComposition) {
@ -352,6 +358,7 @@ const FormatMixinImplementation = superclass =>
* - flow [1] will always be reflected back
* - flow [2] will not be reflected back when this flow was triggered via
* `@user-input-changed` (this will happen later, when `formatOn` condition is met)
* @protected
*/
_reflectBackFormattedValueToUser() {
if (this._reflectBackOn()) {
@ -366,6 +373,7 @@ const FormatMixinImplementation = superclass =>
* call `super._reflectBackOn()`
* @overridable
* @return {boolean}
* @protected
*/
_reflectBackOn() {
return !this.__isHandlingUserInput;
@ -375,6 +383,7 @@ const FormatMixinImplementation = superclass =>
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be
// used as source for the "user-input-changed" event (which can be seen as an abstraction
// layer on top of other events (input, change, whatever))
/** @protected */
_proxyInputEvent() {
this.dispatchEvent(
new CustomEvent('user-input-changed', {
@ -384,6 +393,7 @@ const FormatMixinImplementation = superclass =>
);
}
/** @protected */
_onUserInputChanged() {
// Upwards syncing. Most properties are delegated right away, value is synced to
// `LionField`, to be able to act on (imperatively set) value changes

View file

@ -157,10 +157,18 @@ const InteractionStateMixinImplementation = superclass =>
this.prefilled = !this._isEmpty();
}
/**
* Dispatches custom event on touched state change
* @protected
*/
_onTouchedChanged() {
this.dispatchEvent(new CustomEvent('touched-changed', { bubbles: true, composed: true }));
}
/**
* Dispatches custom event on touched state change
* @protected
*/
_onDirtyChanged() {
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
}
@ -181,6 +189,7 @@ const InteractionStateMixinImplementation = superclass =>
* (a user leaves(blurs) a field).
* When a user enters a field without altering the value(making it `dirty`),
* an error message shouldn't be shown either.
* @protected
*/
_showFeedbackConditionFor() {
return (this.touched && this.dirty) || this.prefilled || this.submitted;

View file

@ -71,6 +71,10 @@ export class LionField extends FormControlMixin(
this.submitted = false;
}
/**
* Resets modelValue to initial value.
* Interaction states are cleared
*/
reset() {
this.modelValue = this._initialModelValue;
this.resetInteractionState();
@ -84,6 +88,10 @@ export class LionField extends FormControlMixin(
this.modelValue = ''; // can't set null here, because IE11 treats it as a string
}
/**
* Dispatches custom bubble event
* @protected
*/
_onChange() {
this.dispatchEvent(
new CustomEvent('user-input-changed', {

View file

@ -59,6 +59,7 @@ const NativeTextFieldMixinImplementation = superclass =>
/**
* Restores the cursor to its original position after updating the value.
* @param {string} newValue The value that should be saved.
* @protected
*/
_setValueAndPreserveCaret(newValue) {
// Only preserve caret if focused (changing selectionStart will move focus in Safari)

View file

@ -31,6 +31,7 @@ const ChoiceGroupMixinImplementation = superclass =>
};
}
// @ts-ignore
get modelValue() {
const elems = this._getCheckedElements();
if (this.multipleChoice) {
@ -127,15 +128,21 @@ const ChoiceGroupMixinImplementation = superclass =>
constructor() {
super();
this.multipleChoice = false;
/** @type {'child'|'choice-group'|'fieldset'} */
/** @type {'child'|'choice-group'|'fieldset'}
* @protected
*/
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
/** @private */
this.__isInitialModelValue = true;
/** @private */
this.__isInitialSerializedValue = true;
/** @private */
this.__isInitialFormattedValue = true;
/** @type {Promise<any> & {done?:boolean}} */
this.registrationComplete = new Promise((resolve, reject) => {
/** @private */
this.__resolveRegistrationComplete = resolve;
/** @private */
this.__rejectRegistrationComplete = reject;
});
this.registrationComplete.done = false;
@ -156,6 +163,7 @@ const ChoiceGroupMixinImplementation = superclass =>
super.connectedCallback();
// Double microtask queue to account for Webkit race condition
Promise.resolve().then(() =>
// @ts-ignore
Promise.resolve().then(() => this.__resolveRegistrationComplete()),
);
@ -203,6 +211,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/**
* @override from FormControlMixin
* @protected
*/
_triggerInitialModelValueChangedEvent() {
this.registrationComplete.then(() => {
@ -213,6 +222,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/**
* @override
* @param {string} property
* @protected
*/
_getFromAllFormElements(property, filterCondition = () => true) {
// For modelValue, serializedValue and formattedValue, an exception should be made,
@ -229,6 +239,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/**
* @param {FormControl} child
* @protected
*/
_throwWhenInvalidChildModelValue(child) {
if (
@ -246,6 +257,9 @@ const ChoiceGroupMixinImplementation = superclass =>
}
}
/**
* @protected
*/
_isEmpty() {
if (this.multipleChoice) {
return this.modelValue.length === 0;
@ -262,6 +276,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/**
* @param {CustomEvent & {target:FormControl}} ev
* @protected
*/
_checkSingleChoiceElements(ev) {
const { target } = ev;
@ -278,6 +293,9 @@ const ChoiceGroupMixinImplementation = superclass =>
// this.__triggerCheckedValueChanged();
}
/**
* @protected
*/
_getCheckedElements() {
// We want to filter out disabled values by default
return this.formElements.filter(el => el.checked && !el.disabled);
@ -286,6 +304,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/**
* @param {string | any[]} value
* @param {Function} check
* @protected
*/
_setCheckedElements(value, check) {
for (let i = 0; i < this.formElements.length; i += 1) {
@ -309,6 +328,9 @@ const ChoiceGroupMixinImplementation = superclass =>
}
}
/**
* @private
*/
__setChoiceGroupTouched() {
const value = this.modelValue;
if (value != null && value !== this.__previousCheckedValue) {
@ -321,6 +343,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/**
* @override FormControlMixin
* @param {CustomEvent} ev
* @protected
*/
_onBeforeRepropagateChildrenValues(ev) {
// Normalize target, since we might receive 'portal events' (from children in a modal,

View file

@ -127,7 +127,9 @@ const ChoiceInputMixinImplementation = superclass =>
super();
this.modelValue = { value: '', checked: false };
this.disabled = false;
/** @protected */
this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this);
/** @protected */
this._toggleChecked = this._toggleChecked.bind(this);
}
@ -177,10 +179,16 @@ const ChoiceInputMixinImplementation = superclass =>
`;
}
/**
* @protected
*/
_choiceGraphicTemplate() {
return nothing;
}
/**
* @protected
*/
_afterTemplate() {
return nothing;
}
@ -208,6 +216,7 @@ const ChoiceInputMixinImplementation = superclass =>
* This method prevents the duplicate click and ensures the correct isTrusted event
* with the correct event.target arrives at the host.
* @param {Event} ev
* @protected
*/
// eslint-disable-next-line no-unused-vars
_preventDuplicateLabelClick(ev) {
@ -218,7 +227,10 @@ const ChoiceInputMixinImplementation = superclass =>
this._inputNode.addEventListener('click', __inputClickHandler);
}
/** @param {Event} ev */
/**
* @param {Event} ev
* @protected
*/
// eslint-disable-next-line no-unused-vars
_toggleChecked(ev) {
if (this.disabled) {
@ -234,6 +246,7 @@ const ChoiceInputMixinImplementation = superclass =>
* to sync differently with parent form group name
* Right now it checks tag name match where the parent form group tagname
* should include the child field tagname ('checkbox' is included in 'checkbox-group')
* @protected
*/
_syncNameToParentFormGroup() {
// @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin
@ -245,6 +258,7 @@ const ChoiceInputMixinImplementation = superclass =>
/**
* @param {boolean} checked
* @private
*/
__syncModelCheckedToChecked(checked) {
this.checked = checked;
@ -252,11 +266,15 @@ const ChoiceInputMixinImplementation = superclass =>
/**
* @param {any} checked
* @private
*/
__syncCheckedToModel(checked) {
this.modelValue = { value: this.choiceValue, checked };
}
/**
* @private
*/
__syncCheckedToInputElement() {
// ._inputNode might not be available yet(slot content)
// or at all (no reliance on platform construct, in case of [role=option])
@ -273,6 +291,7 @@ const ChoiceInputMixinImplementation = superclass =>
* However on Chrome on Mac whenever you use the keyboard
* it fires the input AND change event. Other Browsers only fires the change event.
* Therefore we disable the input event here.
* @protected
*/
_proxyInputEvent() {}
@ -282,6 +301,7 @@ const ChoiceInputMixinImplementation = superclass =>
* (requestUpdateInternal) callback
* @param {{ modelValue:unknown }} newV
* @param {{ modelValue:unknown }} [old]
* @protected
*/
_onModelValueChanged({ modelValue }, old) {
let _old;
@ -321,6 +341,7 @@ const ChoiceInputMixinImplementation = superclass =>
/**
* Used for required validator.
* @protected
*/
_isEmpty() {
return !this.checked;
@ -330,6 +351,7 @@ const ChoiceInputMixinImplementation = superclass =>
* @override
* Overridden from FormatMixin, since a different modelValue is used for choice inputs.
* Synchronization from user input is already arranged in this Mixin.
* @protected
*/
_syncValueUpwards() {}
};

View file

@ -81,6 +81,7 @@ const FormGroupMixinImplementation = superclass =>
return this;
}
// @ts-ignore
get modelValue() {
return this._getFromAllFormElements('modelValue');
}
@ -167,6 +168,7 @@ const FormGroupMixinImplementation = superclass =>
connectedCallback() {
super.connectedCallback();
this.setAttribute('role', 'group');
// @ts-ignore
Promise.resolve().then(() => this.__resolveRegistrationComplete());
this.registrationComplete.then(() => {
@ -325,6 +327,7 @@ const FormGroupMixinImplementation = superclass =>
*/
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
const result = {};
// @ts-ignore
this.formElements._keys().forEach(name => {
const elem = this.formElements[name];
if (elem instanceof FormControlsCollection) {

View file

@ -94,6 +94,7 @@ export class FormControlsCollection extends Array {
/**
* @desc Gives back the named keys and filters out array indexes
* @return {string[]}
* @protected
*/
_keys() {
return Object.keys(this).filter(k => Number.isNaN(Number(k)));

View file

@ -146,6 +146,7 @@ const FormRegistrarMixinImplementation = superclass =>
/**
* @param {CustomEvent} ev
* @protected
*/
_onRequestToAddFormElement(ev) {
const child = ev.detail.element;
@ -170,6 +171,7 @@ const FormRegistrarMixinImplementation = superclass =>
/**
* @param {CustomEvent} ev
* @protected
*/
_onRequestToChangeFormElementName(ev) {
const element = this.formElements[ev.detail.oldName];
@ -181,6 +183,7 @@ const FormRegistrarMixinImplementation = superclass =>
/**
* @param {CustomEvent} ev
* @protected
*/
_onRequestToRemoveFormElement(ev) {
const child = ev.detail.element;

View file

@ -38,6 +38,7 @@ const FormRegistrarPortalMixinImplementation = superclass =>
/**
* @param {CustomEvent} ev
* @private
*/
__redispatchEventForFormRegistrarPortalMixin(ev) {
ev.stopPropagation();

View file

@ -29,6 +29,7 @@ export class AsyncQueue {
}
}
/** @private */
async __run() {
this.__running = true;
await this.__queue[0]();

View file

@ -59,6 +59,7 @@ const SyncUpdatableMixinImplementation = superclass =>
* @param {string} name
* @param {*} newValue
* @param {*} oldValue
* @private
*/
static __syncUpdatableHasChanged(name, newValue, oldValue) {
// @ts-expect-error accessing private lit property
@ -69,6 +70,7 @@ const SyncUpdatableMixinImplementation = superclass =>
return newValue !== oldValue;
}
/** @private */
__syncUpdatableInitialize() {
const ns = this.__SyncUpdatableNamespace;
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this

View file

@ -26,6 +26,7 @@ export class LionValidationFeedback extends LitElement {
* @param {string | Node | TemplateResult } opts.message message or feedback node or TemplateResult
* @param {string} [opts.type]
* @param {Validator} [opts.validator]
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_messageTemplate({ message }) {

View file

@ -99,6 +99,7 @@ export const ValidateMixinImplementation = superclass =>
* @overridable
* Adds "._feedbackNode" as described below
*/
// @ts-ignore
get slots() {
/**
* FIXME: Ugly workaround https://github.com/microsoft/TypeScript/issues/40110
@ -141,6 +142,7 @@ export const ValidateMixinImplementation = superclass =>
/** @type {Object.<string, Object.<string, boolean>>} */
this.validationStates = {};
/** @protected */
this._visibleMessagesAmount = 1;
this.isPending = false;
@ -150,23 +152,35 @@ export const ValidateMixinImplementation = superclass =>
/** @type {Validator[]} */
this.defaultValidators = [];
/** @type {Validator[]} */
/**
* @type {Validator[]}
* @private
*/
this.__syncValidationResult = [];
/** @type {Validator[]} */
/**
* @type {Validator[]}
* @private
*/
this.__asyncValidationResult = [];
/**
* @desc contains results from sync Validators, async Validators and ResultValidators
* @type {Validator[]}
* @private
*/
this.__validationResult = [];
/** @type {Validator[]} */
/**
* @type {Validator[]}
* @private
*/
this.__prevValidationResult = [];
/** @type {Validator[]} */
this.__prevShownValidationResult = [];
/** @private */
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
/** @protected */
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this);
}
@ -358,6 +372,7 @@ export const ValidateMixinImplementation = superclass =>
* @param {Validator[]} syncValidators
* @param {unknown} value
* @param {{ hasAsync: boolean }} opts
* @private
*/
__executeSyncValidators(syncValidators, value, { hasAsync }) {
if (syncValidators.length) {
@ -372,6 +387,7 @@ export const ValidateMixinImplementation = superclass =>
* @desc step A3, calls __finishValidation
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators
* @param {?} value
* @private
*/
async __executeAsyncValidators(asyncValidators, value) {
if (asyncValidators.length) {
@ -389,6 +405,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* @desc step B, called by __finishValidation
* @param {Validator[]} regularValidationResult result of steps 1-3
* @private
*/
__executeResultValidators(regularValidationResult) {
const resultValidators = /** @type {ResultValidator[]} */ (this._allValidators.filter(v => {
@ -409,6 +426,7 @@ export const ValidateMixinImplementation = superclass =>
* @param {object} options
* @param {'sync'|'async'} options.source
* @param {boolean} [options.hasAsync] whether async validators are configured in this run.
* @private
* If not, we have nothing left to wait for.
*/
__finishValidation({ source, hasAsync }) {
@ -442,11 +460,15 @@ export const ValidateMixinImplementation = superclass =>
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
if (source === 'async' || !hasAsync) {
if (this.__validateCompleteResolve) {
// @ts-ignore
this.__validateCompleteResolve();
}
}
}
/**
* @private
*/
__clearValidationResults() {
this.__syncValidationResult = [];
this.__asyncValidationResult = [];
@ -454,6 +476,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* @param {Event|CustomEvent} e
* @private
*/
__onValidatorUpdated(e) {
if (e.type === 'param-changed' || e.type === 'config-changed') {
@ -461,6 +484,9 @@ export const ValidateMixinImplementation = superclass =>
}
}
/**
* @private
*/
__setupValidators() {
const events = ['param-changed', 'config-changed'];
if (this.__prevValidators) {
@ -504,6 +530,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* @param {?} v
* @private
*/
__isEmpty(v) {
if (typeof this._isEmpty === 'function') {
@ -533,6 +560,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* @param {Validator[]} validators list of objects having a .getMessage method
* @return {Promise.<FeedbackMessage[]>}
* @private
*/
async __getFeedbackMessages(validators) {
let fieldName = await this.fieldName;
@ -541,6 +569,7 @@ export const ValidateMixinImplementation = superclass =>
if (validator.config.fieldName) {
fieldName = await validator.config.fieldName;
}
// @ts-ignore
const message = await validator._getMessage({
modelValue: this.modelValue,
formControl: this,
@ -564,6 +593,7 @@ export const ValidateMixinImplementation = superclass =>
* - we compute the 'show' flag (like 'hasErrorVisible') for all types
* - we set the customValidity message of the highest prio Validator
* - we set aria-invalid="true" in case hasErrorVisible is true
* @protected
*/
_updateFeedbackComponent() {
const { _feedbackNode } = this;
@ -600,6 +630,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* Show the validity feedback when returning true, don't show when false
* @param {string} type
* @protected
*/
// eslint-disable-next-line no-unused-vars
_showFeedbackConditionFor(type) {
@ -608,6 +639,7 @@ export const ValidateMixinImplementation = superclass =>
/**
* @param {string} type
* @protected
*/
_hasFeedbackVisibleFor(type) {
return (
@ -636,6 +668,9 @@ export const ValidateMixinImplementation = superclass =>
}
}
/**
* @protected
*/
_updateShouldShowFeedbackFor() {
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
.constructor);
@ -656,6 +691,7 @@ export const ValidateMixinImplementation = superclass =>
* also filter out occurrences (based on interaction states)
* @param {{ validationResult: Validator[] }} opts
* @return {Validator[]} ordered list of Validators with feedback messages visible to the
* @protected
* end user
*/
_prioritizeAndFilterFeedback({ validationResult }) {

View file

@ -77,6 +77,7 @@ export class Validator {
* @overridable
* @param {MessageData} [data]
* @returns {Promise<string|Node>}
* @protected
*/
async _getMessage(data) {
const ctor = /** @type {typeof Validator} */ (this.constructor);
@ -130,6 +131,9 @@ export class Validator {
*/
abortExecution() {} // eslint-disable-line
/**
* @private
*/
__fakeExtendsEventTarget() {
const delegate = document.createDocumentFragment();

View file

@ -1,4 +1,5 @@
import { LitElement } from '@lion/core';
// @ts-ignore
import { localizeTearDown } from '@lion/localize/test-helpers';
import {
defineCE,
@ -10,6 +11,7 @@ import {
aTimeout,
} from '@open-wc/testing';
import sinon from 'sinon';
// @ts-ignore
import { IsNumber, Validator, LionField } from '@lion/form-core';
import '@lion/form-core/define';
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
@ -19,6 +21,7 @@ import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
*/
export function runFormGroupMixinSuite(cfg = {}) {
class FormChild extends LionField {
// @ts-ignore
get slots() {
return {
...super.slots,
@ -85,9 +88,11 @@ export function runFormGroupMixinSuite(cfg = {}) {
// TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test
it(`${tagString} has an up to date list of every form element in .formElements`, async () => {
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
// @ts-ignore
expect(el.formElements._keys().length).to.equal(3);
expect(el.formElements['hobbies[]'].length).to.equal(2);
el.removeChild(el.formElements['hobbies[]'][0]);
// @ts-ignore
expect(el.formElements._keys().length).to.equal(3);
expect(el.formElements['hobbies[]'].length).to.equal(1);
});
@ -111,6 +116,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
el.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
// @ts-ignore
expect(el.formElements._keys().length).to.equal(3);
expect(el.formElements['hobbies[]'].length).to.equal(2);
expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess');
@ -202,12 +208,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
html`<${childTag} name="lastName"></${childTag}>`,
));
// @ts-ignore
expect(el.formElements._keys().length).to.equal(3);
el.appendChild(newField);
// @ts-ignore
expect(el.formElements._keys().length).to.equal(4);
el._inputNode.removeChild(newField);
// @ts-ignore
expect(el.formElements._keys().length).to.equal(3);
});
@ -732,7 +741,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
fieldset.formElements.comment.modelValue = 'Foo';
// @ts-ignore
expect(fieldset.formElements._keys().length).to.equal(2);
// @ts-ignore
expect(newFieldset.formElements._keys().length).to.equal(3);
expect(fieldset.serializedValue).to.deep.equal({
comment: 'Foo',

View file

@ -20,6 +20,15 @@ async function expectThrowsAsync(method, errorMessage) {
expect(error.message).to.equal(errorMessage);
}
}
/**
* @param {Validator} validatorEl
*/
function getProtectedMembers(validatorEl) {
return {
// @ts-ignore
getMessage: (...args) => validatorEl._getMessage(...args),
};
}
describe('Validator', () => {
it('has an "execute" function returning "shown" state', async () => {
@ -52,8 +61,11 @@ describe('Validator', () => {
}
}
const vali = new MyValidator({}, { getMessage: 'This is the custom error message' });
const { getMessage } = getProtectedMembers(vali);
await expectThrowsAsync(
() => new MyValidator({}, { getMessage: 'This is the custom error message' })._getMessage(),
() => getMessage(),
"You must provide a value for getMessage of type 'function', you provided a value of type: string",
);
});
@ -76,7 +88,8 @@ describe('Validator', () => {
}
}
const vali = new MyValidator('myParam', { my: 'config', getMessage: configSpy });
vali._getMessage();
const { getMessage } = getProtectedMembers(vali);
getMessage();
expect(configSpy.args[0][0]).to.deep.equal({
name: 'MyValidator',
@ -102,7 +115,8 @@ describe('Validator', () => {
}
}
const vali = new MyValidator('myParam', { my: 'config' });
vali._getMessage();
const { getMessage } = getProtectedMembers(vali);
getMessage();
expect(data).to.deep.equal({
name: 'MyValidator',

View file

@ -112,7 +112,7 @@ export declare class FormControlHost {
/**
* Based on the role, details of handling model-value-changed repropagation differ.
*/
_repropagationRole: 'child' | 'choice-group' | 'fieldset';
protected _repropagationRole: 'child' | 'choice-group' | 'fieldset';
/**
* By default, a field with _repropagationRole 'choice-group' will act as an
* 'endpoint'. This means it will be considered as an individual field: for
@ -129,24 +129,24 @@ export declare class FormControlHost {
updated(changedProperties: import('@lion/core').PropertyValues): void;
render(): TemplateResult;
_groupOneTemplate(): TemplateResult;
_groupTwoTemplate(): TemplateResult;
protected _groupOneTemplate(): TemplateResult;
protected _groupTwoTemplate(): TemplateResult;
_labelTemplate(): TemplateResult;
_helpTextTemplate(): TemplateResult;
_inputGroupTemplate(): TemplateResult;
protected _inputGroupTemplate(): TemplateResult;
_inputGroupBeforeTemplate(): TemplateResult;
_inputGroupPrefixTemplate(): TemplateResult | typeof nothing;
_inputGroupInputTemplate(): TemplateResult;
protected _inputGroupInputTemplate(): TemplateResult;
_inputGroupSuffixTemplate(): TemplateResult | typeof nothing;
_inputGroupAfterTemplate(): TemplateResult;
_feedbackTemplate(): TemplateResult;
_triggerInitialModelValueChangedEvent(): void;
protected _triggerInitialModelValueChangedEvent(): void;
_enhanceLightDomClasses(): void;
_enhanceLightDomA11y(): void;
_enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void;
__reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void;
_isEmpty(modelValue?: unknown): boolean;
protected _isEmpty(modelValue?: unknown): boolean;
_getAriaDescriptionElements(): HTMLElement[];
public addToAriaLabelledBy(
element: HTMLElement,
@ -167,7 +167,7 @@ export declare class FormControlHost {
__getDirectSlotChild(slotName: string): HTMLElement;
__dispatchInitialModelValueChangedEvent(): void;
__repropagateChildrenInitialized: boolean | undefined;
_onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
__repropagateChildrenValues(ev: CustomEvent): void;
}

View file

@ -22,15 +22,15 @@ export declare class FormatHost {
set value(value: string);
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
__callParser(value: string | undefined): object;
private __callParser(value: string | undefined): object;
__callFormatter(): string;
_onModelValueChanged(arg: { modelValue: unknown }): void;
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
_dispatchModelValueChangedEvent(): void;
_syncValueUpwards(): void;
protected _syncValueUpwards(): void;
_reflectBackFormattedValueToUser(): void;
_reflectBackFormattedValueDebounced(): void;
_reflectBackOn(): boolean;
_proxyInputEvent(): void;
protected _proxyInputEvent(): void;
_onUserInputChanged(): void;
connectedCallback(): void;

View file

@ -28,17 +28,17 @@ export declare class ChoiceGroupHost {
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
_triggerInitialModelValueChangedEvent(): void;
protected _triggerInitialModelValueChangedEvent(): void;
_getFromAllFormElements(property: string, filterCondition: Function): void;
_throwWhenInvalidChildModelValue(child: FormControlHost): void;
_isEmpty(): void;
protected _isEmpty(): void;
_checkSingleChoiceElements(ev: Event): void;
_getCheckedElements(): void;
protected _getCheckedElements(): void;
_setCheckedElements(value: any, check: boolean): void;
@ -46,7 +46,7 @@ export declare class ChoiceGroupHost {
__delegateNameAttribute(child: FormControlHost): void;
_onBeforeRepropagateChildrenValues(ev: Event): void;
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
}
export declare function ChoiceGroupImplementation<T extends Constructor<LitElement>>(

View file

@ -35,7 +35,7 @@ export declare class ChoiceInputHost {
render(): TemplateResult;
_choiceGraphicTemplate(): TemplateResult;
_afterTemplate(): TemplateResult;
protected _afterTemplate(): TemplateResult;
connectedCallback(): void;
disconnectedCallback(): void;
@ -54,9 +54,9 @@ export declare class ChoiceInputHost {
__isHandlingUserInput: boolean;
_proxyInputEvent(): void;
protected _proxyInputEvent(): void;
_onModelValueChanged(
protected _onModelValueChanged(
newV: { modelValue: ChoiceInputModelValue },
oldV: { modelValue: ChoiceInputModelValue },
): void;
@ -65,9 +65,9 @@ export declare class ChoiceInputHost {
formatter(modelValue: ChoiceInputModelValue): string;
_isEmpty(): void;
protected _isEmpty(): void;
_syncValueUpwards(): void;
protected _syncValueUpwards(): void;
type: string;

View file

@ -9,7 +9,7 @@ export declare class ElementWithParentFormGroup {
}
export declare class FormRegistrarHost {
_isFormOrFieldset: boolean;
protected _isFormOrFieldset: boolean;
formElements: FormControlsCollection & { [x: string]: any };
addFormElement(
child:

View file

@ -35,6 +35,16 @@ import '@lion/form-core/define';
* @typedef {import('@lion/listbox').LionOption} LionOption
*/
/**
* @param {FormControl} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
repropagationRole: el._repropagationRole,
};
}
const featureName = 'model value';
const getFirstPaintTitle = /** @param {number} count */ count =>
@ -386,13 +396,14 @@ describe('detail.isTriggeredByUser', () => {
* @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'}
*/
function detectType(el) {
if (el._repropagationRole === 'child') {
const { repropagationRole } = getProtectedMembers(el);
if (repropagationRole === 'child') {
if (featureDetectChoiceField(el)) {
return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField';
}
return 'RegularField';
}
return el._repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset';
return repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset';
}
/**

View file

@ -15,7 +15,9 @@ const throwFormNodeError = () => {
export class LionForm extends LionFieldset {
constructor() {
super();
/** @protected */
this._submit = this._submit.bind(this);
/** @protected */
this._reset = this._reset.bind(this);
}
@ -48,6 +50,7 @@ export class LionForm extends LionFieldset {
/**
* @param {Event} ev
* @protected
*/
_submit(ev) {
ev.preventDefault();
@ -66,6 +69,7 @@ export class LionForm extends LionFieldset {
/**
* @param {Event} ev
* @protected
*/
_reset(ev) {
ev.preventDefault();
@ -74,11 +78,13 @@ export class LionForm extends LionFieldset {
this.dispatchEvent(new Event('reset', { bubbles: true }));
}
/** @private */
__registerEventsForLionForm() {
this._formNode.addEventListener('submit', this._submit);
this._formNode.addEventListener('reset', this._reset);
}
/** @private */
__teardownEventsForLionForm() {
this._formNode.removeEventListener('submit', this._submit);
this._formNode.removeEventListener('reset', this._reset);

View file

@ -5,6 +5,7 @@
export class IconManager {
constructor() {
/** @private */
this.__iconResolvers = new Map();
}

View file

@ -98,6 +98,7 @@ export class LionIcon extends LitElement {
this.role = 'img';
this.ariaLabel = '';
this.iconId = '';
/** @private */
this.__svg = nothing;
}
@ -141,6 +142,7 @@ export class LionIcon extends LitElement {
return this.__svg;
}
/** @protected */
_onLabelChanged() {
if (this.ariaLabel) {
this.setAttribute('aria-hidden', 'false');
@ -152,6 +154,7 @@ export class LionIcon extends LitElement {
/**
* @param {TemplateResult | nothing} svgObject
* @protected
*/
_renderSvg(svgObject) {
validateSvg(svgObject);
@ -161,6 +164,7 @@ export class LionIcon extends LitElement {
}
}
/** @protected */
// eslint-disable-next-line class-methods-use-this
get _iconManager() {
return icons;
@ -168,6 +172,7 @@ export class LionIcon extends LitElement {
/**
* @param {string} prevIconId
* @protected
*/
async _onIconIdChanged(prevIconId) {
if (!this.iconId) {

View file

@ -7,14 +7,27 @@ import { IconManager } from '../src/IconManager.js';
* @typedef {import("lit-html").TemplateResult} TemplateResult
*/
/**
* @param {IconManager} iconManagerEl
*/
function getProtectedMembers(iconManagerEl) {
// @ts-ignore
const { __iconResolvers: iconResolvers } = iconManagerEl;
return {
iconResolvers,
};
}
describe('IconManager', () => {
it('starts off with an empty map of resolvers', () => {
const manager = new IconManager();
expect(manager.__iconResolvers.size).to.equal(0);
const { iconResolvers } = getProtectedMembers(manager);
expect(iconResolvers.size).to.equal(0);
});
it('allows adding an icon resolver', () => {
const manager = new IconManager();
const { iconResolvers } = getProtectedMembers(manager);
/**
* @param {string} iconset
* @param {string} icon
@ -24,7 +37,7 @@ describe('IconManager', () => {
const resolver = (iconset, icon) => nothing;
manager.addIconResolver('foo', resolver);
expect(manager.__iconResolvers.get('foo')).to.equal(resolver);
expect(iconResolvers.get('foo')).to.equal(resolver);
});
it('does not allow adding a resolve for the same namespace twice', () => {

View file

@ -10,6 +10,7 @@ import { parseAmount } from './parsers.js';
*
* @customElement lion-input-amount
*/
// @ts-ignore
export class LionInputAmount extends LocalizeMixin(LionInput) {
/** @type {any} */
static get properties() {
@ -68,10 +69,13 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
this.formatter = formatAmount;
/** @type {string | undefined} */
this.currency = undefined;
/** @private */
this.__isPasting = false;
this.addEventListener('paste', () => {
/** @private */
this.__isPasting = true;
/** @private */
this.__parserCallcountSincePaste = 0;
});
@ -99,17 +103,20 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
/**
* @override of FormatMixin
* @private
*/
__callParser(value = this.formattedValue) {
// TODO: (@daKmor) input and change events both trigger parsing therefore we need to handle the second parse
this.__parserCallcountSincePaste += 1;
this.__isPasting = this.__parserCallcountSincePaste === 2;
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
// @ts-ignore
return super.__callParser(value);
}
/**
* @override of FormatMixin
* @protected
*/
_reflectBackOn() {
return super._reflectBackOn() || this.__isPasting;
@ -118,6 +125,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
/**
* @param {Object} opts
* @param {string} opts.currency
* @protected
*/
_onCurrencyChanged({ currency }) {
if (this._isPrivateSlot('after') && this._currencyDisplayNode) {
@ -128,6 +136,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
this.__setCurrencyDisplayLabel();
}
/** @private */
__setCurrencyDisplayLabel() {
// TODO: (@erikkroes) for optimal a11y, abbreviations should be part of aria-label
// example, for a language switch with text 'en', an aria-label of 'english' is not

View file

@ -93,6 +93,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
];
}
/** @private */
__dispatchCloseEvent() {
this.dispatchEvent(new Event('close-overlay'));
}

View file

@ -169,18 +169,26 @@ export class LionInputDatepicker extends ScopedElementsMixin(
constructor() {
super();
/** @private */
this.__invokerId = this.__createUniqueIdForA11y();
/** @protected */
this._calendarInvokerSlot = 'suffix';
// Configuration flags for subclassers
/** @protected */
this._focusCentralDateOnCalendarOpen = true;
/** @protected */
this._hideOnUserSelect = true;
/** @protected */
this._syncOnUserSelect = true;
/** @private */
this.__openCalendarOverlay = this.__openCalendarOverlay.bind(this);
/** @protected */
this._onCalendarUserSelectedChanged = this._onCalendarUserSelectedChanged.bind(this);
}
/** @private */
__createUniqueIdForA11y() {
return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`;
}
@ -197,6 +205,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
}
}
/** @private */
__toggleInvokerDisabled() {
if (this._invokerNode) {
const invokerNode = /** @type {HTMLElement & {disabled: boolean}} */ (this._invokerNode);
@ -226,6 +235,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
* Defining this overlay as a templates from OverlayMixin
* this is our source to give as .contentNode to OverlayController.
* Important: do not change the name of this method.
* @protected
*/
_overlayTemplate() {
// TODO: add performance optimization to only render the calendar if needed

View file

@ -13,6 +13,27 @@ import { LionInputDatepicker } from '../src/LionInputDatepicker.js';
* @typedef {import('@lion/core').TemplateResult} TemplateResult
*/
/**
* @param {LionInputDatepicker} datepickerEl
*/
function getProtectedMembersDatepicker(datepickerEl) {
// @ts-ignore
const { __invokerId: invokerId } = datepickerEl;
return {
invokerId,
};
}
/**
* @param {LionCalendar} calendarEl
*/
function getProtectedMembersCalendar(calendarEl) {
return {
// @ts-ignore
dateSelectedByUser: (...args) => calendarEl.__dateSelectedByUser(...args),
};
}
const fixture = /** @type {(arg: TemplateResult) => Promise<LionInputDatepicker>} */ (_fixture);
describe('<lion-input-datepicker>', () => {
@ -278,8 +299,10 @@ describe('<lion-input-datepicker>', () => {
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector(
'[data-tag-name="lion-calendar"]',
));
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
// First set a fixed date as if selected by a user
calendarEl.__dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
await el.updateComplete;
const elObj = new DatepickerInputObject(el);
@ -303,8 +326,10 @@ describe('<lion-input-datepicker>', () => {
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector(
'[data-tag-name="lion-calendar"]',
));
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
// First set a fixed date as if selected by a user
calendarEl.__dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
await el.updateComplete;
const elObj = new DatepickerInputObject(el);
@ -411,6 +436,7 @@ describe('<lion-input-datepicker>', () => {
const myEl = await fixture(html`<${myTag}></${myTag}>`);
const myElObj = new DatepickerInputObject(myEl);
const { invokerId } = getProtectedMembersDatepicker(myEl);
expect(myElObj.invokerEl.tagName.toLowerCase()).to.equal('my-button');
// All other tests will still pass. Small checkup:
@ -419,7 +445,7 @@ describe('<lion-input-datepicker>', () => {
expect(myElObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
expect(myElObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog');
expect(myElObj.invokerEl.getAttribute('slot')).to.equal('suffix');
expect(myElObj.invokerEl.getAttribute('id')).to.equal(myEl.__invokerId);
expect(myElObj.invokerEl.getAttribute('id')).to.equal(invokerId);
await myElObj.openCalendar();
expect(myElObj.overlayController.isShown).to.equal(true);
});

View file

@ -70,6 +70,7 @@ export class LionInputRange extends LionInput {
*/
this.parser = modelValue => parseFloat(modelValue);
this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`;
/** @private */
this.__styleTag = document.createElement('style');
}
@ -114,6 +115,7 @@ export class LionInputRange extends LionInput {
}
}
/** @protected */
_inputGroupTemplate() {
return html`
<div>
@ -131,6 +133,7 @@ export class LionInputRange extends LionInput {
`;
}
/** @protected */
_inputGroupInputTemplate() {
return html`
<div class="input-group__input">
@ -147,6 +150,7 @@ export class LionInputRange extends LionInput {
`;
}
/** @private */
__setupStyleTag() {
this.__styleTag.textContent = /** @type {typeof LionInputRange} */ (this.constructor)
.rangeStyles(unsafeCSS(this.scopedClass))
@ -154,6 +158,7 @@ export class LionInputRange extends LionInput {
this.insertBefore(this.__styleTag, this.childNodes[0]);
}
/** @private */
__teardownStyleTag() {
this.removeChild(this.__styleTag);
}

View file

@ -177,6 +177,7 @@ export class LionInputStepper extends LionInput {
* Get slotted element
* @param {String} slotName - slot name
* @returns {HTMLButtonElement|Object}
* @private
*/
__getSlot(slotName) {
return (
@ -215,6 +216,7 @@ export class LionInputStepper extends LionInput {
/**
* Get the increment button node
* @returns {Element|null}
* @private
*/
__getIncrementButtonNode() {
const renderParent = document.createElement('div');
@ -232,6 +234,7 @@ export class LionInputStepper extends LionInput {
/**
* Get the decrement button node
* @returns {Element|null}
* @private
*/
__getDecrementButtonNode() {
const renderParent = document.createElement('div');
@ -249,6 +252,7 @@ export class LionInputStepper extends LionInput {
/**
* Toggle +/- buttons on change
* @override
* @protected
*/
_onChange() {
super._onChange();
@ -258,6 +262,7 @@ export class LionInputStepper extends LionInput {
/**
* Get the decrementor button sign template
* @returns {String|import('@lion/core').TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_decrementorSignTemplate() {
@ -267,6 +272,7 @@ export class LionInputStepper extends LionInput {
/**
* Get the incrementor button sign template
* @returns {String|import('@lion/core').TemplateResult}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_incrementorSignTemplate() {
@ -276,6 +282,7 @@ export class LionInputStepper extends LionInput {
/**
* Get the increment button template
* @returns {import('@lion/core').TemplateResult}
* @protected
*/
_decrementorTemplate() {
return html`
@ -294,6 +301,7 @@ export class LionInputStepper extends LionInput {
/**
* Get the decrement button template
* @returns {import('@lion/core').TemplateResult}
* @protected
*/
_incrementorTemplate() {
return html`

View file

@ -103,6 +103,7 @@ export class LionInput extends NativeTextFieldMixin(
}
}
/** @private */
__delegateReadOnly() {
if (this._inputNode) {
this._inputNode.readOnly = this.readOnly;

View file

@ -66,7 +66,9 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
constructor() {
super();
this.active = false;
/** @private */
this.__onClick = this.__onClick.bind(this);
/** @private */
this.__registerEventListeners();
}
@ -109,14 +111,17 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
this.setAttribute('role', 'option');
}
/** @private */
__registerEventListeners() {
this.addEventListener('click', this.__onClick);
}
/** @private */
__unRegisterEventListeners() {
this.removeEventListener('click', this.__onClick);
}
/** @private */
__onClick() {
if (this.disabled) {
return;

View file

@ -98,6 +98,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @override FormControlMixin
* @protected
*/
// eslint-disable-next-line
_inputGroupInputTemplate() {
@ -259,31 +260,58 @@ const ListboxMixinImplementation = superclass =>
*/
this.selectionFollowsFocus = false;
/** @type {number | null} */
/**
* @type {number | null}
* @protected
*/
this._listboxActiveDescendant = null;
/** @private */
this.__hasInitialSelectedFormElement = false;
/** @protected */
this._repropagationRole = 'choice-group'; // configures FormControlMixin
/**
* When listbox is coupled to a textbox (in case we are dealing with a combobox),
* spaces should not select an element (they need to be put in the textbox)
* @protected
*/
this._listboxReceivesNoFocus = false;
/** @type {string | string[] | undefined} */
/**
* @type {string | string[] | undefined}
* @private
*/
this.__oldModelValue = undefined;
/** @type {EventListener} */
/**
* @type {EventListener}
* @protected
*/
this._listboxOnKeyDown = this._listboxOnKeyDown.bind(this);
/** @type {EventListener} */
/**
* @type {EventListener}
* @protected
*/
this._listboxOnClick = this._listboxOnClick.bind(this);
/** @type {EventListener} */
/**
* @type {EventListener}
* @protected
*/
this._listboxOnKeyUp = this._listboxOnKeyUp.bind(this);
/** @type {EventListener} */
/**
* @type {EventListener}
* @protected
*/
this._onChildActiveChanged = this._onChildActiveChanged.bind(this);
/** @type {EventListener} */
/**
* @type {EventListener}
* @private
*/
this.__proxyChildModelValueChanged = this.__proxyChildModelValueChanged.bind(this);
/** @type {EventListener} */
/**
* @type {EventListener}
* @private
*/
this.__preventScrollingWithArrowKeys = this.__preventScrollingWithArrowKeys.bind(this);
}
@ -418,11 +446,13 @@ const ListboxMixinImplementation = superclass =>
/**
* @override ChoiceGroupMixin: in the select disabled options are still going to a possible
* value, for example when prefilling or programmatically setting it.
* @protected
*/
_getCheckedElements() {
return this.formElements.filter(el => el.checked);
}
/** @protected */
_setupListboxNode() {
if (this._listboxNode) {
this.__setupListboxNodeInteractions();
@ -439,6 +469,7 @@ const ListboxMixinImplementation = superclass =>
}
}
/** @protected */
_teardownListboxNode() {
if (this._listboxNode) {
this._listboxNode.removeEventListener('keydown', this._listboxOnKeyDown);
@ -450,6 +481,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {number} currentIndex
* @param {number} [offset=1]
* @protected
*/
_getNextEnabledOption(currentIndex, offset = 1) {
return this.__getEnabledOption(currentIndex, offset);
@ -458,6 +490,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {number} currentIndex
* @param {number} [offset=-1]
* @protected
*/
_getPreviousEnabledOption(currentIndex, offset = -1) {
return this.__getEnabledOption(currentIndex, offset);
@ -466,6 +499,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @overridable
* @param {Event & { target: LionOption }} ev
* @protected
*/
// eslint-disable-next-line no-unused-vars, class-methods-use-this
_onChildActiveChanged({ target }) {
@ -480,6 +514,7 @@ const ListboxMixinImplementation = superclass =>
* an item.
*
* @param {KeyboardEvent} ev - the keydown event object
* @protected
*/
_listboxOnKeyDown(ev) {
if (this.disabled) {
@ -565,6 +600,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @overridable
* @param {MouseEvent} ev
* @protected
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
_listboxOnClick(ev) {
@ -587,6 +623,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @overridable
* @param {KeyboardEvent} ev
* @protected
*/
// eslint-disable-next-line class-methods-use-this, no-unused-vars
_listboxOnKeyUp(ev) {
@ -615,11 +652,13 @@ const ListboxMixinImplementation = superclass =>
/**
* @configure FormControlMixin
* @protected
*/
_onLabelClick() {
this._listboxNode.focus();
}
/** @private */
__setupEventListeners() {
this._listboxNode.addEventListener(
'active-changed',
@ -631,6 +670,7 @@ const ListboxMixinImplementation = superclass =>
);
}
/** @private */
__teardownEventListeners() {
this._listboxNode.removeEventListener(
'active-changed',
@ -644,6 +684,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {LionOption | null} el
* @private
*/
__setChildActive(el) {
this.formElements.forEach(formElement => {
@ -662,6 +703,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {LionOption|LionOption[]} [exclude]
* @protected
*/
_uncheckChildren(exclude = []) {
const excludes = Array.isArray(exclude) ? exclude : [exclude];
@ -675,6 +717,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {Event & { target: LionOption }} cfgOrEvent
* @private
*/
__onChildCheckedChanged(cfgOrEvent) {
const { target } = cfgOrEvent;
@ -690,6 +733,7 @@ const ListboxMixinImplementation = superclass =>
* // TODO: add to choiceGroup
* @param {string} attribute
* @param {number} value
* @private
*/
__setAttributeForAllFormElements(attribute, value) {
this.formElements.forEach(formElement => {
@ -699,6 +743,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {CustomEvent & { target: LionOption; }} ev
* @private
*/
__proxyChildModelValueChanged(ev) {
// We need to redispatch the model-value-changed event on 'this', so it will
@ -729,6 +774,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {number} currentIndex
* @param {number} offset
* @private
*/
__getEnabledOption(currentIndex, offset) {
/**
@ -760,6 +806,7 @@ const ListboxMixinImplementation = superclass =>
/**
* Moves options put in unnamed slot to slot with [role="listbox"]
* @private
*/
__moveOptionsToListboxNode() {
const slot = /** @type {HTMLSlotElement} */ (
@ -780,6 +827,7 @@ const ListboxMixinImplementation = superclass =>
/**
* @param {KeyboardEvent} ev
* @private
*/
__preventScrollingWithArrowKeys(ev) {
if (this.disabled) {
@ -798,6 +846,7 @@ const ListboxMixinImplementation = superclass =>
/**
* Helper method used within `._setupListboxNode`
* @private
*/
__setupListboxNodeInteractions() {
this._listboxNode.setAttribute('role', 'listbox');
@ -812,6 +861,7 @@ const ListboxMixinImplementation = superclass =>
}
// TODO: move to ChoiceGroupMixin?
/** @private */
__requestOptionsToBeDisabled() {
this.formElements.forEach(el => {
if (el.makeRequestToBeDisabled) {
@ -820,6 +870,7 @@ const ListboxMixinImplementation = superclass =>
});
}
/** @private */
__retractRequestOptionsToBeDisabled() {
this.formElements.forEach(el => {
if (el.retractRequestToBeDisabled) {
@ -828,6 +879,7 @@ const ListboxMixinImplementation = superclass =>
});
}
/** @private */
__initInteractionStates() {
this.initInteractionState();
}

View file

@ -15,20 +15,35 @@ import isLocalizeESModule from './isLocalizeESModule.js';
export class LocalizeManager {
// eslint-disable-line no-unused-vars
constructor({ autoLoadOnLocaleChange = false, fallbackLocale = '' } = {}) {
/** @private */
this.__delegationTarget = document.createDocumentFragment();
/** @protected */
this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
/** @protected */
this._fallbackLocale = fallbackLocale;
/** @type {Object.<string, Object.<string, Object>>} */
/**
* @type {Object.<string, Object.<string, Object>>}
* @private
*/
this.__storage = {};
/** @type {Map.<RegExp|string, function>} */
/**
* @type {Map.<RegExp|string, function>}
* @private
*/
this.__namespacePatternsMap = new Map();
/** @type {Object.<string, function|null>} */
/**
* @type {Object.<string, function|null>}
* @private
*/
this.__namespaceLoadersCache = {};
/** @type {Object.<string, Object.<string, Promise.<Object>>>} */
/**
* @type {Object.<string, Object.<string, Promise.<Object>>>}
* @private
*/
this.__namespaceLoaderPromisesCache = {};
this.formatNumberOptions = {
@ -50,6 +65,7 @@ export class LocalizeManager {
*/
const initialLocale = document.documentElement.getAttribute('data-localize-lang');
/** @protected */
this._supportExternalTranslationTools = Boolean(initialLocale);
if (this._supportExternalTranslationTools) {
@ -61,9 +77,11 @@ export class LocalizeManager {
document.documentElement.lang = this.locale || 'en-GB';
}
/** @protected */
this._setupHtmlLangAttributeObserver();
}
/** @protected */
_setupTranslationToolSupport() {
/**
* This value allows for support for Google Translate (or other 3rd parties taking control
@ -132,6 +150,7 @@ export class LocalizeManager {
/**
* @param {string} locale
* @protected
*/
_setHtmlLangAttribute(locale) {
this._teardownHtmlLangAttributeObserver();
@ -142,6 +161,7 @@ export class LocalizeManager {
/**
* @param {string} value
* @throws {Error} Language only locales are not allowed(Use 'en-GB' instead of 'en')
* @private
*/
// eslint-disable-next-line class-methods-use-this
__handleLanguageOnly(value) {
@ -248,6 +268,7 @@ export class LocalizeManager {
return formatter.format(vars);
}
/** @protected */
_setupHtmlLangAttributeObserver() {
if (!this._htmlLangAttributeObserver) {
this._htmlLangAttributeObserver = new MutationObserver(mutations => {
@ -273,6 +294,7 @@ export class LocalizeManager {
});
}
/** @protected */
_teardownHtmlLangAttributeObserver() {
if (this._htmlLangAttributeObserver) {
this._htmlLangAttributeObserver.disconnect();
@ -282,6 +304,7 @@ export class LocalizeManager {
/**
* @param {string} locale
* @param {string} namespace
* @protected
*/
_isNamespaceInCache(locale, namespace) {
return !!(this.__storage[locale] && this.__storage[locale][namespace]);
@ -290,6 +313,7 @@ export class LocalizeManager {
/**
* @param {string} locale
* @param {string} namespace
* @protected
*/
_getCachedNamespaceLoaderPromise(locale, namespace) {
if (this.__namespaceLoaderPromisesCache[locale]) {
@ -304,6 +328,7 @@ export class LocalizeManager {
* @param {boolean} isDynamicImport
* @param {string} namespace
* @returns {Promise.<Object|void>}
* @protected
*/
_loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace) {
const loader = this._getNamespaceLoader(namespaceObj, isDynamicImport, namespace);
@ -326,6 +351,7 @@ export class LocalizeManager {
* @param {boolean} isDynamicImport
* @param {string} namespace
* @throws {Error} Namespace shall setup properly. Check loader!
* @protected
*/
_getNamespaceLoader(namespaceObj, isDynamicImport, namespace) {
let loader = this.__namespaceLoadersCache[namespace];
@ -356,6 +382,7 @@ export class LocalizeManager {
* @param {string} [fallbackLocale]
* @returns {Promise.<any>}
* @throws {Error} Data for namespace and (locale or fallback locale) could not be loaded.
* @protected
*/
_getNamespaceLoaderPromise(loader, locale, namespace, fallbackLocale = this._fallbackLocale) {
return loader(locale, namespace).catch(() => {
@ -384,6 +411,7 @@ export class LocalizeManager {
* @param {string} locale
* @param {string} namespace
* @param {Promise.<Object>} promise
* @protected
*/
_cacheNamespaceLoaderPromise(locale, namespace, promise) {
if (!this.__namespaceLoaderPromisesCache[locale]) {
@ -395,6 +423,7 @@ export class LocalizeManager {
/**
* @param {string} namespace
* @returns {function|null}
* @protected
*/
_lookupNamespaceLoader(namespace) {
/* eslint-disable no-restricted-syntax */
@ -413,6 +442,7 @@ export class LocalizeManager {
/**
* @param {string} locale
* @returns {string}
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_getLangFromLocale(locale) {
@ -448,6 +478,7 @@ export class LocalizeManager {
* @param {string} newLocale
* @param {string} oldLocale
* @returns {undefined}
* @protected
*/
_onLocaleChanged(newLocale, oldLocale) {
if (newLocale === oldLocale) {
@ -463,6 +494,7 @@ export class LocalizeManager {
* @param {string} newLocale
* @param {string} oldLocale
* @returns {Promise.<Object>}
* @protected
*/
_loadAllMissing(newLocale, oldLocale) {
const oldLocaleNamespaces = this.__storage[oldLocale] || {};
@ -486,6 +518,7 @@ export class LocalizeManager {
* @param {string | string[]} keys
* @param {string} locale
* @returns {string | undefined}
* @protected
*/
_getMessageForKeys(keys, locale) {
if (typeof keys === 'string') {
@ -509,6 +542,7 @@ export class LocalizeManager {
* @param {string} locale
* @returns {string}
* @throws {Error} `key`is missing namespace. The format for `key` is "namespace:name"
* @protected
*
*/
_getMessageForKey(key, locale) {

View file

@ -29,6 +29,7 @@ const LocalizeMixinImplementation = superclass =>
constructor() {
super();
/** @private */
this.__boundLocalizeOnLocaleChanged =
/** @param {...Object} args */
(...args) => {
@ -37,10 +38,12 @@ const LocalizeMixinImplementation = superclass =>
};
// should be loaded in advance
/** @private */
this.__localizeStartLoadingNamespaces();
if (this.localizeNamespacesLoaded) {
this.localizeNamespacesLoaded.then(() => {
/** @private */
this.__localizeMessageSync = true;
});
}
@ -100,6 +103,7 @@ const LocalizeMixinImplementation = superclass =>
/**
* @returns {string[]}
* @private
*/
__getUniqueNamespaces() {
/** @type {string[]} */
@ -114,20 +118,24 @@ const LocalizeMixinImplementation = superclass =>
return uniqueNamespaces;
}
/** @private */
__localizeStartLoadingNamespaces() {
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces());
}
/** @private */
__localizeAddLocaleChangedListener() {
localize.addEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
}
/** @private */
__localizeRemoveLocaleChangedListener() {
localize.removeEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
}
/**
* @param {CustomEvent} event
* @private
*/
__localizeOnLocaleChanged(event) {
this.onLocaleChanged(event.detail.newLocale, event.detail.oldLocale);

View file

@ -2,8 +2,10 @@ import { localize } from '../src/localize.js';
export const localizeTearDown = () => {
// makes sure that between tests the localization is reset to default state
// @ts-ignore
localize._teardownHtmlLangAttributeObserver();
document.documentElement.lang = 'en-GB';
// @ts-ignore
localize._setupHtmlLangAttributeObserver();
localize.reset();
};

View file

@ -6,6 +6,21 @@ import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers/in
import { LocalizeManager } from '../src/LocalizeManager.js';
/**
* @param {LocalizeManager} localizeManagerEl
*/
function getProtectedMembers(localizeManagerEl) {
// @ts-ignore
const {
__storage: storage,
_supportExternalTranslationTools: supportExternalTranslationTools,
} = localizeManagerEl;
return {
storage,
supportExternalTranslationTools,
};
}
/**
* @param {string} str
* Useful for IE11 where LTR and RTL symbols are put by Intl when rendering dates
@ -84,10 +99,11 @@ describe('LocalizeManager', () => {
describe('addData()', () => {
it('allows to provide inline data', () => {
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'lion-hello': { greeting: 'Hi!' },
},
@ -95,7 +111,7 @@ describe('LocalizeManager', () => {
manager.addData('en-GB', 'lion-goodbye', { farewell: 'Cheers!' });
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'lion-hello': { greeting: 'Hi!' },
'lion-goodbye': { farewell: 'Cheers!' },
@ -105,7 +121,7 @@ describe('LocalizeManager', () => {
manager.addData('nl-NL', 'lion-hello', { greeting: 'Hoi!' });
manager.addData('nl-NL', 'lion-goodbye', { farewell: 'Doei!' });
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'lion-hello': { greeting: 'Hi!' },
'lion-goodbye': { farewell: 'Cheers!' },
@ -119,6 +135,7 @@ describe('LocalizeManager', () => {
it('prevents mutating existing data for the same locale & namespace', () => {
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
@ -126,7 +143,7 @@ describe('LocalizeManager', () => {
manager.addData('en-GB', 'lion-hello', { greeting: 'Hello!' });
}).to.throw();
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'lion-hello': { greeting: 'Hi!' } },
});
});
@ -137,13 +154,14 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
});
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-component': { greeting: 'Hello!' },
},
@ -154,6 +172,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.locale = 'en-US';
await manager.loadNamespace(
@ -164,7 +183,7 @@ describe('LocalizeManager', () => {
{ locale: 'nl-NL' },
);
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'nl-NL': {
'my-component': { greeting: 'Hello!' },
},
@ -176,6 +195,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-send-button/en-GB.js', { default: { submit: 'Send' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
await manager.loadNamespaces([
{
@ -188,7 +208,7 @@ describe('LocalizeManager', () => {
},
]);
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-defaults': { submit: 'Submit' },
'my-send-button': { submit: 'Send' },
@ -201,6 +221,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-send-button/nl-NL.js', { default: { submit: 'Send' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.locale = 'en-US';
await manager.loadNamespaces(
@ -217,7 +238,7 @@ describe('LocalizeManager', () => {
{ locale: 'nl-NL' },
);
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'nl-NL': {
'my-defaults': { submit: 'Submit' },
'my-send-button': { submit: 'Send' },
@ -229,13 +250,14 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
});
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-component': { greeting: 'Hello!' },
},
@ -265,6 +287,7 @@ describe('LocalizeManager', () => {
describe('fallback locale', () => {
it('can load a fallback locale if current one can not be loaded', async () => {
manager = new LocalizeManager({ fallbackLocale: 'en-GB' });
const { storage } = getProtectedMembers(manager);
manager.locale = 'nl-NL';
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
@ -274,7 +297,7 @@ describe('LocalizeManager', () => {
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
});
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'nl-NL': {
'my-component': { greeting: 'Hello!' },
},
@ -283,6 +306,7 @@ describe('LocalizeManager', () => {
it('can load fallback generic language file if fallback locale file is not found', async () => {
manager = new LocalizeManager({ fallbackLocale: 'en-GB' });
const { storage } = getProtectedMembers(manager);
manager.locale = 'nl-NL';
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
@ -292,7 +316,7 @@ describe('LocalizeManager', () => {
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
});
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'nl-NL': {
'my-component': { greeting: 'Hello!' },
},
@ -345,6 +369,7 @@ describe('LocalizeManager', () => {
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader(
'my-component',
@ -357,7 +382,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespace('my-component');
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-component': { greeting: 'Hello!' },
},
@ -373,6 +398,7 @@ describe('LocalizeManager', () => {
});
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader(
'my-defaults',
@ -394,7 +420,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespaces(['my-defaults', 'my-send-button']);
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-send-button': {
submit: 'Send',
@ -410,6 +436,7 @@ describe('LocalizeManager', () => {
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader(
/my-.+/,
@ -425,7 +452,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespace('my-component');
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-component': { greeting: 'Hello!' },
},
@ -437,6 +464,7 @@ describe('LocalizeManager', () => {
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader(
/my-.+/,
@ -452,7 +480,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespaces(['my-defaults', 'my-send-button']);
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': {
'my-defaults': { submit: 'Submit' },
'my-send-button': { submit: 'Send' },
@ -467,20 +495,21 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } });
manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
});
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
});
manager.locale = 'nl-NL';
await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
'nl-NL': { 'my-component': { greeting: 'Hallo!' } },
});
@ -491,14 +520,15 @@ describe('LocalizeManager', () => {
it('has a Promise "loadingComplete" that resolved once all pending loading is done', async () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
});
expect(manager.__storage).to.deep.equal({});
expect(storage).to.deep.equal({});
await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
});
});
@ -526,11 +556,12 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Loaded hello!' } });
manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.addData('en-GB', 'my-component', { greeting: 'Hello!' });
await manager.loadNamespace('my-component');
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
});
@ -544,7 +575,7 @@ describe('LocalizeManager', () => {
});
expect(called).to.equal(0);
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
});
});
@ -684,11 +715,15 @@ describe('When supporting external translation tools like Google Translate', ()
it('triggers support for external translation tools via data-localize-lang', async () => {
document.documentElement.removeAttribute('data-localize-lang');
manager = getInstance();
expect(manager._supportExternalTranslationTools).to.be.false;
const { supportExternalTranslationTools: first } = getProtectedMembers(manager);
expect(first).to.be.false;
document.documentElement.setAttribute('data-localize-lang', 'nl-NL');
manager = getInstance();
expect(manager._supportExternalTranslationTools).to.be.true;
const { supportExternalTranslationTools: second } = getProtectedMembers(manager);
expect(second).to.be.true;
});
});
@ -776,13 +811,14 @@ describe('[deprecated] When not supporting external translation tools like Googl
manager = new LocalizeManager({
autoLoadOnLocaleChange: true,
});
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({
/** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
});
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
});
@ -790,7 +826,7 @@ describe('[deprecated] When not supporting external translation tools like Googl
await aTimeout(0); // wait for mutation observer to be called
await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({
expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } },
'nl-NL': { 'my-component': { greeting: 'Hallo!' } },
});

View file

@ -4,6 +4,21 @@ import { LocalizeManager } from '../src/LocalizeManager.js';
import { localize, setLocalize } from '../src/localize.js';
/**
* @param {LocalizeManager} localizeManagerEl
*/
function getProtectedMembers(localizeManagerEl) {
// @ts-ignore
const {
_autoLoadOnLocaleChange: autoLoadOnLocaleChange,
_fallbackLocale: fallbackLocale,
} = localizeManagerEl;
return {
autoLoadOnLocaleChange,
fallbackLocale,
};
}
describe('localize', () => {
// this is an important mindset:
// we don't test the singleton
@ -32,10 +47,12 @@ describe('localize', () => {
});
it('is configured to automatically load namespaces if locale is changed', () => {
expect(localize._autoLoadOnLocaleChange).to.equal(true);
const { autoLoadOnLocaleChange } = getProtectedMembers(localize);
expect(autoLoadOnLocaleChange).to.equal(true);
});
it('is configured to fallback to the locale "en-GB"', () => {
expect(localize._fallbackLocale).to.equal('en-GB');
const { fallbackLocale } = getProtectedMembers(localize);
expect(fallbackLocale).to.equal('en-GB');
});
});

View file

@ -92,6 +92,7 @@ export const ArrowMixinImplementation = superclass =>
constructor() {
super();
this.hasArrow = true;
/** @private */
this.__setupRepositionCompletePromise();
}
@ -105,10 +106,12 @@ export const ArrowMixinImplementation = superclass =>
`;
}
/** @protected */
_arrowNodeTemplate() {
return html` <div class="arrow" data-popper-arrow>${this._arrowTemplate()}</div> `;
}
/** @protected */
// eslint-disable-next-line class-methods-use-this
_arrowTemplate() {
return html`
@ -123,6 +126,7 @@ export const ArrowMixinImplementation = superclass =>
* and adds onCreate and onUpdate hooks to sync from popper state
* @configure OverlayMixin
* @returns {OverlayConfig}
* @protected
*/
// eslint-disable-next-line
_defineOverlayConfig() {
@ -143,6 +147,7 @@ export const ArrowMixinImplementation = superclass =>
/**
* @param {Partial<PopperOptions>} popperConfigToExtendFrom
* @returns {Partial<PopperOptions>}
* @protected
*/
_getPopperArrowConfig(popperConfigToExtendFrom) {
/** @type {Partial<PopperOptions> & { afterWrite: (arg0: Partial<import('@popperjs/core/lib/popper').State>) => void }} */
@ -177,6 +182,7 @@ export const ArrowMixinImplementation = superclass =>
return popperCfg;
}
/** @private */
__setupRepositionCompletePromise() {
this.repositionComplete = new Promise(resolve => {
this.__repositionCompleteResolver = resolve;
@ -189,6 +195,7 @@ export const ArrowMixinImplementation = superclass =>
/**
* @param {Partial<import('@popperjs/core/lib/popper').State>} data
* @private
*/
__syncFromPopperState(data) {
if (!data) {

View file

@ -94,9 +94,13 @@ export class OverlayController extends EventTargetShim {
constructor(config = {}, manager = overlays) {
super();
this.manager = manager;
/** @private */
this.__sharedConfig = config;
/** @type {OverlayConfig} */
/**
* @type {OverlayConfig}
* @protected
*/
this._defaultConfig = {
placementMode: undefined,
contentNode: config.contentNode,
@ -155,7 +159,9 @@ export class OverlayController extends EventTargetShim {
};
this.manager.add(this);
/** @protected */
this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
/** @private */
this.__originalAttrs = new Map();
if (this._defaultConfig.contentNode) {
if (!this._defaultConfig.contentNode.isConnected) {
@ -166,11 +172,17 @@ export class OverlayController extends EventTargetShim {
this.__isContentNodeProjected = Boolean(this._defaultConfig.contentNode.assignedSlot);
}
this.updateConfig(config);
/** @private */
this.__hasActiveTrapsKeyboardFocus = false;
/** @private */
this.__hasActiveBackdrop = true;
/** @type {HTMLElement | undefined} */
/**
* @type {HTMLElement | undefined}
* @private
*/
this.__backdropNodeToBeTornDown = undefined;
/** @private */
this.__escKeyHandler = this.__escKeyHandler.bind(this);
}
@ -377,6 +389,7 @@ export class OverlayController extends EventTargetShim {
* we need to know where we should reappend contentWrapperNode (or contentNode in case it's
* projected).
* @type {HTMLElement}
* @protected
*/
get _renderTarget() {
/** config [g1] */
@ -395,6 +408,7 @@ export class OverlayController extends EventTargetShim {
/**
* @desc The element our local overlay will be positioned relative to.
* @type {HTMLElement | undefined}
* @protected
*/
get _referenceNode() {
return this.referenceNode || this.invokerNode;
@ -430,7 +444,10 @@ export class OverlayController extends EventTargetShim {
// Teardown all previous configs
this.teardown();
/** @type {OverlayConfig} */
/**
* @type {OverlayConfig}
* @private
*/
this.__prevConfig = this.config || {};
/** @type {OverlayConfig} */
@ -452,15 +469,19 @@ export class OverlayController extends EventTargetShim {
},
};
/** @private */
this.__validateConfiguration(/** @type {OverlayConfig} */ (this.config));
// TODO: remove this, so we only have the getters (no setters)
// Object.assign(this, this.config);
/** @protected */
this._init({ cfgToAdd });
/** @private */
this.__elementToFocusAfterHide = undefined;
}
/**
* @param {OverlayConfig} newConfig
* @private
*/
// eslint-disable-next-line class-methods-use-this
__validateConfiguration(newConfig) {
@ -499,6 +520,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ cfgToAdd: OverlayConfig }} options
* @protected
*/
_init({ cfgToAdd }) {
this.__initContentWrapperNode({ cfgToAdd });
@ -514,6 +536,7 @@ export class OverlayController extends EventTargetShim {
this._handleFeatures({ phase: 'init' });
}
/** @private */
__initConnectionTarget() {
// Now, add our node to the right place in dom (renderTarget)
if (this.contentWrapperNode !== this.__prevConfig?.contentWrapperNode) {
@ -544,6 +567,7 @@ export class OverlayController extends EventTargetShim {
* Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
* can lead to problems with event listeners...
* @param {{ cfgToAdd: OverlayConfig }} options
* @private
*/
__initContentWrapperNode({ cfgToAdd }) {
if (this.config?.contentWrapperNode && this.placementMode === 'local') {
@ -578,6 +602,7 @@ export class OverlayController extends EventTargetShim {
/**
* Display local overlays on top of elements with no z-index that appear later in the DOM
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleZIndex({ phase }) {
if (this.placementMode !== 'local') {
@ -594,6 +619,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @private
*/
__setupTeardownAccessibility({ phase }) {
if (phase === 'init') {
@ -634,6 +660,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {HTMLElement} node
* @param {string[]} attrs
* @private
*/
__storeOriginalAttrs(node, attrs) {
const attrMap = {};
@ -643,6 +670,7 @@ export class OverlayController extends EventTargetShim {
this.__originalAttrs.set(node, attrMap);
}
/** @private */
__restoreOriginalAttrs() {
for (const [node, attrMap] of this.__originalAttrs) {
Object.entries(attrMap).forEach(([attrName, value]) => {
@ -706,6 +734,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
async _handlePosition({ phase }) {
if (this.placementMode === 'global') {
@ -729,6 +758,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_keepBodySize({ phase }) {
switch (phase) {
@ -817,6 +847,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} hideConfig
* @protected
*/
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async _transitionHide(hideConfig) {
@ -874,6 +905,7 @@ export class OverlayController extends EventTargetShim {
}
}
/** @protected */
_restoreFocus() {
// We only are allowed to move focus if we (still) 'own' it.
// Otherwise we assume the 'outside world' has, purposefully, taken over
@ -894,6 +926,7 @@ export class OverlayController extends EventTargetShim {
/**
* All features are handled here.
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleFeatures({ phase }) {
this._handleZIndex({ phase });
@ -929,6 +962,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handlePreventsScroll({ phase }) {
switch (phase) {
@ -944,6 +978,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleBlocking({ phase }) {
switch (phase) {
@ -966,6 +1001,7 @@ export class OverlayController extends EventTargetShim {
* it is removed. Otherwise this is the first time displaying a backdrop, so a animation-in
* animation is played.
* @param {{ animation?: boolean, phase: OverlayPhase }} config
* @protected
*/
_handleBackdrop({ phase }) {
switch (phase) {
@ -1026,6 +1062,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleTrapsKeyboardFocus({ phase }) {
if (phase === 'show') {
@ -1063,12 +1100,14 @@ export class OverlayController extends EventTargetShim {
}
}
/** @private */
__escKeyHandler(/** @type {KeyboardEvent} */ ev) {
return ev.key === 'Escape' && this.hide();
}
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleHidesOnEsc({ phase }) {
if (phase === 'show') {
@ -1086,6 +1125,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleHidesOnOutsideEsc({ phase }) {
if (phase === 'show') {
@ -1097,6 +1137,7 @@ export class OverlayController extends EventTargetShim {
}
}
/** @protected */
_handleInheritsReferenceWidth() {
if (!this._referenceNode || this.placementMode === 'global') {
return;
@ -1119,6 +1160,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleHidesOnOutsideClick({ phase }) {
const addOrRemoveListener = phase === 'show' ? 'addEventListener' : 'removeEventListener';
@ -1210,6 +1252,7 @@ export class OverlayController extends EventTargetShim {
/**
* @param {{ phase: OverlayPhase }} config
* @protected
*/
_handleAccessibility({ phase }) {
if (phase === 'init' || phase === 'teardown') {
@ -1231,6 +1274,7 @@ export class OverlayController extends EventTargetShim {
this._teardownContentWrapperNode();
}
/** @protected */
_teardownContentWrapperNode() {
if (
this.placementMode === 'global' &&
@ -1241,6 +1285,7 @@ export class OverlayController extends EventTargetShim {
}
}
/** @private */
async __createPopperInstance() {
if (this._popper) {
this._popper.destroy();

View file

@ -26,6 +26,7 @@ export const OverlayMixinImplementation = superclass =>
constructor() {
super();
this.opened = false;
/** @private */
this.__needsSetup = true;
/** @type {OverlayConfig} */
this.config = {};
@ -73,6 +74,7 @@ export const OverlayMixinImplementation = superclass =>
* In case overriding _defineOverlayConfig is not enough
* @param {DefineOverlayConfig} config
* @returns {OverlayController}
* @protected
*/
// eslint-disable-next-line
_defineOverlay({ contentNode, invokerNode, referenceNode, backdropNode, contentWrapperNode }) {
@ -102,6 +104,7 @@ export const OverlayMixinImplementation = superclass =>
* @desc returns an object with default configuration options for your overlay component.
* This is generally speaking easier to override than _defineOverlay method entirely.
* @returns {OverlayConfig}
* @protected
*/
// eslint-disable-next-line
_defineOverlayConfig() {
@ -125,6 +128,7 @@ export const OverlayMixinImplementation = superclass =>
* @overridable
* @desc use this method to setup your open and close event listeners
* For example, set a click event listener on _overlayInvokerNode to set opened to true
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_setupOpenCloseListeners() {
@ -146,6 +150,7 @@ export const OverlayMixinImplementation = superclass =>
/**
* @overridable
* @desc use this method to tear down your event listeners
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_teardownOpenCloseListeners() {
@ -207,6 +212,7 @@ export const OverlayMixinImplementation = superclass =>
return this.shadowRoot.querySelector('#overlay-content-node-wrapper');
}
/** @protected */
_setupOverlayCtrl() {
/** @type {OverlayController} */
this._overlayCtrl = this._defineOverlay({
@ -221,6 +227,7 @@ export const OverlayMixinImplementation = superclass =>
this._setupOpenCloseListeners();
}
/** @protected */
_teardownOverlayCtrl() {
this._teardownOpenCloseListeners();
this.__teardownSyncFromOverlayController();
@ -234,6 +241,7 @@ export const OverlayMixinImplementation = superclass =>
* (intercepted in before-hide for instance), so that we need to sync the controller state
* to this webcomponent again, preventing eternal loops.
* @param {boolean} newOpened
* @protected
*/
async _setOpenedWithoutPropertyEffects(newOpened) {
this.__blockSyncToOverlayCtrl = true;
@ -242,6 +250,7 @@ export const OverlayMixinImplementation = superclass =>
this.__blockSyncToOverlayCtrl = false;
}
/** @private */
__setupSyncFromOverlayController() {
this.__onOverlayCtrlShow = () => {
this.opened = true;
@ -292,6 +301,7 @@ export const OverlayMixinImplementation = superclass =>
(this._overlayCtrl).addEventListener('before-hide', this.__onBeforeHide);
}
/** @private */
__teardownSyncFromOverlayController() {
/** @type {OverlayController} */
(this._overlayCtrl).removeEventListener(
@ -312,6 +322,7 @@ export const OverlayMixinImplementation = superclass =>
);
}
/** @private */
__syncToOverlayController() {
if (this.opened) {
/** @type {OverlayController} */

View file

@ -56,12 +56,22 @@ export class OverlaysManager {
}
constructor() {
/** @type {OverlayController[]} */
/**
* @type {OverlayController[]}
* @private
*/
this.__list = [];
/** @type {OverlayController[]} */
/**
* @type {OverlayController[]}
* @private
*/
this.__shownList = [];
/** @private */
this.__siblingsInert = false;
/** @type {WeakMap<OverlayController, OverlayController[]>} */
/**
* @type {WeakMap<OverlayController, OverlayController[]>}
* @private
*/
this.__blockingMap = new WeakMap();
}

View file

@ -15,6 +15,18 @@ import { mimicClick } from '../test-helpers.js';
* @typedef {import('../types/OverlayConfig').ViewportPlacement} ViewportPlacement
*/
/**
* @param {OverlayController} overlayControllerEl
*/
function getProtectedMembers(overlayControllerEl) {
// @ts-ignore
const { _contentId: contentId, _renderTarget: renderTarget } = overlayControllerEl;
return {
contentId,
renderTarget,
};
}
const withGlobalTestConfig = () =>
/** @type {OverlayConfig} */ ({
placementMode: 'global',
@ -133,7 +145,8 @@ describe('OverlayController', () => {
const ctrl = new OverlayController({
...withGlobalTestConfig(),
});
expect(ctrl._renderTarget).to.equal(overlays.globalRootNode);
const { renderTarget } = getProtectedMembers(ctrl);
expect(renderTarget).to.equal(overlays.globalRootNode);
});
it.skip('creates local target next to sibling for placement mode "local"', async () => {
@ -141,7 +154,8 @@ describe('OverlayController', () => {
...withLocalTestConfig(),
invokerNode: /** @type {HTMLElement} */ (await fixture(html`<button>Invoker</button>`)),
});
expect(ctrl._renderTarget).to.be.undefined;
const { renderTarget } = getProtectedMembers(ctrl);
expect(renderTarget).to.be.undefined;
expect(ctrl.content).to.equal(ctrl.invokerNode?.nextElementSibling);
});
@ -156,7 +170,8 @@ describe('OverlayController', () => {
...withLocalTestConfig(),
contentNode,
});
expect(ctrl._renderTarget).to.equal(parentNode);
const { renderTarget } = getProtectedMembers(ctrl);
expect(renderTarget).to.equal(parentNode);
});
it('throws when passing a content node that was created "offline"', async () => {
@ -179,8 +194,10 @@ describe('OverlayController', () => {
...withLocalTestConfig(),
contentNode,
});
const { renderTarget } = getProtectedMembers(overlay);
expect(overlay.contentNode.isConnected).to.be.true;
expect(overlay._renderTarget).to.not.be.undefined;
expect(renderTarget).to.not.be.undefined;
});
});
});
@ -1265,7 +1282,8 @@ describe('OverlayController', () => {
...withLocalTestConfig(),
handlesAccessibility: true,
});
expect(ctrl.contentNode.id).to.contain(ctrl._contentId);
const { contentId } = getProtectedMembers(ctrl);
expect(ctrl.contentNode.id).to.contain(contentId);
});
it('preserves content id when present', async () => {
@ -1418,7 +1436,9 @@ describe('OverlayController', () => {
isTooltip: true,
invokerNode,
});
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(ctrl._contentId);
const { contentId } = getProtectedMembers(ctrl);
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(contentId);
});
it('adds [aria-labelledby] on invoker when invokerRelation is label', async () => {
@ -1432,8 +1452,10 @@ describe('OverlayController', () => {
invokerRelation: 'label',
invokerNode,
});
const { contentId } = getProtectedMembers(ctrl);
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(null);
expect(ctrl.invokerNode?.getAttribute('aria-labelledby')).to.equal(ctrl._contentId);
expect(ctrl.invokerNode?.getAttribute('aria-labelledby')).to.equal(contentId);
});
it('adds [role=tooltip] on content', async () => {

View file

@ -18,13 +18,13 @@ export declare class ArrowHost {
static get styles(): CSSResultArray;
render(): TemplateResult;
_arrowTemplate(): TemplateResult;
_arrowNodeTemplate(): TemplateResult;
_defineOverlayConfig(): OverlayConfig;
_getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
__setupRepositionCompletePromise(): void;
protected _arrowTemplate(): TemplateResult;
protected _arrowNodeTemplate(): TemplateResult;
protected _defineOverlayConfig(): OverlayConfig;
protected _getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
private __setupRepositionCompletePromise(): void;
get _arrowNode(): Element | null;
__syncFromPopperState(data: Partial<State>): void;
private __syncFromPopperState(data: Partial<State>): void;
}
export declare function ArrowImplementation<T extends Constructor<LitElement>>(

View file

@ -115,6 +115,7 @@ export class LionPagination extends LocalizeMixin(LitElement) {
constructor() {
super();
/** @private */
this.__visiblePages = 5;
this.current = 1;
this.count = 0;

View file

@ -67,6 +67,7 @@ export class LionProgressIndicator extends LocalizeMixin(LitElement) {
];
}
/** @protected */
_graphicTemplate() {
return nothing;
}

View file

@ -84,6 +84,7 @@ export class LionSelectInvoker extends LionButton {
this.type = 'button';
}
/** @private */
// eslint-disable-next-line class-methods-use-this
__handleKeydown(/** @type {KeyboardEvent} */ event) {
switch (event.key) {
@ -104,6 +105,7 @@ export class LionSelectInvoker extends LionButton {
this.removeEventListener('keydown', this.__handleKeydown);
}
/** @protected */
_contentTemplate() {
if (this.selectedElement) {
const labelNodes = Array.from(this.selectedElement.childNodes);
@ -117,16 +119,19 @@ export class LionSelectInvoker extends LionButton {
/**
* To be overriden for a placeholder, used when `hasNoDefaultSelected` is true on the select rich
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_noSelectionTemplate() {
return html``;
}
/** @protected */
_beforeTemplate() {
return html` <div id="content-wrapper">${this._contentTemplate()}</div> `;
}
/** @protected */
// eslint-disable-next-line class-methods-use-this
_afterTemplate() {
return html`${!this.singleOption ? html`<slot name="after"></slot>` : ''}`;

View file

@ -53,6 +53,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @enhance FormControlMixin
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate() {
@ -118,14 +119,22 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
this.interactionMode = 'auto';
this.singleOption = false;
/** @protected */
this._arrowWidth = 28;
/** @private */
this.__onKeyUp = this.__onKeyUp.bind(this);
/** @private */
this.__invokerOnBlur = this.__invokerOnBlur.bind(this);
/** @private */
this.__overlayOnHide = this.__overlayOnHide.bind(this);
/** @private */
this.__overlayOnShow = this.__overlayOnShow.bind(this);
/** @private */
this.__invokerOnClick = this.__invokerOnClick.bind(this);
/** @private */
this.__overlayBeforeShow = this.__overlayBeforeShow.bind(this);
/** @protected */
this._listboxOnClick = this._listboxOnClick.bind(this);
}
@ -248,20 +257,24 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
* In the select disabled options are still going to a possible value for example
* when prefilling or programmatically setting it.
* @override ChoiceGroupMixin
* @protected
*/
_getCheckedElements() {
return this.formElements.filter(el => el.checked);
}
/** @protected */
_onFormElementsChanged() {
this.singleOption = this.formElements.length === 1;
this._invokerNode.singleOption = this.singleOption;
}
/** @private */
__initInteractionStates() {
this.initInteractionState();
}
/** @private */
__toggleInvokerDisabled() {
if (this._invokerNode) {
this._invokerNode.disabled = this.disabled;
@ -269,6 +282,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
}
}
/** @private */
__syncInvokerElement() {
// sync to invoker
if (this._invokerNode) {
@ -285,6 +299,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
}
}
/** @private */
__setupInvokerNode() {
this._invokerNode.id = `invoker-${this._inputId}`;
this._invokerNode.setAttribute('aria-haspopup', 'listbox');
@ -292,22 +307,26 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
this.__setupInvokerNodeEventListener();
}
/** @private */
__invokerOnClick() {
if (!this.disabled && !this.readOnly && !this.singleOption && !this.__blockListShow) {
this._overlayCtrl.toggle();
}
}
/** @private */
__invokerOnBlur() {
this.dispatchEvent(new Event('blur'));
}
/** @private */
__setupInvokerNodeEventListener() {
this._invokerNode.addEventListener('click', this.__invokerOnClick);
this._invokerNode.addEventListener('blur', this.__invokerOnBlur);
}
/** @private */
__teardownInvokerNode() {
this._invokerNode.removeEventListener('click', this.__invokerOnClick);
this._invokerNode.removeEventListener('blur', this.__invokerOnBlur);
@ -315,6 +334,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @configure OverlayMixin
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
@ -328,6 +348,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
* By default, we will set it to 'min', and then set it back to what it was initially when
* something is selected.
* As a subclasser you can override this behavior.
* @protected
*/
_noDefaultSelectedInheritsWidth() {
if (this.checkedIndex === -1) {
@ -339,12 +360,14 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
}
}
/** @private */
__overlayBeforeShow() {
if (this.hasNoDefaultSelected) {
this._noDefaultSelectedInheritsWidth();
}
}
/** @private */
__overlayOnShow() {
if (this.checkedIndex != null) {
this.activeIndex = /** @type {number} */ (this.checkedIndex);
@ -352,12 +375,14 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
this._listboxNode.focus();
}
/** @private */
__overlayOnHide() {
this._invokerNode.focus();
}
/**
* @enhance OverlayMixin
* @protected
*/
_setupOverlayCtrl() {
super._setupOverlayCtrl();
@ -372,6 +397,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @enhance OverlayMixin
* @protected
*/
_teardownOverlayCtrl() {
super._teardownOverlayCtrl();
@ -383,6 +409,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* Align invoker width with content width
* Make sure display is not set to "none" while calculating the content width
* @protected
*/
async _alignInvokerWidth() {
if (this._overlayCtrl && this._overlayCtrl.content) {
@ -410,6 +437,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @configure FormControlMixin
* @protected
*/
_onLabelClick() {
this._invokerNode.focus();
@ -417,6 +445,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @configure OverlayMixin
* @protected
*/
get _overlayInvokerNode() {
return this._invokerNode;
@ -424,6 +453,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @configure OverlayMixin
* @protected
*/
get _overlayContentNode() {
return this._listboxNode;
@ -431,6 +461,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* @param {KeyboardEvent} ev
* @private
*/
__onKeyUp(ev) {
if (this.disabled) {
@ -474,6 +505,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
* an item.
*
* @param {KeyboardEvent} ev - the keydown event object
* @protected
*/
_listboxOnKeyDown(ev) {
super._listboxOnKeyDown(ev);
@ -502,15 +534,18 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
}
}
/** @protected */
_listboxOnClick() {
this.opened = false;
}
/** @protected */
_setupListboxNode() {
super._setupListboxNode();
this._listboxNode.addEventListener('click', this._listboxOnClick);
}
/** @protected */
_teardownListboxNode() {
super._teardownListboxNode();
if (this._listboxNode) {
@ -521,6 +556,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
/**
* Normally, when textbox gets focus or a char is typed, it opens listbox.
* In transition phases (like clicking option) we prevent this.
* @private
*/
__blockListShowDuringTransition() {
this.__blockListShow = true;

View file

@ -84,6 +84,7 @@ export class LionSelect extends LionFieldWithSelect {
this._inputNode.removeEventListener('change', this._proxyChangeEvent);
}
/** @protected */
_proxyChangeEvent() {
this.dispatchEvent(
new CustomEvent('user-input-changed', {

View file

@ -2,6 +2,7 @@ const sym = Symbol.for('lion::SingletonManagerClassStorage');
export class SingletonManagerClass {
constructor() {
/** protected */
this._map = window[sym] ? window[sym] : (window[sym] = new Map());
}

View file

@ -30,12 +30,14 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
* Therefore we do a full override and typecast to an intersection type that includes LionSwitchButton
* @returns {LionSwitchButton}
*/
// @ts-ignore
get _inputNode() {
return /** @type {LionSwitchButton} */ (Array.from(this.children).find(
el => el.slot === 'input',
));
}
// @ts-ignore
get slots() {
return {
...super.slots,
@ -62,10 +64,12 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
`;
}
/** @protected */
_groupOneTemplate() {
return html`${this._labelTemplate()} ${this._helpTextTemplate()} ${this._feedbackTemplate()}`;
}
/** @protected */
_groupTwoTemplate() {
return html`${this._inputGroupTemplate()}`;
}
@ -74,6 +78,7 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
super();
this.role = 'switch';
this.checked = false;
/** @private */
this.__handleButtonSwitchCheckedChanged = this.__handleButtonSwitchCheckedChanged.bind(this);
}
@ -109,16 +114,19 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
/**
* Override this function from ChoiceInputMixin.
* @protected
*/
// eslint-disable-next-line class-methods-use-this
_isEmpty() {
return false;
}
/** @private */
__handleButtonSwitchCheckedChanged() {
this.checked = this._inputNode.checked;
}
/** @protected */
_syncButtonSwitch() {
this._inputNode.disabled = this.disabled;
}

View file

@ -77,8 +77,11 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
this.role = 'switch';
this.checked = false;
/** @protected */
this._toggleChecked = this._toggleChecked.bind(this);
/** @private */
this.__handleKeydown = this.__handleKeydown.bind(this);
/** @private */
this.__handleKeyup = this.__handleKeyup.bind(this);
}
@ -97,6 +100,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
this.removeEventListener('keyup', this.__handleKeyup);
}
/** @protected */
_toggleChecked() {
if (this.disabled) {
return;
@ -106,6 +110,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
this.checked = !this.checked;
}
/** @private */
__checkedStateChange() {
this.dispatchEvent(
new Event('checked-changed', {
@ -118,6 +123,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
/**
* @param {KeyboardEvent} e
* @private
*/
// eslint-disable-next-line class-methods-use-this
__handleKeydown(e) {
@ -129,6 +135,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
/**
* @param {KeyboardEvent} e
* @private
*/
__handleKeyup(e) {
if ([32 /* space */, 13 /* enter */].indexOf(e.keyCode) !== -1) {

View file

@ -7,6 +7,16 @@ import '@lion/switch/define';
* @typedef {import('@lion/core').TemplateResult} TemplateResult
*/
/**
* @param {LionSwitch} lionSwitchEl
*/
function getProtectedMembers(lionSwitchEl) {
return {
// @ts-ignore
inputNode: lionSwitchEl._inputNode,
};
}
const fixture = /** @type {(arg: TemplateResult) => Promise<LionSwitch>} */ (_fixture);
describe('lion-switch', () => {
@ -31,13 +41,14 @@ describe('lion-switch', () => {
it('should sync its "disabled" state to child button', async () => {
const el = await fixture(html`<lion-switch disabled></lion-switch>`);
expect(el._inputNode.disabled).to.be.true;
expect(el._inputNode.hasAttribute('disabled')).to.be.true;
const { inputNode } = getProtectedMembers(el);
expect(inputNode.disabled).to.be.true;
expect(inputNode.hasAttribute('disabled')).to.be.true;
el.disabled = false;
await el.updateComplete;
await el.updateComplete; // safari takes longer
expect(el._inputNode.disabled).to.be.false;
expect(el._inputNode.hasAttribute('disabled')).to.be.false;
expect(inputNode.disabled).to.be.false;
expect(inputNode.hasAttribute('disabled')).to.be.false;
});
it('is hidden when attribute hidden is true', async () => {
@ -47,20 +58,23 @@ describe('lion-switch', () => {
it('should sync its "checked" state to child button', async () => {
const uncheckedEl = await fixture(html`<lion-switch></lion-switch>`);
const { inputNode: uncheckeInputNode } = getProtectedMembers(uncheckedEl);
const checkedEl = await fixture(html`<lion-switch checked></lion-switch>`);
expect(uncheckedEl._inputNode.checked).to.be.false;
expect(checkedEl._inputNode.checked).to.be.true;
const { inputNode: checkeInputNode } = getProtectedMembers(checkedEl);
expect(uncheckeInputNode.checked).to.be.false;
expect(checkeInputNode.checked).to.be.true;
uncheckedEl.checked = true;
checkedEl.checked = false;
await uncheckedEl.updateComplete;
await checkedEl.updateComplete;
expect(uncheckedEl._inputNode.checked).to.be.true;
expect(checkedEl._inputNode.checked).to.be.false;
expect(uncheckeInputNode.checked).to.be.true;
expect(checkeInputNode.checked).to.be.false;
});
it('should sync "checked" state received from child button', async () => {
const el = await fixture(html`<lion-switch></lion-switch>`);
const button = el._inputNode;
const { inputNode } = getProtectedMembers(el);
const button = inputNode;
expect(el.checked).to.be.false;
button.click();
expect(el.checked).to.be.true;
@ -86,8 +100,9 @@ describe('lion-switch', () => {
it('should dispatch "checked-changed" event when toggled via button or label', async () => {
const handlerSpy = sinon.spy();
const el = await fixture(html`<lion-switch .choiceValue=${'foo'}></lion-switch>`);
const { inputNode } = getProtectedMembers(el);
el.addEventListener('checked-changed', handlerSpy);
el._inputNode.click();
inputNode.click();
el._labelNode.click();
await el.updateComplete;
expect(handlerSpy.callCount).to.equal(2);

View file

@ -166,6 +166,7 @@ export class LionTabs extends LitElement {
this.__setupSlots();
}
/** @private */
__setupSlots() {
if (this.shadowRoot) {
const tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
@ -181,6 +182,7 @@ export class LionTabs extends LitElement {
}
}
/** @private */
__setupStore() {
/** @type {StoreEntry[]} */
this.__store = [];
@ -222,6 +224,7 @@ export class LionTabs extends LitElement {
});
}
/** @private */
__cleanStore() {
if (!this.__store) {
return;
@ -235,6 +238,7 @@ export class LionTabs extends LitElement {
/**
* @param {number} index
* @returns {EventHandlerNonNull}
* @private
*/
__createButtonClickHandler(index) {
return () => {
@ -244,6 +248,7 @@ export class LionTabs extends LitElement {
/**
* @param {Event} ev
* @private
*/
__handleButtonKeyup(ev) {
const _ev = /** @type {KeyboardEvent} */ (ev);
@ -289,6 +294,7 @@ export class LionTabs extends LitElement {
/**
* @param {number} value The new index for focus
* @protected
*/
_setSelectedIndexWithFocus(value) {
const stale = this.__selectedIndex;
@ -305,10 +311,12 @@ export class LionTabs extends LitElement {
return this.__selectedIndex || 0;
}
/** @protected */
get _pairCount() {
return (this.__store && this.__store.length) || 0;
}
/** @private */
__updateSelected(withFocus = false) {
if (
!(this.__store && typeof this.selectedIndex === 'number' && this.__store[this.selectedIndex])

View file

@ -302,6 +302,8 @@ describe('<lion-tabs>', () => {
<div slot="panel">panel 2</div>
</lion-tabs>
`));
// @ts-ignore : this el is LionTabs
el._setSelectedIndexWithFocus(1);
expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be.true;
});

View file

@ -43,6 +43,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
};
}
// @ts-ignore
get slots() {
return {
...super.slots,
@ -95,7 +96,8 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
if (changedProperties.has('rows')) {
const native = this._inputNode;
if (native) {
native.rows = this.rows;
// eslint-disable-next-line dot-notation
native['rows'] = this.rows;
}
}
@ -169,6 +171,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
autosize.update(this._inputNode);
}
/** @private */
__initializeAutoresize() {
// @ts-ignore this property is added by webcomponentsjs polyfill for old browsers
if (this.__shady_native_contains) {
@ -181,6 +184,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
}
}
/** @private */
async __waitForTextareaRenderedInRealDOM() {
let count = 3; // max tasks to wait for
// @ts-ignore this property is added by webcomponentsjs polyfill for old browsers
@ -191,6 +195,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
}
}
/** @private */
__startAutoresize() {
autosize(this._inputNode);
this.setTextareaMaxHeight();

View file

@ -9,6 +9,16 @@ import '@lion/textarea/define';
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture);
/**
* @param {LionTextarea} lionTextareaEl
*/
function getProtectedMembers(lionTextareaEl) {
const { _inputNode: input } = lionTextareaEl;
return {
input,
};
}
function hasBrowserResizeSupport() {
const textarea = document.createElement('textarea');
return textarea.style.resize !== undefined;
@ -31,20 +41,25 @@ describe('<lion-textarea>', () => {
it('has .readOnly=false .rows=2 and rows="2" by default', async () => {
const el = await fixture(`<lion-textarea>foo</lion-textarea>`);
const { input } = getProtectedMembers(el);
expect(el.rows).to.equal(2);
expect(el.getAttribute('rows')).to.be.equal('2');
expect(el._inputNode.rows).to.equal(2);
expect(el._inputNode.getAttribute('rows')).to.be.equal('2');
// @ts-ignore
expect(input.rows).to.equal(2);
expect(input.getAttribute('rows')).to.be.equal('2');
expect(el.readOnly).to.be.false;
expect(el._inputNode.hasAttribute('readonly')).to.be.false;
expect(input.hasAttribute('readonly')).to.be.false;
});
it('sync rows down to the native textarea', async () => {
const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`);
const { input } = getProtectedMembers(el);
expect(el.rows).to.equal(8);
expect(el.getAttribute('rows')).to.be.equal('8');
expect(el._inputNode.rows).to.equal(8);
expect(el._inputNode.getAttribute('rows')).to.be.equal('8');
// @ts-ignore
expect(input.rows).to.equal(8);
expect(input.getAttribute('rows')).to.be.equal('8');
});
it('sync readOnly to the native textarea', async () => {
@ -59,7 +74,8 @@ describe('<lion-textarea>', () => {
}
const el = await fixture(`<lion-textarea></lion-textarea>`);
const computedStyle = window.getComputedStyle(el._inputNode);
const { input } = getProtectedMembers(el);
const computedStyle = window.getComputedStyle(input);
expect(computedStyle.resize).to.equal('none');
});
@ -139,13 +155,14 @@ describe('<lion-textarea>', () => {
it('has an attribute that can be used to set the placeholder text of the textarea', async () => {
const el = await fixture(`<lion-textarea placeholder="text"></lion-textarea>`);
const { input } = getProtectedMembers(el);
expect(el.getAttribute('placeholder')).to.equal('text');
expect(el._inputNode.getAttribute('placeholder')).to.equal('text');
expect(input.getAttribute('placeholder')).to.equal('text');
el.placeholder = 'foo';
await el.updateComplete;
expect(el.getAttribute('placeholder')).to.equal('foo');
expect(el._inputNode.getAttribute('placeholder')).to.equal('foo');
expect(input.getAttribute('placeholder')).to.equal('foo');
});
it('fires resize textarea when a visibility change has been detected', async () => {

View file

@ -49,10 +49,13 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
* @type {'label'|'description'}
*/
this.invokerRelation = 'description';
/** @protected */
this._mouseActive = false;
/** @protected */
this._keyActive = false;
}
/** @protected */
// eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() {
return /** @type {OverlayConfig} */ ({
@ -67,6 +70,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
});
}
/** @protected */
_hasDisabledInvoker() {
if (this._overlayCtrl && this._overlayCtrl.invoker) {
return (
@ -77,6 +81,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
return false;
}
/** @protected */
_setupOpenCloseListeners() {
super._setupOpenCloseListeners();
this.__resetActive = this.__resetActive.bind(this);
@ -92,6 +97,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
this._overlayInvokerNode.addEventListener('focusout', this._hideKey);
}
/** @protected */
_teardownOpenCloseListeners() {
super._teardownOpenCloseListeners();
this._overlayCtrl.removeEventListener('hide', this.__resetActive);
@ -101,11 +107,13 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
}
/** @private */
__resetActive() {
this._mouseActive = false;
this._keyActive = false;
}
/** @protected */
_showMouse() {
if (!this._keyActive) {
this._mouseActive = true;
@ -115,12 +123,14 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
}
}
/** @protected */
_hideMouse() {
if (!this._keyActive) {
this.opened = false;
}
}
/** @protected */
_showKey() {
if (!this._mouseActive) {
this._keyActive = true;
@ -130,6 +140,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
}
}
/** @protected */
_hideKey() {
if (!this._mouseActive) {
this.opened = false;

View file

@ -4,30 +4,45 @@ import { localize } from '@lion/localize';
import { Required } from '@lion/form-core';
import { loadDefaultFeedbackMessages } from '../src/loadDefaultFeedbackMessages.js';
/**
* @typedef {import('@lion/form-core').Validator} Validator
*/
/**
* @param {Validator} validatorEl
*/
function getProtectedMembers(validatorEl) {
// @ts-ignore protected members allowed in test
return {
// @ts-ignore
getMessage: (...args) => validatorEl._getMessage(...args),
};
}
describe('loadDefaultFeedbackMessages', () => {
it('will set default feedback message for Required', async () => {
const el = new Required();
expect(await el._getMessage()).to.equals(
const { getMessage } = getProtectedMembers(el);
expect(await getMessage()).to.equals(
'Please configure an error message for "Required" by overriding "static async getMessage()"',
);
loadDefaultFeedbackMessages();
expect(await el._getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
expect(await getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
});
it('will await loading of translations when switching locale', async () => {
const el = new Required();
const { getMessage } = getProtectedMembers(el);
loadDefaultFeedbackMessages();
expect(await el._getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
expect(await el._getMessage({ fieldName: 'user name' })).to.equal(
'Please enter a(n) user name.',
);
expect(await getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
expect(await getMessage({ fieldName: 'user name' })).to.equal('Please enter a(n) user name.');
localize.locale = 'de-DE';
expect(await el._getMessage({ fieldName: 'Password' })).to.equal(
expect(await getMessage({ fieldName: 'Password' })).to.equal(
'Password muss ausgefüllt werden.',
);
expect(await el._getMessage({ fieldName: 'Benutzername' })).to.equal(
expect(await getMessage({ fieldName: 'Benutzername' })).to.equal(
'Benutzername muss ausgefüllt werden.',
);
});