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

View file

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

View file

@ -17,6 +17,7 @@ class Cache {
/** /**
* @type {{[url: string]: {expires: number, data: object} }} * @type {{[url: string]: {expires: number, data: object} }}
* @protected
*/ */
this._cacheObject = {}; this._cacheObject = {};
} }
@ -92,6 +93,7 @@ class Cache {
* Validate cache on each call to the Cache * Validate cache on each call to the Cache
* When the expiration date has passed, the _cacheObject will be replaced by an * When the expiration date has passed, the _cacheObject will be replaced by an
* empty object * empty object
* @protected
*/ */
_validateCache() { _validateCache() {
if (new Date().getTime() > this.expiration) { 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 isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter';
const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' '; const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ';
/**
* @typedef {import('@lion/core').TemplateResult} TemplateResult
*/
export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) { export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) {
static get properties() { static get properties() {
return { 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 // eslint-disable-next-line class-methods-use-this
_beforeTemplate() { _beforeTemplate() {
return html``; return html``;
} }
/**
*
* @returns {TemplateResult} button template
* @protected
*/
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_afterTemplate() { _afterTemplate() {
return html``; return html``;
@ -143,7 +157,10 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
]; ];
} }
/** @type {HTMLButtonElement} */ /**
* @type {HTMLButtonElement}
* @protected
*/
get _nativeButtonNode() { get _nativeButtonNode() {
return /** @type {HTMLButtonElement} */ (Array.from(this.children).find( return /** @type {HTMLButtonElement} */ (Array.from(this.children).find(
child => child.slot === '_button', 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 * 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. * without side effects caused by the click bubbling back up to lion-button.
* @param {Event} ev * @param {Event} ev
* @private
*/ */
async __clickDelegationHandler(ev) { async __clickDelegationHandler(ev) {
// Wait for updateComplete if form is not yet available // Wait for updateComplete if form is not yet available
@ -249,18 +267,27 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
} }
} }
/**
* @private
*/
__setupDelegationInConstructor() { __setupDelegationInConstructor() {
// do not move to connectedCallback, otherwise IE11 breaks. // do not move to connectedCallback, otherwise IE11 breaks.
// more info: https://github.com/ing-bank/lion/issues/179#issuecomment-511763835 // more info: https://github.com/ing-bank/lion/issues/179#issuecomment-511763835
this.addEventListener('click', this.__clickDelegationHandler, true); this.addEventListener('click', this.__clickDelegationHandler, true);
} }
/**
* @private
*/
__setupEvents() { __setupEvents() {
this.addEventListener('mousedown', this.__mousedownHandler); this.addEventListener('mousedown', this.__mousedownHandler);
this.addEventListener('keydown', this.__keydownHandler); this.addEventListener('keydown', this.__keydownHandler);
this.addEventListener('keyup', this.__keyupHandler); this.addEventListener('keyup', this.__keyupHandler);
} }
/**
* @private
*/
__teardownEvents() { __teardownEvents() {
this.removeEventListener('mousedown', this.__mousedownHandler); this.removeEventListener('mousedown', this.__mousedownHandler);
this.removeEventListener('keydown', this.__keydownHandler); this.removeEventListener('keydown', this.__keydownHandler);
@ -268,6 +295,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
this.removeEventListener('click', this.__clickDelegationHandler); this.removeEventListener('click', this.__clickDelegationHandler);
} }
/**
* @private
*/
__mousedownHandler() { __mousedownHandler() {
this.active = true; this.active = true;
const mouseupHandler = () => { const mouseupHandler = () => {
@ -281,6 +311,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
/** /**
* @param {KeyboardEvent} e * @param {KeyboardEvent} e
* @private
*/ */
__keydownHandler(e) { __keydownHandler(e) {
if (this.active || !isKeyboardClickEvent(e)) { if (this.active || !isKeyboardClickEvent(e)) {
@ -309,6 +340,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
/** /**
* @param {KeyboardEvent} e * @param {KeyboardEvent} e
* @private
*/ */
__keyupHandler(e) { __keyupHandler(e) {
if (isKeyboardClickEvent(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 * Prevents that someone who listens outside or on form catches the click event
* @param {Event} e * @param {Event} e
* @private
*/ */
__preventEventLeakage(e) { __preventEventLeakage(e) {
if (e.target === this.__submitAndResetHelperButton) { if (e.target === this.__submitAndResetHelperButton) {
@ -331,6 +364,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
} }
} }
/**
* @private
*/
__setupSubmitAndResetHelperOnConnected() { __setupSubmitAndResetHelperOnConnected() {
this._form = this._nativeButtonNode.form; this._form = this._nativeButtonNode.form;
@ -339,6 +375,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
} }
} }
/**
* @private
*/
__teardownSubmitAndResetHelperOnDisconnected() { __teardownSubmitAndResetHelperOnDisconnected() {
if (this._form) { if (this._form) {
this._form.removeEventListener('click', this.__preventEventLeakage); this._form.removeEventListener('click', this.__preventEventLeakage);

View file

@ -9,6 +9,16 @@ import '@lion/button/define';
* @typedef {import('@lion/button/src/LionButton').LionButton} LionButton * @typedef {import('@lion/button/src/LionButton').LionButton} LionButton
*/ */
/**
* @param {LionButton} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
nativeButtonNode: el._nativeButtonNode,
};
}
describe('lion-button', () => { describe('lion-button', () => {
it('behaves like native `button` in terms of a11y', async () => { it('behaves like native `button` in terms of a11y', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`)); 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 () => { it('has .type="submit" and type="submit" by default', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`)); const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
const { nativeButtonNode } = getProtectedMembers(el);
expect(el.type).to.equal('submit'); expect(el.type).to.equal('submit');
expect(el.getAttribute('type')).to.be.equal('submit'); expect(el.getAttribute('type')).to.be.equal('submit');
expect(el._nativeButtonNode.type).to.equal('submit'); expect(nativeButtonNode.type).to.equal('submit');
expect(el._nativeButtonNode.getAttribute('type')).to.be.equal('submit'); expect(nativeButtonNode.getAttribute('type')).to.be.equal('submit');
}); });
it('sync type down to the native button', async () => { it('sync type down to the native button', async () => {
const el = /** @type {LionButton} */ (await fixture( const el = /** @type {LionButton} */ (await fixture(
`<lion-button type="button">foo</lion-button>`, `<lion-button type="button">foo</lion-button>`,
)); ));
const { nativeButtonNode } = getProtectedMembers(el);
expect(el.type).to.equal('button'); expect(el.type).to.equal('button');
expect(el.getAttribute('type')).to.be.equal('button'); expect(el.getAttribute('type')).to.be.equal('button');
expect(el._nativeButtonNode.type).to.equal('button'); expect(nativeButtonNode.type).to.equal('button');
expect(el._nativeButtonNode.getAttribute('type')).to.be.equal('button'); expect(nativeButtonNode.getAttribute('type')).to.be.equal('button');
}); });
it('hides the native button in the UI', async () => { it('hides the native button in the UI', async () => {
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`)); const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
expect(el._nativeButtonNode.getAttribute('tabindex')).to.equal('-1'); const { nativeButtonNode } = getProtectedMembers(el);
expect(window.getComputedStyle(el._nativeButtonNode).clip).to.equal('rect(0px, 0px, 0px, 0px)');
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 () => { 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 () => { it('has a native button node with aria-hidden set to true', async () => {
const el = /** @type {LionButton} */ (await fixture('<lion-button></lion-button>')); 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 () => { it('is accessible', async () => {

View file

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

View file

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

View file

@ -6,6 +6,16 @@ import '@lion/checkbox-group/define';
* @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup * @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
*/ */
/**
* @param {LionCheckboxIndeterminate} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
subCheckboxes: el._subCheckboxes,
};
}
describe('<lion-checkbox-indeterminate>', () => { describe('<lion-checkbox-indeterminate>', () => {
it('should have type = checkbox', async () => { it('should have type = checkbox', async () => {
// Arrange // Arrange
@ -93,8 +103,10 @@ describe('<lion-checkbox-indeterminate>', () => {
'lion-checkbox-indeterminate', 'lion-checkbox-indeterminate',
)); ));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Act // Act
elIndeterminate._subCheckboxes[0].checked = true; subCheckboxes[0].checked = true;
await el.updateComplete; await el.updateComplete;
// Assert // Assert
@ -115,11 +127,12 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate', 'lion-checkbox-indeterminate',
)); ));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Act // Act
elIndeterminate._subCheckboxes[0].checked = true; subCheckboxes[0].checked = true;
elIndeterminate._subCheckboxes[1].checked = true; subCheckboxes[1].checked = true;
elIndeterminate._subCheckboxes[2].checked = true; subCheckboxes[2].checked = true;
await el.updateComplete; await el.updateComplete;
// Assert // Assert
@ -145,12 +158,13 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act // Act
elIndeterminate._inputNode.click(); elIndeterminate._inputNode.click();
await elIndeterminate.updateComplete; await elIndeterminate.updateComplete;
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Assert // Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false; expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true; expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true; expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[2].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 () => { it('should sync all children when parent is checked (from unchecked to checked)', async () => {
@ -171,12 +185,13 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act // Act
elIndeterminate._inputNode.click(); elIndeterminate._inputNode.click();
await elIndeterminate.updateComplete; await elIndeterminate.updateComplete;
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Assert // Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false; expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true; expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true; expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elIndeterminate?._subCheckboxes[2].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 () => { it('should sync all children when parent is checked (from checked to unchecked)', async () => {
@ -197,12 +212,13 @@ describe('<lion-checkbox-indeterminate>', () => {
// Act // Act
elIndeterminate._inputNode.click(); elIndeterminate._inputNode.click();
await elIndeterminate.updateComplete; await elIndeterminate.updateComplete;
const elProts = getProtectedMembers(elIndeterminate);
// Assert // Assert
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false; expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false; expect(elProts.subCheckboxes[0].hasAttribute('checked')).to.be.false;
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false; expect(elProts.subCheckboxes[1].hasAttribute('checked')).to.be.false;
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.false; expect(elProts.subCheckboxes[2].hasAttribute('checked')).to.be.false;
}); });
it('should work as expected with siblings checkbox-indeterminate', async () => { it('should work as expected with siblings checkbox-indeterminate', async () => {
@ -251,13 +267,18 @@ describe('<lion-checkbox-indeterminate>', () => {
await elFirstIndeterminate.updateComplete; await elFirstIndeterminate.updateComplete;
await elSecondIndeterminate.updateComplete; await elSecondIndeterminate.updateComplete;
const elFirstSubCheckboxes = getProtectedMembers(elFirstIndeterminate);
const elSecondSubCheckboxes = getProtectedMembers(elSecondIndeterminate);
// Assert - the second sibling should not be affected // 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.hasAttribute('indeterminate')).to.be.false;
expect(elFirstIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true; expect(elFirstSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.true;
expect(elFirstIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true; expect(elFirstSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.true;
expect(elSecondIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false; expect(elFirstSubCheckboxes.subCheckboxes[2].hasAttribute('checked')).to.be.true;
expect(elSecondIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
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 () => { it('should work as expected with nested indeterminate checkboxes', async () => {
@ -301,9 +322,13 @@ describe('<lion-checkbox-indeterminate>', () => {
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'#parent-checkbox-indeterminate', '#parent-checkbox-indeterminate',
)); ));
const elNestedSubCheckboxes = getProtectedMembers(elNestedIndeterminate);
const elParentSubCheckboxes = getProtectedMembers(elParentIndeterminate);
// Act - check a nested checkbox // Act - check a nested checkbox
elNestedIndeterminate?._subCheckboxes[0]._inputNode.click(); if (elNestedIndeterminate) {
elNestedSubCheckboxes.subCheckboxes[0]._inputNode.click();
}
await el.updateComplete; await el.updateComplete;
// Assert // Assert
@ -311,8 +336,8 @@ describe('<lion-checkbox-indeterminate>', () => {
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true; expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
// Act - check all nested checkbox // Act - check all nested checkbox
elNestedIndeterminate?._subCheckboxes[1]._inputNode.click(); if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[1]._inputNode.click();
elNestedIndeterminate?._subCheckboxes[2]._inputNode.click(); if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[2]._inputNode.click();
await el.updateComplete; await el.updateComplete;
// Assert // Assert
@ -322,8 +347,12 @@ describe('<lion-checkbox-indeterminate>', () => {
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true; expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
// Act - finally check all remaining checkbox // Act - finally check all remaining checkbox
elParentIndeterminate?._subCheckboxes[0]._inputNode.click(); if (elParentIndeterminate) {
elParentIndeterminate?._subCheckboxes[1]._inputNode.click(); elParentSubCheckboxes.subCheckboxes[0]._inputNode.click();
}
if (elParentIndeterminate) {
elParentSubCheckboxes.subCheckboxes[1]._inputNode.click();
}
await el.updateComplete; await el.updateComplete;
// Assert // Assert
@ -354,11 +383,12 @@ describe('<lion-checkbox-indeterminate>', () => {
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector( const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
'lion-checkbox-indeterminate', 'lion-checkbox-indeterminate',
)); ));
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
// Act // Act
elIndeterminate._subCheckboxes[0].checked = true; subCheckboxes[0].checked = true;
elIndeterminate._subCheckboxes[1].checked = true; subCheckboxes[1].checked = true;
elIndeterminate._subCheckboxes[2].checked = true; subCheckboxes[2].checked = true;
await el.updateComplete; await el.updateComplete;
// Assert // 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 // eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
async _hideAnimation(opts) {} async _hideAnimation(opts) {}
/**
* @protected
*/
get _invokerNode() { get _invokerNode() {
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
child => child.slot === 'invoker', child => child.slot === 'invoker',
); );
} }
/**
* @protected
*/
get _contentNode() { get _contentNode() {
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
child => child.slot === 'content', child => child.slot === 'content',
); );
} }
/**
* @protected
*/
get _contentHeight() { get _contentHeight() {
const size = this._contentNode?.getBoundingClientRect().height || 0; const size = this._contentNode?.getBoundingClientRect().height || 0;
return `${size}px`; return `${size}px`;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -274,16 +274,19 @@ const FormControlMixinImplementation = superclass =>
} }
} }
/** @protected */
_triggerInitialModelValueChangedEvent() { _triggerInitialModelValueChangedEvent() {
this.__dispatchInitialModelValueChangedEvent(); this.__dispatchInitialModelValueChangedEvent();
} }
/** @protected */
_enhanceLightDomClasses() { _enhanceLightDomClasses() {
if (this._inputNode) { if (this._inputNode) {
this._inputNode.classList.add('form-control'); this._inputNode.classList.add('form-control');
} }
} }
/** @protected */
_enhanceLightDomA11y() { _enhanceLightDomA11y() {
const { _inputNode, _labelNode, _helpTextNode, _feedbackNode } = this; const { _inputNode, _labelNode, _helpTextNode, _feedbackNode } = this;
@ -310,6 +313,7 @@ const FormControlMixinImplementation = superclass =>
* When boolean attribute data-label or data-description is found, * 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 * the slot element will be connected to the input via aria-labelledby or aria-describedby
* @param {string[]} additionalSlots * @param {string[]} additionalSlots
* @protected
*/ */
_enhanceLightDomA11yForAdditionalSlots( _enhanceLightDomA11yForAdditionalSlots(
additionalSlots = ['prefix', 'suffix', 'before', 'after'], additionalSlots = ['prefix', 'suffix', 'before', 'after'],
@ -335,6 +339,7 @@ const FormControlMixinImplementation = superclass =>
* @param {string} attrName * @param {string} attrName
* @param {HTMLElement[]} nodes * @param {HTMLElement[]} nodes
* @param {boolean|undefined} reorder * @param {boolean|undefined} reorder
* @private
*/ */
__reflectAriaAttr(attrName, nodes, reorder) { __reflectAriaAttr(attrName, nodes, reorder) {
if (this._inputNode) { if (this._inputNode) {
@ -393,6 +398,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
_groupOneTemplate() { _groupOneTemplate() {
return html` ${this._labelTemplate()} ${this._helpTextTemplate()} `; return html` ${this._labelTemplate()} ${this._helpTextTemplate()} `;
@ -400,6 +406,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
_groupTwoTemplate() { _groupTwoTemplate() {
return html` ${this._inputGroupTemplate()} ${this._feedbackTemplate()} `; return html` ${this._inputGroupTemplate()} ${this._feedbackTemplate()} `;
@ -407,6 +414,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_labelTemplate() { _labelTemplate() {
@ -419,6 +427,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_helpTextTemplate() { _helpTextTemplate() {
@ -431,6 +440,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
_inputGroupTemplate() { _inputGroupTemplate() {
return html` return html`
@ -447,6 +457,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_inputGroupBeforeTemplate() { _inputGroupBeforeTemplate() {
@ -459,6 +470,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult | nothing} * @return {TemplateResult | nothing}
* @protected
*/ */
_inputGroupPrefixTemplate() { _inputGroupPrefixTemplate() {
return !Array.from(this.children).find(child => child.slot === 'prefix') return !Array.from(this.children).find(child => child.slot === 'prefix')
@ -472,6 +484,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_inputGroupInputTemplate() { _inputGroupInputTemplate() {
@ -484,6 +497,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult | nothing} * @return {TemplateResult | nothing}
* @protected
*/ */
_inputGroupSuffixTemplate() { _inputGroupSuffixTemplate() {
return !Array.from(this.children).find(child => child.slot === 'suffix') return !Array.from(this.children).find(child => child.slot === 'suffix')
@ -497,6 +511,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_inputGroupAfterTemplate() { _inputGroupAfterTemplate() {
@ -509,6 +524,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {TemplateResult} * @return {TemplateResult}
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_feedbackTemplate() { _feedbackTemplate() {
@ -522,6 +538,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @param {?} modelValue * @param {?} modelValue
* @return {boolean} * @return {boolean}
* @protected
*/ */
// @ts-ignore FIXME: Move to FormatMixin? Since there we have access to modelValue prop // @ts-ignore FIXME: Move to FormatMixin? Since there we have access to modelValue prop
_isEmpty(modelValue = this.modelValue) { _isEmpty(modelValue = this.modelValue) {
@ -675,6 +692,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @return {Array.<HTMLElement|undefined>} * @return {Array.<HTMLElement|undefined>}
* @protected
*/ */
// Returns dom references to all elements that should be referred to by field(s) // Returns dom references to all elements that should be referred to by field(s)
_getAriaDescriptionElements() { _getAriaDescriptionElements() {
@ -727,6 +745,7 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @param {string} slotName * @param {string} slotName
* @return {HTMLElement | undefined} * @return {HTMLElement | undefined}
* @private
*/ */
__getDirectSlotChild(slotName) { __getDirectSlotChild(slotName) {
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find( return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
@ -734,6 +753,7 @@ const FormControlMixinImplementation = superclass =>
); );
} }
/** @private */
__dispatchInitialModelValueChangedEvent() { __dispatchInitialModelValueChangedEvent() {
// When we are not a fieldset / choice-group, we don't need to wait for our children // When we are not a fieldset / choice-group, we don't need to wait for our children
// to send a unified event // to send a unified event
@ -762,12 +782,14 @@ const FormControlMixinImplementation = superclass =>
/** /**
* @param {CustomEvent} ev * @param {CustomEvent} ev
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this, no-unused-vars // eslint-disable-next-line class-methods-use-this, no-unused-vars
_onBeforeRepropagateChildrenValues(ev) {} _onBeforeRepropagateChildrenValues(ev) {}
/** /**
* @param {CustomEvent} ev * @param {CustomEvent} ev
* @private
*/ */
__repropagateChildrenValues(ev) { __repropagateChildrenValues(ev) {
// Allows sub classes to internally listen to the children change events // 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. * 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 * This will fix the types and reduce the need for ignores/expect-errors
* @param {EventTarget & import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} target * @param {EventTarget & import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} target
* @protected
*/ */
_repropagationCondition(target) { _repropagationCondition(target) {
return !( return !(
@ -861,6 +884,7 @@ const FormControlMixinImplementation = superclass =>
* _onLabelClick() { * _onLabelClick() {
* this._invokerNode.focus(); * this._invokerNode.focus();
* } * }
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_onLabelClick() {} _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 * @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: * set again, so that its observer won't be triggered. Can be:
* 'model'|'formatted'|'serialized'. * 'model'|'formatted'|'serialized'.
* @protected
*/ */
_calculateValues({ source } = { source: null }) { _calculateValues({ source } = { source: null }) {
if (this.__preventRecursiveTrigger) return; // prevent infinite loops if (this.__preventRecursiveTrigger) return; // prevent infinite loops
@ -236,6 +237,7 @@ const FormatMixinImplementation = superclass =>
/** /**
* @param {string|undefined} value * @param {string|undefined} value
* @return {?} * @return {?}
* @private
*/ */
__callParser(value = this.formattedValue) { __callParser(value = this.formattedValue) {
// A) check if we need to parse at all // A) check if we need to parse at all
@ -273,6 +275,7 @@ const FormatMixinImplementation = superclass =>
/** /**
* @returns {string|undefined} * @returns {string|undefined}
* @private
*/ */
__callFormatter() { __callFormatter() {
// - Why check for this.hasError? // - Why check for this.hasError?
@ -309,6 +312,7 @@ const FormatMixinImplementation = superclass =>
/** /**
* Observer Handlers * Observer Handlers
* @param {{ modelValue: unknown; }[]} args * @param {{ modelValue: unknown; }[]} args
* @protected
*/ */
_onModelValueChanged(...args) { _onModelValueChanged(...args) {
this._calculateValues({ source: 'model' }); this._calculateValues({ source: 'model' });
@ -319,6 +323,7 @@ const FormatMixinImplementation = superclass =>
* @param {{ modelValue: unknown; }[]} args * @param {{ modelValue: unknown; }[]} args
* This is wrapped in a distinct method, so that parents can control when the changed event * 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. * is fired. For objects, a deep comparison might be needed.
* @protected
*/ */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
_dispatchModelValueChangedEvent(...args) { _dispatchModelValueChangedEvent(...args) {
@ -339,6 +344,7 @@ const FormatMixinImplementation = superclass =>
* Downwards syncing should only happen for `LionField`.value changes from 'above'. * Downwards syncing should only happen for `LionField`.value changes from 'above'.
* This triggers _onModelValueChanged and connects user input * This triggers _onModelValueChanged and connects user input
* to the parsing/formatting/serializing loop. * to the parsing/formatting/serializing loop.
* @protected
*/ */
_syncValueUpwards() { _syncValueUpwards() {
if (!this.__isHandlingComposition) { if (!this.__isHandlingComposition) {
@ -352,6 +358,7 @@ const FormatMixinImplementation = superclass =>
* - flow [1] will always be reflected back * - flow [1] will always be reflected back
* - flow [2] will not be reflected back when this flow was triggered via * - 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) * `@user-input-changed` (this will happen later, when `formatOn` condition is met)
* @protected
*/ */
_reflectBackFormattedValueToUser() { _reflectBackFormattedValueToUser() {
if (this._reflectBackOn()) { if (this._reflectBackOn()) {
@ -366,6 +373,7 @@ const FormatMixinImplementation = superclass =>
* call `super._reflectBackOn()` * call `super._reflectBackOn()`
* @overridable * @overridable
* @return {boolean} * @return {boolean}
* @protected
*/ */
_reflectBackOn() { _reflectBackOn() {
return !this.__isHandlingUserInput; 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 // ("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 // 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)) // layer on top of other events (input, change, whatever))
/** @protected */
_proxyInputEvent() { _proxyInputEvent() {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('user-input-changed', { new CustomEvent('user-input-changed', {
@ -384,6 +393,7 @@ const FormatMixinImplementation = superclass =>
); );
} }
/** @protected */
_onUserInputChanged() { _onUserInputChanged() {
// Upwards syncing. Most properties are delegated right away, value is synced to // Upwards syncing. Most properties are delegated right away, value is synced to
// `LionField`, to be able to act on (imperatively set) value changes // `LionField`, to be able to act on (imperatively set) value changes

View file

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

View file

@ -71,6 +71,10 @@ export class LionField extends FormControlMixin(
this.submitted = false; this.submitted = false;
} }
/**
* Resets modelValue to initial value.
* Interaction states are cleared
*/
reset() { reset() {
this.modelValue = this._initialModelValue; this.modelValue = this._initialModelValue;
this.resetInteractionState(); 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 this.modelValue = ''; // can't set null here, because IE11 treats it as a string
} }
/**
* Dispatches custom bubble event
* @protected
*/
_onChange() { _onChange() {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('user-input-changed', { 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. * Restores the cursor to its original position after updating the value.
* @param {string} newValue The value that should be saved. * @param {string} newValue The value that should be saved.
* @protected
*/ */
_setValueAndPreserveCaret(newValue) { _setValueAndPreserveCaret(newValue) {
// Only preserve caret if focused (changing selectionStart will move focus in Safari) // 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() { get modelValue() {
const elems = this._getCheckedElements(); const elems = this._getCheckedElements();
if (this.multipleChoice) { if (this.multipleChoice) {
@ -127,15 +128,21 @@ const ChoiceGroupMixinImplementation = superclass =>
constructor() { constructor() {
super(); super();
this.multipleChoice = false; this.multipleChoice = false;
/** @type {'child'|'choice-group'|'fieldset'} */ /** @type {'child'|'choice-group'|'fieldset'}
* @protected
*/
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
/** @private */
this.__isInitialModelValue = true; this.__isInitialModelValue = true;
/** @private */
this.__isInitialSerializedValue = true; this.__isInitialSerializedValue = true;
/** @private */
this.__isInitialFormattedValue = true; this.__isInitialFormattedValue = true;
/** @type {Promise<any> & {done?:boolean}} */ /** @type {Promise<any> & {done?:boolean}} */
this.registrationComplete = new Promise((resolve, reject) => { this.registrationComplete = new Promise((resolve, reject) => {
/** @private */
this.__resolveRegistrationComplete = resolve; this.__resolveRegistrationComplete = resolve;
/** @private */
this.__rejectRegistrationComplete = reject; this.__rejectRegistrationComplete = reject;
}); });
this.registrationComplete.done = false; this.registrationComplete.done = false;
@ -156,6 +163,7 @@ const ChoiceGroupMixinImplementation = superclass =>
super.connectedCallback(); super.connectedCallback();
// Double microtask queue to account for Webkit race condition // Double microtask queue to account for Webkit race condition
Promise.resolve().then(() => Promise.resolve().then(() =>
// @ts-ignore
Promise.resolve().then(() => this.__resolveRegistrationComplete()), Promise.resolve().then(() => this.__resolveRegistrationComplete()),
); );
@ -203,6 +211,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/** /**
* @override from FormControlMixin * @override from FormControlMixin
* @protected
*/ */
_triggerInitialModelValueChangedEvent() { _triggerInitialModelValueChangedEvent() {
this.registrationComplete.then(() => { this.registrationComplete.then(() => {
@ -213,6 +222,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/** /**
* @override * @override
* @param {string} property * @param {string} property
* @protected
*/ */
_getFromAllFormElements(property, filterCondition = () => true) { _getFromAllFormElements(property, filterCondition = () => true) {
// For modelValue, serializedValue and formattedValue, an exception should be made, // For modelValue, serializedValue and formattedValue, an exception should be made,
@ -229,6 +239,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/** /**
* @param {FormControl} child * @param {FormControl} child
* @protected
*/ */
_throwWhenInvalidChildModelValue(child) { _throwWhenInvalidChildModelValue(child) {
if ( if (
@ -246,6 +257,9 @@ const ChoiceGroupMixinImplementation = superclass =>
} }
} }
/**
* @protected
*/
_isEmpty() { _isEmpty() {
if (this.multipleChoice) { if (this.multipleChoice) {
return this.modelValue.length === 0; return this.modelValue.length === 0;
@ -262,6 +276,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/** /**
* @param {CustomEvent & {target:FormControl}} ev * @param {CustomEvent & {target:FormControl}} ev
* @protected
*/ */
_checkSingleChoiceElements(ev) { _checkSingleChoiceElements(ev) {
const { target } = ev; const { target } = ev;
@ -278,6 +293,9 @@ const ChoiceGroupMixinImplementation = superclass =>
// this.__triggerCheckedValueChanged(); // this.__triggerCheckedValueChanged();
} }
/**
* @protected
*/
_getCheckedElements() { _getCheckedElements() {
// We want to filter out disabled values by default // We want to filter out disabled values by default
return this.formElements.filter(el => el.checked && !el.disabled); return this.formElements.filter(el => el.checked && !el.disabled);
@ -286,6 +304,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/** /**
* @param {string | any[]} value * @param {string | any[]} value
* @param {Function} check * @param {Function} check
* @protected
*/ */
_setCheckedElements(value, check) { _setCheckedElements(value, check) {
for (let i = 0; i < this.formElements.length; i += 1) { for (let i = 0; i < this.formElements.length; i += 1) {
@ -309,6 +328,9 @@ const ChoiceGroupMixinImplementation = superclass =>
} }
} }
/**
* @private
*/
__setChoiceGroupTouched() { __setChoiceGroupTouched() {
const value = this.modelValue; const value = this.modelValue;
if (value != null && value !== this.__previousCheckedValue) { if (value != null && value !== this.__previousCheckedValue) {
@ -321,6 +343,7 @@ const ChoiceGroupMixinImplementation = superclass =>
/** /**
* @override FormControlMixin * @override FormControlMixin
* @param {CustomEvent} ev * @param {CustomEvent} ev
* @protected
*/ */
_onBeforeRepropagateChildrenValues(ev) { _onBeforeRepropagateChildrenValues(ev) {
// Normalize target, since we might receive 'portal events' (from children in a modal, // Normalize target, since we might receive 'portal events' (from children in a modal,

View file

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

View file

@ -81,6 +81,7 @@ const FormGroupMixinImplementation = superclass =>
return this; return this;
} }
// @ts-ignore
get modelValue() { get modelValue() {
return this._getFromAllFormElements('modelValue'); return this._getFromAllFormElements('modelValue');
} }
@ -167,6 +168,7 @@ const FormGroupMixinImplementation = superclass =>
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.setAttribute('role', 'group'); this.setAttribute('role', 'group');
// @ts-ignore
Promise.resolve().then(() => this.__resolveRegistrationComplete()); Promise.resolve().then(() => this.__resolveRegistrationComplete());
this.registrationComplete.then(() => { this.registrationComplete.then(() => {
@ -325,6 +327,7 @@ const FormGroupMixinImplementation = superclass =>
*/ */
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) { _getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
const result = {}; const result = {};
// @ts-ignore
this.formElements._keys().forEach(name => { this.formElements._keys().forEach(name => {
const elem = this.formElements[name]; const elem = this.formElements[name];
if (elem instanceof FormControlsCollection) { 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 * @desc Gives back the named keys and filters out array indexes
* @return {string[]} * @return {string[]}
* @protected
*/ */
_keys() { _keys() {
return Object.keys(this).filter(k => Number.isNaN(Number(k))); return Object.keys(this).filter(k => Number.isNaN(Number(k)));

View file

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

View file

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

View file

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

View file

@ -59,6 +59,7 @@ const SyncUpdatableMixinImplementation = superclass =>
* @param {string} name * @param {string} name
* @param {*} newValue * @param {*} newValue
* @param {*} oldValue * @param {*} oldValue
* @private
*/ */
static __syncUpdatableHasChanged(name, newValue, oldValue) { static __syncUpdatableHasChanged(name, newValue, oldValue) {
// @ts-expect-error accessing private lit property // @ts-expect-error accessing private lit property
@ -69,6 +70,7 @@ const SyncUpdatableMixinImplementation = superclass =>
return newValue !== oldValue; return newValue !== oldValue;
} }
/** @private */
__syncUpdatableInitialize() { __syncUpdatableInitialize() {
const ns = this.__SyncUpdatableNamespace; const ns = this.__SyncUpdatableNamespace;
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this 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 | Node | TemplateResult } opts.message message or feedback node or TemplateResult
* @param {string} [opts.type] * @param {string} [opts.type]
* @param {Validator} [opts.validator] * @param {Validator} [opts.validator]
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_messageTemplate({ message }) { _messageTemplate({ message }) {

View file

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

View file

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

View file

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

View file

@ -20,6 +20,15 @@ async function expectThrowsAsync(method, errorMessage) {
expect(error.message).to.equal(errorMessage); expect(error.message).to.equal(errorMessage);
} }
} }
/**
* @param {Validator} validatorEl
*/
function getProtectedMembers(validatorEl) {
return {
// @ts-ignore
getMessage: (...args) => validatorEl._getMessage(...args),
};
}
describe('Validator', () => { describe('Validator', () => {
it('has an "execute" function returning "shown" state', async () => { 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( 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", "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 }); const vali = new MyValidator('myParam', { my: 'config', getMessage: configSpy });
vali._getMessage(); const { getMessage } = getProtectedMembers(vali);
getMessage();
expect(configSpy.args[0][0]).to.deep.equal({ expect(configSpy.args[0][0]).to.deep.equal({
name: 'MyValidator', name: 'MyValidator',
@ -102,7 +115,8 @@ describe('Validator', () => {
} }
} }
const vali = new MyValidator('myParam', { my: 'config' }); const vali = new MyValidator('myParam', { my: 'config' });
vali._getMessage(); const { getMessage } = getProtectedMembers(vali);
getMessage();
expect(data).to.deep.equal({ expect(data).to.deep.equal({
name: 'MyValidator', name: 'MyValidator',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -35,6 +35,16 @@ import '@lion/form-core/define';
* @typedef {import('@lion/listbox').LionOption} LionOption * @typedef {import('@lion/listbox').LionOption} LionOption
*/ */
/**
* @param {FormControl} el
*/
function getProtectedMembers(el) {
return {
// @ts-ignore
repropagationRole: el._repropagationRole,
};
}
const featureName = 'model value'; const featureName = 'model value';
const getFirstPaintTitle = /** @param {number} count */ count => const getFirstPaintTitle = /** @param {number} count */ count =>
@ -386,13 +396,14 @@ describe('detail.isTriggeredByUser', () => {
* @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'} * @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'}
*/ */
function detectType(el) { function detectType(el) {
if (el._repropagationRole === 'child') { const { repropagationRole } = getProtectedMembers(el);
if (repropagationRole === 'child') {
if (featureDetectChoiceField(el)) { if (featureDetectChoiceField(el)) {
return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField'; return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField';
} }
return 'RegularField'; 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 { export class LionForm extends LionFieldset {
constructor() { constructor() {
super(); super();
/** @protected */
this._submit = this._submit.bind(this); this._submit = this._submit.bind(this);
/** @protected */
this._reset = this._reset.bind(this); this._reset = this._reset.bind(this);
} }
@ -48,6 +50,7 @@ export class LionForm extends LionFieldset {
/** /**
* @param {Event} ev * @param {Event} ev
* @protected
*/ */
_submit(ev) { _submit(ev) {
ev.preventDefault(); ev.preventDefault();
@ -66,6 +69,7 @@ export class LionForm extends LionFieldset {
/** /**
* @param {Event} ev * @param {Event} ev
* @protected
*/ */
_reset(ev) { _reset(ev) {
ev.preventDefault(); ev.preventDefault();
@ -74,11 +78,13 @@ export class LionForm extends LionFieldset {
this.dispatchEvent(new Event('reset', { bubbles: true })); this.dispatchEvent(new Event('reset', { bubbles: true }));
} }
/** @private */
__registerEventsForLionForm() { __registerEventsForLionForm() {
this._formNode.addEventListener('submit', this._submit); this._formNode.addEventListener('submit', this._submit);
this._formNode.addEventListener('reset', this._reset); this._formNode.addEventListener('reset', this._reset);
} }
/** @private */
__teardownEventsForLionForm() { __teardownEventsForLionForm() {
this._formNode.removeEventListener('submit', this._submit); this._formNode.removeEventListener('submit', this._submit);
this._formNode.removeEventListener('reset', this._reset); this._formNode.removeEventListener('reset', this._reset);

View file

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

View file

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

View file

@ -7,14 +7,27 @@ import { IconManager } from '../src/IconManager.js';
* @typedef {import("lit-html").TemplateResult} TemplateResult * @typedef {import("lit-html").TemplateResult} TemplateResult
*/ */
/**
* @param {IconManager} iconManagerEl
*/
function getProtectedMembers(iconManagerEl) {
// @ts-ignore
const { __iconResolvers: iconResolvers } = iconManagerEl;
return {
iconResolvers,
};
}
describe('IconManager', () => { describe('IconManager', () => {
it('starts off with an empty map of resolvers', () => { it('starts off with an empty map of resolvers', () => {
const manager = new IconManager(); 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', () => { it('allows adding an icon resolver', () => {
const manager = new IconManager(); const manager = new IconManager();
const { iconResolvers } = getProtectedMembers(manager);
/** /**
* @param {string} iconset * @param {string} iconset
* @param {string} icon * @param {string} icon
@ -24,7 +37,7 @@ describe('IconManager', () => {
const resolver = (iconset, icon) => nothing; const resolver = (iconset, icon) => nothing;
manager.addIconResolver('foo', resolver); 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', () => { 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 * @customElement lion-input-amount
*/ */
// @ts-ignore
export class LionInputAmount extends LocalizeMixin(LionInput) { export class LionInputAmount extends LocalizeMixin(LionInput) {
/** @type {any} */ /** @type {any} */
static get properties() { static get properties() {
@ -68,10 +69,13 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
this.formatter = formatAmount; this.formatter = formatAmount;
/** @type {string | undefined} */ /** @type {string | undefined} */
this.currency = undefined; this.currency = undefined;
/** @private */
this.__isPasting = false; this.__isPasting = false;
this.addEventListener('paste', () => { this.addEventListener('paste', () => {
/** @private */
this.__isPasting = true; this.__isPasting = true;
/** @private */
this.__parserCallcountSincePaste = 0; this.__parserCallcountSincePaste = 0;
}); });
@ -99,17 +103,20 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
/** /**
* @override of FormatMixin * @override of FormatMixin
* @private
*/ */
__callParser(value = this.formattedValue) { __callParser(value = this.formattedValue) {
// TODO: (@daKmor) input and change events both trigger parsing therefore we need to handle the second parse // TODO: (@daKmor) input and change events both trigger parsing therefore we need to handle the second parse
this.__parserCallcountSincePaste += 1; this.__parserCallcountSincePaste += 1;
this.__isPasting = this.__parserCallcountSincePaste === 2; this.__isPasting = this.__parserCallcountSincePaste === 2;
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto'; this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
// @ts-ignore
return super.__callParser(value); return super.__callParser(value);
} }
/** /**
* @override of FormatMixin * @override of FormatMixin
* @protected
*/ */
_reflectBackOn() { _reflectBackOn() {
return super._reflectBackOn() || this.__isPasting; return super._reflectBackOn() || this.__isPasting;
@ -118,6 +125,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
/** /**
* @param {Object} opts * @param {Object} opts
* @param {string} opts.currency * @param {string} opts.currency
* @protected
*/ */
_onCurrencyChanged({ currency }) { _onCurrencyChanged({ currency }) {
if (this._isPrivateSlot('after') && this._currencyDisplayNode) { if (this._isPrivateSlot('after') && this._currencyDisplayNode) {
@ -128,6 +136,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
this.__setCurrencyDisplayLabel(); this.__setCurrencyDisplayLabel();
} }
/** @private */
__setCurrencyDisplayLabel() { __setCurrencyDisplayLabel() {
// TODO: (@erikkroes) for optimal a11y, abbreviations should be part of aria-label // 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 // 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() { __dispatchCloseEvent() {
this.dispatchEvent(new Event('close-overlay')); this.dispatchEvent(new Event('close-overlay'));
} }

View file

@ -169,18 +169,26 @@ export class LionInputDatepicker extends ScopedElementsMixin(
constructor() { constructor() {
super(); super();
/** @private */
this.__invokerId = this.__createUniqueIdForA11y(); this.__invokerId = this.__createUniqueIdForA11y();
/** @protected */
this._calendarInvokerSlot = 'suffix'; this._calendarInvokerSlot = 'suffix';
// Configuration flags for subclassers // Configuration flags for subclassers
/** @protected */
this._focusCentralDateOnCalendarOpen = true; this._focusCentralDateOnCalendarOpen = true;
/** @protected */
this._hideOnUserSelect = true; this._hideOnUserSelect = true;
/** @protected */
this._syncOnUserSelect = true; this._syncOnUserSelect = true;
/** @private */
this.__openCalendarOverlay = this.__openCalendarOverlay.bind(this); this.__openCalendarOverlay = this.__openCalendarOverlay.bind(this);
/** @protected */
this._onCalendarUserSelectedChanged = this._onCalendarUserSelectedChanged.bind(this); this._onCalendarUserSelectedChanged = this._onCalendarUserSelectedChanged.bind(this);
} }
/** @private */
__createUniqueIdForA11y() { __createUniqueIdForA11y() {
return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`; return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`;
} }
@ -197,6 +205,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
} }
} }
/** @private */
__toggleInvokerDisabled() { __toggleInvokerDisabled() {
if (this._invokerNode) { if (this._invokerNode) {
const invokerNode = /** @type {HTMLElement & {disabled: boolean}} */ (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 * Defining this overlay as a templates from OverlayMixin
* this is our source to give as .contentNode to OverlayController. * this is our source to give as .contentNode to OverlayController.
* Important: do not change the name of this method. * Important: do not change the name of this method.
* @protected
*/ */
_overlayTemplate() { _overlayTemplate() {
// TODO: add performance optimization to only render the calendar if needed // 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 * @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); const fixture = /** @type {(arg: TemplateResult) => Promise<LionInputDatepicker>} */ (_fixture);
describe('<lion-input-datepicker>', () => { describe('<lion-input-datepicker>', () => {
@ -278,8 +299,10 @@ describe('<lion-input-datepicker>', () => {
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector( const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector(
'[data-tag-name="lion-calendar"]', '[data-tag-name="lion-calendar"]',
)); ));
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
// First set a fixed date as if selected by a user // 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; await el.updateComplete;
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
@ -303,8 +326,10 @@ describe('<lion-input-datepicker>', () => {
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector( const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector(
'[data-tag-name="lion-calendar"]', '[data-tag-name="lion-calendar"]',
)); ));
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
// First set a fixed date as if selected by a user // 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; await el.updateComplete;
const elObj = new DatepickerInputObject(el); const elObj = new DatepickerInputObject(el);
@ -411,6 +436,7 @@ describe('<lion-input-datepicker>', () => {
const myEl = await fixture(html`<${myTag}></${myTag}>`); const myEl = await fixture(html`<${myTag}></${myTag}>`);
const myElObj = new DatepickerInputObject(myEl); const myElObj = new DatepickerInputObject(myEl);
const { invokerId } = getProtectedMembersDatepicker(myEl);
expect(myElObj.invokerEl.tagName.toLowerCase()).to.equal('my-button'); expect(myElObj.invokerEl.tagName.toLowerCase()).to.equal('my-button');
// All other tests will still pass. Small checkup: // 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-expanded')).to.equal('false');
expect(myElObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog'); expect(myElObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog');
expect(myElObj.invokerEl.getAttribute('slot')).to.equal('suffix'); 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(); await myElObj.openCalendar();
expect(myElObj.overlayController.isShown).to.equal(true); expect(myElObj.overlayController.isShown).to.equal(true);
}); });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,21 @@ import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers/in
import { LocalizeManager } from '../src/LocalizeManager.js'; 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 * @param {string} str
* Useful for IE11 where LTR and RTL symbols are put by Intl when rendering dates * Useful for IE11 where LTR and RTL symbols are put by Intl when rendering dates
@ -84,10 +99,11 @@ describe('LocalizeManager', () => {
describe('addData()', () => { describe('addData()', () => {
it('allows to provide inline data', () => { it('allows to provide inline data', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' }); manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'lion-hello': { greeting: 'Hi!' }, 'lion-hello': { greeting: 'Hi!' },
}, },
@ -95,7 +111,7 @@ describe('LocalizeManager', () => {
manager.addData('en-GB', 'lion-goodbye', { farewell: 'Cheers!' }); manager.addData('en-GB', 'lion-goodbye', { farewell: 'Cheers!' });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'lion-hello': { greeting: 'Hi!' }, 'lion-hello': { greeting: 'Hi!' },
'lion-goodbye': { farewell: 'Cheers!' }, 'lion-goodbye': { farewell: 'Cheers!' },
@ -105,7 +121,7 @@ describe('LocalizeManager', () => {
manager.addData('nl-NL', 'lion-hello', { greeting: 'Hoi!' }); manager.addData('nl-NL', 'lion-hello', { greeting: 'Hoi!' });
manager.addData('nl-NL', 'lion-goodbye', { farewell: 'Doei!' }); manager.addData('nl-NL', 'lion-goodbye', { farewell: 'Doei!' });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'lion-hello': { greeting: 'Hi!' }, 'lion-hello': { greeting: 'Hi!' },
'lion-goodbye': { farewell: 'Cheers!' }, 'lion-goodbye': { farewell: 'Cheers!' },
@ -119,6 +135,7 @@ describe('LocalizeManager', () => {
it('prevents mutating existing data for the same locale & namespace', () => { it('prevents mutating existing data for the same locale & namespace', () => {
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' }); manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
@ -126,7 +143,7 @@ describe('LocalizeManager', () => {
manager.addData('en-GB', 'lion-hello', { greeting: 'Hello!' }); manager.addData('en-GB', 'lion-hello', { greeting: 'Hello!' });
}).to.throw(); }).to.throw();
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'lion-hello': { greeting: 'Hi!' } }, 'en-GB': { 'lion-hello': { greeting: 'Hi!' } },
}); });
}); });
@ -137,13 +154,14 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */ /** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-component': { greeting: 'Hello!' }, 'my-component': { greeting: 'Hello!' },
}, },
@ -154,6 +172,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.locale = 'en-US'; manager.locale = 'en-US';
await manager.loadNamespace( await manager.loadNamespace(
@ -164,7 +183,7 @@ describe('LocalizeManager', () => {
{ locale: 'nl-NL' }, { locale: 'nl-NL' },
); );
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'nl-NL': { 'nl-NL': {
'my-component': { greeting: 'Hello!' }, 'my-component': { greeting: 'Hello!' },
}, },
@ -176,6 +195,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-send-button/en-GB.js', { default: { submit: 'Send' } }); setupFakeImport('./my-send-button/en-GB.js', { default: { submit: 'Send' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
await manager.loadNamespaces([ await manager.loadNamespaces([
{ {
@ -188,7 +208,7 @@ describe('LocalizeManager', () => {
}, },
]); ]);
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-defaults': { submit: 'Submit' }, 'my-defaults': { submit: 'Submit' },
'my-send-button': { submit: 'Send' }, 'my-send-button': { submit: 'Send' },
@ -201,6 +221,7 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-send-button/nl-NL.js', { default: { submit: 'Send' } }); setupFakeImport('./my-send-button/nl-NL.js', { default: { submit: 'Send' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.locale = 'en-US'; manager.locale = 'en-US';
await manager.loadNamespaces( await manager.loadNamespaces(
@ -217,7 +238,7 @@ describe('LocalizeManager', () => {
{ locale: 'nl-NL' }, { locale: 'nl-NL' },
); );
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'nl-NL': { 'nl-NL': {
'my-defaults': { submit: 'Submit' }, 'my-defaults': { submit: 'Submit' },
'my-send-button': { submit: 'Send' }, 'my-send-button': { submit: 'Send' },
@ -229,13 +250,14 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */ /** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-component': { greeting: 'Hello!' }, 'my-component': { greeting: 'Hello!' },
}, },
@ -265,6 +287,7 @@ describe('LocalizeManager', () => {
describe('fallback locale', () => { describe('fallback locale', () => {
it('can load a fallback locale if current one can not be loaded', async () => { it('can load a fallback locale if current one can not be loaded', async () => {
manager = new LocalizeManager({ fallbackLocale: 'en-GB' }); manager = new LocalizeManager({ fallbackLocale: 'en-GB' });
const { storage } = getProtectedMembers(manager);
manager.locale = 'nl-NL'; manager.locale = 'nl-NL';
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
@ -274,7 +297,7 @@ describe('LocalizeManager', () => {
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'nl-NL': { 'nl-NL': {
'my-component': { greeting: 'Hello!' }, '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 () => { it('can load fallback generic language file if fallback locale file is not found', async () => {
manager = new LocalizeManager({ fallbackLocale: 'en-GB' }); manager = new LocalizeManager({ fallbackLocale: 'en-GB' });
const { storage } = getProtectedMembers(manager);
manager.locale = 'nl-NL'; manager.locale = 'nl-NL';
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
@ -292,7 +316,7 @@ describe('LocalizeManager', () => {
'my-component': locale => fakeImport(`./my-component/${locale}.js`), 'my-component': locale => fakeImport(`./my-component/${locale}.js`),
}); });
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'nl-NL': { 'nl-NL': {
'my-component': { greeting: 'Hello!' }, 'my-component': { greeting: 'Hello!' },
}, },
@ -345,6 +369,7 @@ describe('LocalizeManager', () => {
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' }); fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader( manager.setupNamespaceLoader(
'my-component', 'my-component',
@ -357,7 +382,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespace('my-component'); await manager.loadNamespace('my-component');
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-component': { greeting: 'Hello!' }, 'my-component': { greeting: 'Hello!' },
}, },
@ -373,6 +398,7 @@ describe('LocalizeManager', () => {
}); });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader( manager.setupNamespaceLoader(
'my-defaults', 'my-defaults',
@ -394,7 +420,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespaces(['my-defaults', 'my-send-button']); await manager.loadNamespaces(['my-defaults', 'my-send-button']);
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-send-button': { 'my-send-button': {
submit: 'Send', submit: 'Send',
@ -410,6 +436,7 @@ describe('LocalizeManager', () => {
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' }); fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader( manager.setupNamespaceLoader(
/my-.+/, /my-.+/,
@ -425,7 +452,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespace('my-component'); await manager.loadNamespace('my-component');
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-component': { greeting: 'Hello!' }, 'my-component': { greeting: 'Hello!' },
}, },
@ -437,6 +464,7 @@ describe('LocalizeManager', () => {
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' }); fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.setupNamespaceLoader( manager.setupNamespaceLoader(
/my-.+/, /my-.+/,
@ -452,7 +480,7 @@ describe('LocalizeManager', () => {
await manager.loadNamespaces(['my-defaults', 'my-send-button']); await manager.loadNamespaces(['my-defaults', 'my-send-button']);
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'en-GB': {
'my-defaults': { submit: 'Submit' }, 'my-defaults': { submit: 'Submit' },
'my-send-button': { submit: 'Send' }, 'my-send-button': { submit: 'Send' },
@ -467,20 +495,21 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } }); setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } });
manager = new LocalizeManager({ autoLoadOnLocaleChange: true }); manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */ /** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25), '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!' } }, 'en-GB': { 'my-component': { greeting: 'Hello!' } },
}); });
manager.locale = 'nl-NL'; manager.locale = 'nl-NL';
await manager.loadingComplete; await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } }, 'en-GB': { 'my-component': { greeting: 'Hello!' } },
'nl-NL': { 'my-component': { greeting: 'Hallo!' } }, '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 () => { it('has a Promise "loadingComplete" that resolved once all pending loading is done', async () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } }); setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.loadNamespace({ manager.loadNamespace({
/** @param {string} locale */ /** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25), 'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
}); });
expect(manager.__storage).to.deep.equal({}); expect(storage).to.deep.equal({});
await manager.loadingComplete; await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } }, 'en-GB': { 'my-component': { greeting: 'Hello!' } },
}); });
}); });
@ -526,11 +556,12 @@ describe('LocalizeManager', () => {
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Loaded hello!' } }); setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Loaded hello!' } });
manager = new LocalizeManager(); manager = new LocalizeManager();
const { storage } = getProtectedMembers(manager);
manager.addData('en-GB', 'my-component', { greeting: 'Hello!' }); manager.addData('en-GB', 'my-component', { greeting: 'Hello!' });
await manager.loadNamespace('my-component'); await manager.loadNamespace('my-component');
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } }, 'en-GB': { 'my-component': { greeting: 'Hello!' } },
}); });
@ -544,7 +575,7 @@ describe('LocalizeManager', () => {
}); });
expect(called).to.equal(0); expect(called).to.equal(0);
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } }, '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 () => { it('triggers support for external translation tools via data-localize-lang', async () => {
document.documentElement.removeAttribute('data-localize-lang'); document.documentElement.removeAttribute('data-localize-lang');
manager = getInstance(); 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'); document.documentElement.setAttribute('data-localize-lang', 'nl-NL');
manager = getInstance(); 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({ manager = new LocalizeManager({
autoLoadOnLocaleChange: true, autoLoadOnLocaleChange: true,
}); });
const { storage } = getProtectedMembers(manager);
await manager.loadNamespace({ await manager.loadNamespace({
/** @param {string} locale */ /** @param {string} locale */
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25), '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!' } }, '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 aTimeout(0); // wait for mutation observer to be called
await manager.loadingComplete; await manager.loadingComplete;
expect(manager.__storage).to.deep.equal({ expect(storage).to.deep.equal({
'en-GB': { 'my-component': { greeting: 'Hello!' } }, 'en-GB': { 'my-component': { greeting: 'Hello!' } },
'nl-NL': { 'my-component': { greeting: 'Hallo!' } }, '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'; 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', () => { describe('localize', () => {
// this is an important mindset: // this is an important mindset:
// we don't test the singleton // we don't test the singleton
@ -32,10 +47,12 @@ describe('localize', () => {
}); });
it('is configured to automatically load namespaces if locale is changed', () => { 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"', () => { 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() { constructor() {
super(); super();
this.hasArrow = true; this.hasArrow = true;
/** @private */
this.__setupRepositionCompletePromise(); this.__setupRepositionCompletePromise();
} }
@ -105,10 +106,12 @@ export const ArrowMixinImplementation = superclass =>
`; `;
} }
/** @protected */
_arrowNodeTemplate() { _arrowNodeTemplate() {
return html` <div class="arrow" data-popper-arrow>${this._arrowTemplate()}</div> `; return html` <div class="arrow" data-popper-arrow>${this._arrowTemplate()}</div> `;
} }
/** @protected */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_arrowTemplate() { _arrowTemplate() {
return html` return html`
@ -123,6 +126,7 @@ export const ArrowMixinImplementation = superclass =>
* and adds onCreate and onUpdate hooks to sync from popper state * and adds onCreate and onUpdate hooks to sync from popper state
* @configure OverlayMixin * @configure OverlayMixin
* @returns {OverlayConfig} * @returns {OverlayConfig}
* @protected
*/ */
// eslint-disable-next-line // eslint-disable-next-line
_defineOverlayConfig() { _defineOverlayConfig() {
@ -143,6 +147,7 @@ export const ArrowMixinImplementation = superclass =>
/** /**
* @param {Partial<PopperOptions>} popperConfigToExtendFrom * @param {Partial<PopperOptions>} popperConfigToExtendFrom
* @returns {Partial<PopperOptions>} * @returns {Partial<PopperOptions>}
* @protected
*/ */
_getPopperArrowConfig(popperConfigToExtendFrom) { _getPopperArrowConfig(popperConfigToExtendFrom) {
/** @type {Partial<PopperOptions> & { afterWrite: (arg0: Partial<import('@popperjs/core/lib/popper').State>) => void }} */ /** @type {Partial<PopperOptions> & { afterWrite: (arg0: Partial<import('@popperjs/core/lib/popper').State>) => void }} */
@ -177,6 +182,7 @@ export const ArrowMixinImplementation = superclass =>
return popperCfg; return popperCfg;
} }
/** @private */
__setupRepositionCompletePromise() { __setupRepositionCompletePromise() {
this.repositionComplete = new Promise(resolve => { this.repositionComplete = new Promise(resolve => {
this.__repositionCompleteResolver = resolve; this.__repositionCompleteResolver = resolve;
@ -189,6 +195,7 @@ export const ArrowMixinImplementation = superclass =>
/** /**
* @param {Partial<import('@popperjs/core/lib/popper').State>} data * @param {Partial<import('@popperjs/core/lib/popper').State>} data
* @private
*/ */
__syncFromPopperState(data) { __syncFromPopperState(data) {
if (!data) { if (!data) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@ const sym = Symbol.for('lion::SingletonManagerClassStorage');
export class SingletonManagerClass { export class SingletonManagerClass {
constructor() { constructor() {
/** protected */
this._map = window[sym] ? window[sym] : (window[sym] = new Map()); 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 * Therefore we do a full override and typecast to an intersection type that includes LionSwitchButton
* @returns {LionSwitchButton} * @returns {LionSwitchButton}
*/ */
// @ts-ignore
get _inputNode() { get _inputNode() {
return /** @type {LionSwitchButton} */ (Array.from(this.children).find( return /** @type {LionSwitchButton} */ (Array.from(this.children).find(
el => el.slot === 'input', el => el.slot === 'input',
)); ));
} }
// @ts-ignore
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,
@ -62,10 +64,12 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
`; `;
} }
/** @protected */
_groupOneTemplate() { _groupOneTemplate() {
return html`${this._labelTemplate()} ${this._helpTextTemplate()} ${this._feedbackTemplate()}`; return html`${this._labelTemplate()} ${this._helpTextTemplate()} ${this._feedbackTemplate()}`;
} }
/** @protected */
_groupTwoTemplate() { _groupTwoTemplate() {
return html`${this._inputGroupTemplate()}`; return html`${this._inputGroupTemplate()}`;
} }
@ -74,6 +78,7 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
super(); super();
this.role = 'switch'; this.role = 'switch';
this.checked = false; this.checked = false;
/** @private */
this.__handleButtonSwitchCheckedChanged = this.__handleButtonSwitchCheckedChanged.bind(this); this.__handleButtonSwitchCheckedChanged = this.__handleButtonSwitchCheckedChanged.bind(this);
} }
@ -109,16 +114,19 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
/** /**
* Override this function from ChoiceInputMixin. * Override this function from ChoiceInputMixin.
* @protected
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_isEmpty() { _isEmpty() {
return false; return false;
} }
/** @private */
__handleButtonSwitchCheckedChanged() { __handleButtonSwitchCheckedChanged() {
this.checked = this._inputNode.checked; this.checked = this._inputNode.checked;
} }
/** @protected */
_syncButtonSwitch() { _syncButtonSwitch() {
this._inputNode.disabled = this.disabled; this._inputNode.disabled = this.disabled;
} }

View file

@ -77,8 +77,11 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
this.role = 'switch'; this.role = 'switch';
this.checked = false; this.checked = false;
/** @protected */
this._toggleChecked = this._toggleChecked.bind(this); this._toggleChecked = this._toggleChecked.bind(this);
/** @private */
this.__handleKeydown = this.__handleKeydown.bind(this); this.__handleKeydown = this.__handleKeydown.bind(this);
/** @private */
this.__handleKeyup = this.__handleKeyup.bind(this); this.__handleKeyup = this.__handleKeyup.bind(this);
} }
@ -97,6 +100,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
this.removeEventListener('keyup', this.__handleKeyup); this.removeEventListener('keyup', this.__handleKeyup);
} }
/** @protected */
_toggleChecked() { _toggleChecked() {
if (this.disabled) { if (this.disabled) {
return; return;
@ -106,6 +110,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
this.checked = !this.checked; this.checked = !this.checked;
} }
/** @private */
__checkedStateChange() { __checkedStateChange() {
this.dispatchEvent( this.dispatchEvent(
new Event('checked-changed', { new Event('checked-changed', {
@ -118,6 +123,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
/** /**
* @param {KeyboardEvent} e * @param {KeyboardEvent} e
* @private
*/ */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
__handleKeydown(e) { __handleKeydown(e) {
@ -129,6 +135,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
/** /**
* @param {KeyboardEvent} e * @param {KeyboardEvent} e
* @private
*/ */
__handleKeyup(e) { __handleKeyup(e) {
if ([32 /* space */, 13 /* enter */].indexOf(e.keyCode) !== -1) { 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 * @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); const fixture = /** @type {(arg: TemplateResult) => Promise<LionSwitch>} */ (_fixture);
describe('lion-switch', () => { describe('lion-switch', () => {
@ -31,13 +41,14 @@ describe('lion-switch', () => {
it('should sync its "disabled" state to child button', async () => { it('should sync its "disabled" state to child button', async () => {
const el = await fixture(html`<lion-switch disabled></lion-switch>`); const el = await fixture(html`<lion-switch disabled></lion-switch>`);
expect(el._inputNode.disabled).to.be.true; const { inputNode } = getProtectedMembers(el);
expect(el._inputNode.hasAttribute('disabled')).to.be.true; expect(inputNode.disabled).to.be.true;
expect(inputNode.hasAttribute('disabled')).to.be.true;
el.disabled = false; el.disabled = false;
await el.updateComplete; await el.updateComplete;
await el.updateComplete; // safari takes longer await el.updateComplete; // safari takes longer
expect(el._inputNode.disabled).to.be.false; expect(inputNode.disabled).to.be.false;
expect(el._inputNode.hasAttribute('disabled')).to.be.false; expect(inputNode.hasAttribute('disabled')).to.be.false;
}); });
it('is hidden when attribute hidden is true', async () => { 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 () => { it('should sync its "checked" state to child button', async () => {
const uncheckedEl = await fixture(html`<lion-switch></lion-switch>`); const uncheckedEl = await fixture(html`<lion-switch></lion-switch>`);
const { inputNode: uncheckeInputNode } = getProtectedMembers(uncheckedEl);
const checkedEl = await fixture(html`<lion-switch checked></lion-switch>`); const checkedEl = await fixture(html`<lion-switch checked></lion-switch>`);
expect(uncheckedEl._inputNode.checked).to.be.false; const { inputNode: checkeInputNode } = getProtectedMembers(checkedEl);
expect(checkedEl._inputNode.checked).to.be.true; expect(uncheckeInputNode.checked).to.be.false;
expect(checkeInputNode.checked).to.be.true;
uncheckedEl.checked = true; uncheckedEl.checked = true;
checkedEl.checked = false; checkedEl.checked = false;
await uncheckedEl.updateComplete; await uncheckedEl.updateComplete;
await checkedEl.updateComplete; await checkedEl.updateComplete;
expect(uncheckedEl._inputNode.checked).to.be.true; expect(uncheckeInputNode.checked).to.be.true;
expect(checkedEl._inputNode.checked).to.be.false; expect(checkeInputNode.checked).to.be.false;
}); });
it('should sync "checked" state received from child button', async () => { it('should sync "checked" state received from child button', async () => {
const el = await fixture(html`<lion-switch></lion-switch>`); 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; expect(el.checked).to.be.false;
button.click(); button.click();
expect(el.checked).to.be.true; 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 () => { it('should dispatch "checked-changed" event when toggled via button or label', async () => {
const handlerSpy = sinon.spy(); const handlerSpy = sinon.spy();
const el = await fixture(html`<lion-switch .choiceValue=${'foo'}></lion-switch>`); const el = await fixture(html`<lion-switch .choiceValue=${'foo'}></lion-switch>`);
const { inputNode } = getProtectedMembers(el);
el.addEventListener('checked-changed', handlerSpy); el.addEventListener('checked-changed', handlerSpy);
el._inputNode.click(); inputNode.click();
el._labelNode.click(); el._labelNode.click();
await el.updateComplete; await el.updateComplete;
expect(handlerSpy.callCount).to.equal(2); expect(handlerSpy.callCount).to.equal(2);

View file

@ -166,6 +166,7 @@ export class LionTabs extends LitElement {
this.__setupSlots(); this.__setupSlots();
} }
/** @private */
__setupSlots() { __setupSlots() {
if (this.shadowRoot) { if (this.shadowRoot) {
const tabSlot = this.shadowRoot.querySelector('slot[name=tab]'); const tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
@ -181,6 +182,7 @@ export class LionTabs extends LitElement {
} }
} }
/** @private */
__setupStore() { __setupStore() {
/** @type {StoreEntry[]} */ /** @type {StoreEntry[]} */
this.__store = []; this.__store = [];
@ -222,6 +224,7 @@ export class LionTabs extends LitElement {
}); });
} }
/** @private */
__cleanStore() { __cleanStore() {
if (!this.__store) { if (!this.__store) {
return; return;
@ -235,6 +238,7 @@ export class LionTabs extends LitElement {
/** /**
* @param {number} index * @param {number} index
* @returns {EventHandlerNonNull} * @returns {EventHandlerNonNull}
* @private
*/ */
__createButtonClickHandler(index) { __createButtonClickHandler(index) {
return () => { return () => {
@ -244,6 +248,7 @@ export class LionTabs extends LitElement {
/** /**
* @param {Event} ev * @param {Event} ev
* @private
*/ */
__handleButtonKeyup(ev) { __handleButtonKeyup(ev) {
const _ev = /** @type {KeyboardEvent} */ (ev); const _ev = /** @type {KeyboardEvent} */ (ev);
@ -289,6 +294,7 @@ export class LionTabs extends LitElement {
/** /**
* @param {number} value The new index for focus * @param {number} value The new index for focus
* @protected
*/ */
_setSelectedIndexWithFocus(value) { _setSelectedIndexWithFocus(value) {
const stale = this.__selectedIndex; const stale = this.__selectedIndex;
@ -305,10 +311,12 @@ export class LionTabs extends LitElement {
return this.__selectedIndex || 0; return this.__selectedIndex || 0;
} }
/** @protected */
get _pairCount() { get _pairCount() {
return (this.__store && this.__store.length) || 0; return (this.__store && this.__store.length) || 0;
} }
/** @private */
__updateSelected(withFocus = false) { __updateSelected(withFocus = false) {
if ( if (
!(this.__store && typeof this.selectedIndex === 'number' && this.__store[this.selectedIndex]) !(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> <div slot="panel">panel 2</div>
</lion-tabs> </lion-tabs>
`)); `));
// @ts-ignore : this el is LionTabs
el._setSelectedIndexWithFocus(1); el._setSelectedIndexWithFocus(1);
expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be.true; 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() { get slots() {
return { return {
...super.slots, ...super.slots,
@ -95,7 +96,8 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
if (changedProperties.has('rows')) { if (changedProperties.has('rows')) {
const native = this._inputNode; const native = this._inputNode;
if (native) { 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); autosize.update(this._inputNode);
} }
/** @private */
__initializeAutoresize() { __initializeAutoresize() {
// @ts-ignore this property is added by webcomponentsjs polyfill for old browsers // @ts-ignore this property is added by webcomponentsjs polyfill for old browsers
if (this.__shady_native_contains) { if (this.__shady_native_contains) {
@ -181,6 +184,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
} }
} }
/** @private */
async __waitForTextareaRenderedInRealDOM() { async __waitForTextareaRenderedInRealDOM() {
let count = 3; // max tasks to wait for let count = 3; // max tasks to wait for
// @ts-ignore this property is added by webcomponentsjs polyfill for old browsers // @ts-ignore this property is added by webcomponentsjs polyfill for old browsers
@ -191,6 +195,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
} }
} }
/** @private */
__startAutoresize() { __startAutoresize() {
autosize(this._inputNode); autosize(this._inputNode);
this.setTextareaMaxHeight(); this.setTextareaMaxHeight();

View file

@ -9,6 +9,16 @@ import '@lion/textarea/define';
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture); const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture);
/**
* @param {LionTextarea} lionTextareaEl
*/
function getProtectedMembers(lionTextareaEl) {
const { _inputNode: input } = lionTextareaEl;
return {
input,
};
}
function hasBrowserResizeSupport() { function hasBrowserResizeSupport() {
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
return textarea.style.resize !== undefined; return textarea.style.resize !== undefined;
@ -31,20 +41,25 @@ describe('<lion-textarea>', () => {
it('has .readOnly=false .rows=2 and rows="2" by default', async () => { it('has .readOnly=false .rows=2 and rows="2" by default', async () => {
const el = await fixture(`<lion-textarea>foo</lion-textarea>`); const el = await fixture(`<lion-textarea>foo</lion-textarea>`);
const { input } = getProtectedMembers(el);
expect(el.rows).to.equal(2); expect(el.rows).to.equal(2);
expect(el.getAttribute('rows')).to.be.equal('2'); expect(el.getAttribute('rows')).to.be.equal('2');
expect(el._inputNode.rows).to.equal(2); // @ts-ignore
expect(el._inputNode.getAttribute('rows')).to.be.equal('2'); expect(input.rows).to.equal(2);
expect(input.getAttribute('rows')).to.be.equal('2');
expect(el.readOnly).to.be.false; 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 () => { it('sync rows down to the native textarea', async () => {
const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`); const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`);
const { input } = getProtectedMembers(el);
expect(el.rows).to.equal(8); expect(el.rows).to.equal(8);
expect(el.getAttribute('rows')).to.be.equal('8'); expect(el.getAttribute('rows')).to.be.equal('8');
expect(el._inputNode.rows).to.equal(8); // @ts-ignore
expect(el._inputNode.getAttribute('rows')).to.be.equal('8'); expect(input.rows).to.equal(8);
expect(input.getAttribute('rows')).to.be.equal('8');
}); });
it('sync readOnly to the native textarea', async () => { it('sync readOnly to the native textarea', async () => {
@ -59,7 +74,8 @@ describe('<lion-textarea>', () => {
} }
const el = await fixture(`<lion-textarea></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'); 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 () => { 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 el = await fixture(`<lion-textarea placeholder="text"></lion-textarea>`);
const { input } = getProtectedMembers(el);
expect(el.getAttribute('placeholder')).to.equal('text'); 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'; el.placeholder = 'foo';
await el.updateComplete; await el.updateComplete;
expect(el.getAttribute('placeholder')).to.equal('foo'); 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 () => { 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'} * @type {'label'|'description'}
*/ */
this.invokerRelation = 'description'; this.invokerRelation = 'description';
/** @protected */
this._mouseActive = false; this._mouseActive = false;
/** @protected */
this._keyActive = false; this._keyActive = false;
} }
/** @protected */
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
_defineOverlayConfig() { _defineOverlayConfig() {
return /** @type {OverlayConfig} */ ({ return /** @type {OverlayConfig} */ ({
@ -67,6 +70,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
}); });
} }
/** @protected */
_hasDisabledInvoker() { _hasDisabledInvoker() {
if (this._overlayCtrl && this._overlayCtrl.invoker) { if (this._overlayCtrl && this._overlayCtrl.invoker) {
return ( return (
@ -77,6 +81,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
return false; return false;
} }
/** @protected */
_setupOpenCloseListeners() { _setupOpenCloseListeners() {
super._setupOpenCloseListeners(); super._setupOpenCloseListeners();
this.__resetActive = this.__resetActive.bind(this); this.__resetActive = this.__resetActive.bind(this);
@ -92,6 +97,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
this._overlayInvokerNode.addEventListener('focusout', this._hideKey); this._overlayInvokerNode.addEventListener('focusout', this._hideKey);
} }
/** @protected */
_teardownOpenCloseListeners() { _teardownOpenCloseListeners() {
super._teardownOpenCloseListeners(); super._teardownOpenCloseListeners();
this._overlayCtrl.removeEventListener('hide', this.__resetActive); this._overlayCtrl.removeEventListener('hide', this.__resetActive);
@ -101,11 +107,13 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey); this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
} }
/** @private */
__resetActive() { __resetActive() {
this._mouseActive = false; this._mouseActive = false;
this._keyActive = false; this._keyActive = false;
} }
/** @protected */
_showMouse() { _showMouse() {
if (!this._keyActive) { if (!this._keyActive) {
this._mouseActive = true; this._mouseActive = true;
@ -115,12 +123,14 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
} }
} }
/** @protected */
_hideMouse() { _hideMouse() {
if (!this._keyActive) { if (!this._keyActive) {
this.opened = false; this.opened = false;
} }
} }
/** @protected */
_showKey() { _showKey() {
if (!this._mouseActive) { if (!this._mouseActive) {
this._keyActive = true; this._keyActive = true;
@ -130,6 +140,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
} }
} }
/** @protected */
_hideKey() { _hideKey() {
if (!this._mouseActive) { if (!this._mouseActive) {
this.opened = false; this.opened = false;

View file

@ -4,30 +4,45 @@ import { localize } from '@lion/localize';
import { Required } from '@lion/form-core'; import { Required } from '@lion/form-core';
import { loadDefaultFeedbackMessages } from '../src/loadDefaultFeedbackMessages.js'; 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', () => { describe('loadDefaultFeedbackMessages', () => {
it('will set default feedback message for Required', async () => { it('will set default feedback message for Required', async () => {
const el = new Required(); 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()"', 'Please configure an error message for "Required" by overriding "static async getMessage()"',
); );
loadDefaultFeedbackMessages(); 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 () => { it('will await loading of translations when switching locale', async () => {
const el = new Required(); const el = new Required();
const { getMessage } = getProtectedMembers(el);
loadDefaultFeedbackMessages(); 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.');
expect(await el._getMessage({ fieldName: 'user name' })).to.equal( expect(await getMessage({ fieldName: 'user name' })).to.equal('Please enter a(n) user name.');
'Please enter a(n) user name.',
);
localize.locale = 'de-DE'; localize.locale = 'de-DE';
expect(await el._getMessage({ fieldName: 'Password' })).to.equal( expect(await getMessage({ fieldName: 'Password' })).to.equal(
'Password muss ausgefüllt werden.', '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.', 'Benutzername muss ausgefüllt werden.',
); );
}); });