chore: add protected and private type info
This commit is contained in:
parent
3c57fc3062
commit
77a0424595
83 changed files with 1430 additions and 297 deletions
36
.changeset/eighty-kings-hear.md
Normal file
36
.changeset/eighty-kings-hear.md
Normal 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
|
||||
|
|
@ -184,10 +184,16 @@ export class LionAccordion extends LitElement {
|
|||
super();
|
||||
this.styles = {};
|
||||
|
||||
/** @type {StoreEntry[]} */
|
||||
/**
|
||||
* @type {StoreEntry[]}
|
||||
* @private
|
||||
*/
|
||||
this.__store = [];
|
||||
|
||||
/** @type {number} */
|
||||
/**
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
this.__focusedIndex = -1;
|
||||
|
||||
/** @type {number[]} */
|
||||
|
|
@ -200,6 +206,9 @@ export class LionAccordion extends LitElement {
|
|||
this.__setupSlots();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupSlots() {
|
||||
const invokerSlot = this.shadowRoot?.querySelector('slot[name=invoker]');
|
||||
const handleSlotChange = () => {
|
||||
|
|
@ -213,6 +222,9 @@ export class LionAccordion extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupStore() {
|
||||
const invokers = /** @type {HTMLElement[]} */ (Array.from(
|
||||
this.querySelectorAll('[slot="invoker"]'),
|
||||
|
|
@ -248,6 +260,9 @@ export class LionAccordion extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__cleanStore() {
|
||||
if (!this.__store) {
|
||||
return;
|
||||
|
|
@ -261,6 +276,7 @@ export class LionAccordion extends LitElement {
|
|||
/**
|
||||
*
|
||||
* @param {number} index
|
||||
* @private
|
||||
*/
|
||||
__createInvokerClickHandler(index) {
|
||||
return () => {
|
||||
|
|
@ -271,6 +287,7 @@ export class LionAccordion extends LitElement {
|
|||
|
||||
/**
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
__handleInvokerKeydown(e) {
|
||||
const _e = /** @type {KeyboardEvent} */ (e);
|
||||
|
|
@ -313,6 +330,9 @@ export class LionAccordion extends LitElement {
|
|||
return this.__focusedIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
get _pairCount() {
|
||||
return this.__store.length;
|
||||
}
|
||||
|
|
@ -329,6 +349,9 @@ export class LionAccordion extends LitElement {
|
|||
return this.__expanded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__updateFocused() {
|
||||
if (!(this.__store && this.__store[this.focusedIndex])) {
|
||||
return;
|
||||
|
|
@ -345,6 +368,9 @@ export class LionAccordion extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__updateExpanded() {
|
||||
if (!this.__store) {
|
||||
return;
|
||||
|
|
@ -364,6 +390,7 @@ export class LionAccordion extends LitElement {
|
|||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @private
|
||||
*/
|
||||
__toggleExpanded(value) {
|
||||
const { expanded } = this;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export class AjaxClient {
|
|||
* @param {Partial<AjaxClientConfig>} config
|
||||
*/
|
||||
constructor(config = {}) {
|
||||
/** @type {Partial<AjaxClientConfig>} */
|
||||
/**
|
||||
* @type {Partial<AjaxClientConfig>}
|
||||
* @private
|
||||
*/
|
||||
this.__config = {
|
||||
addAcceptLanguage: true,
|
||||
xsrfCookieName: 'XSRF-TOKEN',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class Cache {
|
|||
|
||||
/**
|
||||
* @type {{[url: string]: {expires: number, data: object} }}
|
||||
* @protected
|
||||
*/
|
||||
this._cacheObject = {};
|
||||
}
|
||||
|
|
@ -92,6 +93,7 @@ class Cache {
|
|||
* Validate cache on each call to the Cache
|
||||
* When the expiration date has passed, the _cacheObject will be replaced by an
|
||||
* empty object
|
||||
* @protected
|
||||
*/
|
||||
_validateCache() {
|
||||
if (new Date().getTime() > this.expiration) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import '@lion/core/differentKeyEventNamesShimIE';
|
|||
const isKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ' || e.key === 'Enter';
|
||||
const isSpaceKeyboardClickEvent = (/** @type {KeyboardEvent} */ e) => e.key === ' ';
|
||||
|
||||
/**
|
||||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
*/
|
||||
|
||||
export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement)) {
|
||||
static get properties() {
|
||||
return {
|
||||
|
|
@ -38,11 +42,21 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {TemplateResult} button template
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_beforeTemplate() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {TemplateResult} button template
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_afterTemplate() {
|
||||
return html``;
|
||||
|
|
@ -143,7 +157,10 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
];
|
||||
}
|
||||
|
||||
/** @type {HTMLButtonElement} */
|
||||
/**
|
||||
* @type {HTMLButtonElement}
|
||||
* @protected
|
||||
*/
|
||||
get _nativeButtonNode() {
|
||||
return /** @type {HTMLButtonElement} */ (Array.from(this.children).find(
|
||||
child => child.slot === '_button',
|
||||
|
|
@ -224,6 +241,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
* of the form, and firing click on this button. This will fire the form submit
|
||||
* without side effects caused by the click bubbling back up to lion-button.
|
||||
* @param {Event} ev
|
||||
* @private
|
||||
*/
|
||||
async __clickDelegationHandler(ev) {
|
||||
// Wait for updateComplete if form is not yet available
|
||||
|
|
@ -249,18 +267,27 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupDelegationInConstructor() {
|
||||
// do not move to connectedCallback, otherwise IE11 breaks.
|
||||
// more info: https://github.com/ing-bank/lion/issues/179#issuecomment-511763835
|
||||
this.addEventListener('click', this.__clickDelegationHandler, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupEvents() {
|
||||
this.addEventListener('mousedown', this.__mousedownHandler);
|
||||
this.addEventListener('keydown', this.__keydownHandler);
|
||||
this.addEventListener('keyup', this.__keyupHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__teardownEvents() {
|
||||
this.removeEventListener('mousedown', this.__mousedownHandler);
|
||||
this.removeEventListener('keydown', this.__keydownHandler);
|
||||
|
|
@ -268,6 +295,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
this.removeEventListener('click', this.__clickDelegationHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__mousedownHandler() {
|
||||
this.active = true;
|
||||
const mouseupHandler = () => {
|
||||
|
|
@ -281,6 +311,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
* @private
|
||||
*/
|
||||
__keydownHandler(e) {
|
||||
if (this.active || !isKeyboardClickEvent(e)) {
|
||||
|
|
@ -309,6 +340,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
* @private
|
||||
*/
|
||||
__keyupHandler(e) {
|
||||
if (isKeyboardClickEvent(e)) {
|
||||
|
|
@ -324,6 +356,7 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
/**
|
||||
* Prevents that someone who listens outside or on form catches the click event
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
__preventEventLeakage(e) {
|
||||
if (e.target === this.__submitAndResetHelperButton) {
|
||||
|
|
@ -331,6 +364,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupSubmitAndResetHelperOnConnected() {
|
||||
this._form = this._nativeButtonNode.form;
|
||||
|
||||
|
|
@ -339,6 +375,9 @@ export class LionButton extends DisabledWithTabIndexMixin(SlotMixin(LitElement))
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__teardownSubmitAndResetHelperOnDisconnected() {
|
||||
if (this._form) {
|
||||
this._form.removeEventListener('click', this.__preventEventLeakage);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@ import '@lion/button/define';
|
|||
* @typedef {import('@lion/button/src/LionButton').LionButton} LionButton
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LionButton} el
|
||||
*/
|
||||
function getProtectedMembers(el) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
nativeButtonNode: el._nativeButtonNode,
|
||||
};
|
||||
}
|
||||
|
||||
describe('lion-button', () => {
|
||||
it('behaves like native `button` in terms of a11y', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
|
|
@ -18,26 +28,32 @@ describe('lion-button', () => {
|
|||
|
||||
it('has .type="submit" and type="submit" by default', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.type).to.equal('submit');
|
||||
expect(el.getAttribute('type')).to.be.equal('submit');
|
||||
expect(el._nativeButtonNode.type).to.equal('submit');
|
||||
expect(el._nativeButtonNode.getAttribute('type')).to.be.equal('submit');
|
||||
expect(nativeButtonNode.type).to.equal('submit');
|
||||
expect(nativeButtonNode.getAttribute('type')).to.be.equal('submit');
|
||||
});
|
||||
|
||||
it('sync type down to the native button', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(
|
||||
`<lion-button type="button">foo</lion-button>`,
|
||||
));
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.type).to.equal('button');
|
||||
expect(el.getAttribute('type')).to.be.equal('button');
|
||||
expect(el._nativeButtonNode.type).to.equal('button');
|
||||
expect(el._nativeButtonNode.getAttribute('type')).to.be.equal('button');
|
||||
expect(nativeButtonNode.type).to.equal('button');
|
||||
expect(nativeButtonNode.getAttribute('type')).to.be.equal('button');
|
||||
});
|
||||
|
||||
it('hides the native button in the UI', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture(`<lion-button>foo</lion-button>`));
|
||||
expect(el._nativeButtonNode.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(window.getComputedStyle(el._nativeButtonNode).clip).to.equal('rect(0px, 0px, 0px, 0px)');
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(nativeButtonNode.getAttribute('tabindex')).to.equal('-1');
|
||||
expect(window.getComputedStyle(nativeButtonNode).clip).to.equal('rect(0px, 0px, 0px, 0px)');
|
||||
});
|
||||
|
||||
it('is hidden when attribute hidden is true', async () => {
|
||||
|
|
@ -223,8 +239,9 @@ describe('lion-button', () => {
|
|||
|
||||
it('has a native button node with aria-hidden set to true', async () => {
|
||||
const el = /** @type {LionButton} */ (await fixture('<lion-button></lion-button>'));
|
||||
const { nativeButtonNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el._nativeButtonNode.getAttribute('aria-hidden')).to.equal('true');
|
||||
expect(nativeButtonNode.getAttribute('aria-hidden')).to.equal('true');
|
||||
});
|
||||
|
||||
it('is accessible', async () => {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/** @type {{months: Month[]}} */
|
||||
/** @type {{months: Month[]}}
|
||||
* @private
|
||||
*/
|
||||
this.__data = { months: [] };
|
||||
this.minDate = new Date(0);
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
|
||||
|
|
@ -156,17 +158,27 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
this.firstDayOfWeek = 0;
|
||||
this.weekdayHeaderNotation = 'short';
|
||||
/** @private */
|
||||
this.__today = normalizeDateTime(new Date());
|
||||
/** @type {Date} */
|
||||
this.centralDate = this.__today;
|
||||
/** @type {Date | null} */
|
||||
/**
|
||||
* @type {Date | null}
|
||||
* @private
|
||||
*/
|
||||
this.__focusedDate = null;
|
||||
/** @private */
|
||||
this.__connectedCallbackDone = false;
|
||||
/** @private */
|
||||
this.__eventsAdded = false;
|
||||
this.locale = '';
|
||||
/** @private */
|
||||
this.__boundKeyboardNavigationEvent = this.__keyboardNavigationEvent.bind(this);
|
||||
/** @private */
|
||||
this.__boundClickDateDelegation = this.__clickDateDelegation.bind(this);
|
||||
/** @private */
|
||||
this.__boundFocusDateDelegation = this.__focusDateDelegation.bind(this);
|
||||
/** @private */
|
||||
this.__boundBlurDateDelegation = this.__focusDateDelegation.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -312,6 +324,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__calculateInitialCentralDate() {
|
||||
if (this.centralDate === this.__today && this.selectedDate) {
|
||||
// initialised with selectedDate only if user didn't provide another one
|
||||
|
|
@ -324,6 +339,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
/**
|
||||
* @param {string} month
|
||||
* @param {number} year
|
||||
* @private
|
||||
*/
|
||||
__renderMonthNavigation(month, year) {
|
||||
const nextMonth =
|
||||
|
|
@ -348,6 +364,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
/**
|
||||
* @param {string} month
|
||||
* @param {number} year
|
||||
* @private
|
||||
*/
|
||||
__renderYearNavigation(month, year) {
|
||||
const nextYear = year + 1;
|
||||
|
|
@ -362,6 +379,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__renderNavigation() {
|
||||
const month = getMonthNames({ locale: this.__getLocale() })[this.centralDate.getMonth()];
|
||||
const year = this.centralDate.getFullYear();
|
||||
|
|
@ -372,6 +392,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__renderData() {
|
||||
return dataTemplate(this.__data, {
|
||||
monthsLabels: getMonthNames({ locale: this.__getLocale() }),
|
||||
|
|
@ -393,6 +416,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {string} type
|
||||
* @param {string} previousMonth
|
||||
* @param {number} previousYear
|
||||
* @private
|
||||
*/
|
||||
__getPreviousDisabled(type, previousMonth, previousYear) {
|
||||
let disabled;
|
||||
|
|
@ -417,6 +441,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {string} type
|
||||
* @param {string} nextMonth
|
||||
* @param {number} nextYear
|
||||
* @private
|
||||
*/
|
||||
__getNextDisabled(type, nextMonth, nextYear) {
|
||||
let disabled;
|
||||
|
|
@ -441,6 +466,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {string} type
|
||||
* @param {string} previousMonth
|
||||
* @param {number} previousYear
|
||||
* @private
|
||||
*/
|
||||
__renderPreviousButton(type, previousMonth, previousYear) {
|
||||
const { disabled, month } = this.__getPreviousDisabled(type, previousMonth, previousYear);
|
||||
|
|
@ -470,6 +496,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {string} type
|
||||
* @param {string} nextMonth
|
||||
* @param {number} nextYear
|
||||
* @private
|
||||
*/
|
||||
__renderNextButton(type, nextMonth, nextYear) {
|
||||
const { disabled, month } = this.__getNextDisabled(type, nextMonth, nextYear);
|
||||
|
|
@ -501,6 +528,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {string} type
|
||||
* @param {string} month
|
||||
* @param {number} year
|
||||
* @private
|
||||
*/
|
||||
__getNavigationLabel(mode, type, month, year) {
|
||||
return `${this.msgLit(`lion-calendar:${mode}${type}`)}, ${month} ${year}`;
|
||||
|
|
@ -510,6 +538,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
*
|
||||
* @param {Day} _day
|
||||
* @param {*} param1
|
||||
* @private
|
||||
*/
|
||||
__coreDayPreprocessor(_day, { currentMonth = false } = {}) {
|
||||
const day = createDay(new Date(_day.date), _day);
|
||||
|
|
@ -543,6 +572,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {Day} [options]
|
||||
* @private
|
||||
*/
|
||||
__createData(options) {
|
||||
const data = createMultipleMonth(this.centralDate, {
|
||||
|
|
@ -564,6 +594,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__disableDatesChanged() {
|
||||
if (this.__connectedCallbackDone) {
|
||||
this.__ensureValidCentralDate();
|
||||
|
|
@ -572,6 +605,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {Date} selectedDate
|
||||
* @private
|
||||
*/
|
||||
__dateSelectedByUser(selectedDate) {
|
||||
this.selectedDate = selectedDate;
|
||||
|
|
@ -585,18 +619,27 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__centralDateChanged() {
|
||||
if (this.__connectedCallbackDone) {
|
||||
this.__ensureValidCentralDate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__focusedDateChanged() {
|
||||
if (this.__focusedDate) {
|
||||
this.centralDate = this.__focusedDate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__ensureValidCentralDate() {
|
||||
if (!this.__isEnabledDate(this.centralDate)) {
|
||||
this.centralDate = this.__findBestEnabledDateFor(this.centralDate);
|
||||
|
|
@ -605,6 +648,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {Date} date
|
||||
* @private
|
||||
*/
|
||||
__isEnabledDate(date) {
|
||||
const processedDay = this.__coreDayPreprocessor({ date });
|
||||
|
|
@ -615,6 +659,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {Date} date
|
||||
* @param {Object} opts
|
||||
* @param {String} [opts.mode] Find best date in `future/past/both`
|
||||
* @private
|
||||
*/
|
||||
__findBestEnabledDateFor(date, { mode = 'both' } = {}) {
|
||||
const futureDate =
|
||||
|
|
@ -655,6 +700,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @private
|
||||
*/
|
||||
__clickDateDelegation(ev) {
|
||||
const isDayButton = /** @param {HTMLElement} el */ el =>
|
||||
|
|
@ -666,6 +712,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__focusDateDelegation() {
|
||||
const isDayButton = /** @param {HTMLElement} el */ el =>
|
||||
el.classList.contains('calendar__day-button');
|
||||
|
|
@ -679,6 +728,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__blurDateDelegation() {
|
||||
const isDayButton = /** @param {HTMLElement} el */ el =>
|
||||
el.classList.contains('calendar__day-button');
|
||||
|
|
@ -695,6 +747,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
* @private
|
||||
*/
|
||||
__keyboardNavigationEvent(ev) {
|
||||
const preventedKeys = ['ArrowUp', 'ArrowDown', 'PageDown', 'PageUp'];
|
||||
|
|
@ -744,6 +797,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
* @param {string} opts.dateType
|
||||
* @param {string} opts.type
|
||||
* @param {string} opts.mode
|
||||
* @private
|
||||
*/
|
||||
__modifyDate(modify, { dateType, type, mode }) {
|
||||
let tmpDate = new Date(this.centralDate);
|
||||
|
|
@ -765,6 +819,9 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
|
|||
this[dateType] = tmpDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__getLocale() {
|
||||
return this.locale || localize.locale;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,10 +34,16 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
get _checkboxGroupNode() {
|
||||
return /** @type LionCheckboxGroup */ (this._parentFormGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
get _subCheckboxes() {
|
||||
let checkboxes = [];
|
||||
if (
|
||||
|
|
@ -52,6 +58,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
|||
return /** @type LionCheckbox[] */ (checkboxes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_setOwnCheckedState() {
|
||||
const subCheckboxes = this._subCheckboxes;
|
||||
if (!subCheckboxes.length) {
|
||||
|
|
@ -82,6 +91,7 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
|||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @private
|
||||
*/
|
||||
__onModelValueChanged(ev) {
|
||||
if (this.disabled) {
|
||||
|
|
@ -98,6 +108,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
|||
this._setOwnCheckedState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_afterTemplate() {
|
||||
return html`
|
||||
|
|
@ -107,6 +120,9 @@ export class LionCheckboxIndeterminate extends LionCheckbox {
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_onRequestToAddFormElement() {
|
||||
this._setOwnCheckedState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,16 @@ import '@lion/checkbox-group/define';
|
|||
* @typedef {import('../src/LionCheckboxGroup').LionCheckboxGroup} LionCheckboxGroup
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LionCheckboxIndeterminate} el
|
||||
*/
|
||||
function getProtectedMembers(el) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
subCheckboxes: el._subCheckboxes,
|
||||
};
|
||||
}
|
||||
|
||||
describe('<lion-checkbox-indeterminate>', () => {
|
||||
it('should have type = checkbox', async () => {
|
||||
// Arrange
|
||||
|
|
@ -93,8 +103,10 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
|
||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
||||
|
||||
// Act
|
||||
elIndeterminate._subCheckboxes[0].checked = true;
|
||||
subCheckboxes[0].checked = true;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
@ -115,11 +127,12 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
||||
|
||||
// Act
|
||||
elIndeterminate._subCheckboxes[0].checked = true;
|
||||
elIndeterminate._subCheckboxes[1].checked = true;
|
||||
elIndeterminate._subCheckboxes[2].checked = true;
|
||||
subCheckboxes[0].checked = true;
|
||||
subCheckboxes[1].checked = true;
|
||||
subCheckboxes[2].checked = true;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
@ -145,12 +158,13 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
// Act
|
||||
elIndeterminate._inputNode.click();
|
||||
await elIndeterminate.updateComplete;
|
||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
});
|
||||
|
||||
it('should sync all children when parent is checked (from unchecked to checked)', async () => {
|
||||
|
|
@ -171,12 +185,13 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
// Act
|
||||
elIndeterminate._inputNode.click();
|
||||
await elIndeterminate.updateComplete;
|
||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
expect(elIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
});
|
||||
|
||||
it('should sync all children when parent is checked (from checked to unchecked)', async () => {
|
||||
|
|
@ -197,12 +212,13 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
// Act
|
||||
elIndeterminate._inputNode.click();
|
||||
await elIndeterminate.updateComplete;
|
||||
const elProts = getProtectedMembers(elIndeterminate);
|
||||
|
||||
// Assert
|
||||
expect(elIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||
expect(elIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.false;
|
||||
expect(elProts.subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||
expect(elProts.subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||
expect(elProts.subCheckboxes[2].hasAttribute('checked')).to.be.false;
|
||||
});
|
||||
|
||||
it('should work as expected with siblings checkbox-indeterminate', async () => {
|
||||
|
|
@ -251,13 +267,18 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
await elFirstIndeterminate.updateComplete;
|
||||
await elSecondIndeterminate.updateComplete;
|
||||
|
||||
const elFirstSubCheckboxes = getProtectedMembers(elFirstIndeterminate);
|
||||
const elSecondSubCheckboxes = getProtectedMembers(elSecondIndeterminate);
|
||||
|
||||
// Assert - the second sibling should not be affected
|
||||
expect(elFirstIndeterminate?.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elFirstIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elFirstIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elFirstIndeterminate?._subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
expect(elSecondIndeterminate?._subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||
expect(elSecondIndeterminate?._subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||
|
||||
expect(elFirstIndeterminate.hasAttribute('indeterminate')).to.be.false;
|
||||
expect(elFirstSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.true;
|
||||
expect(elFirstSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.true;
|
||||
expect(elFirstSubCheckboxes.subCheckboxes[2].hasAttribute('checked')).to.be.true;
|
||||
|
||||
expect(elSecondSubCheckboxes.subCheckboxes[0].hasAttribute('checked')).to.be.false;
|
||||
expect(elSecondSubCheckboxes.subCheckboxes[1].hasAttribute('checked')).to.be.false;
|
||||
});
|
||||
|
||||
it('should work as expected with nested indeterminate checkboxes', async () => {
|
||||
|
|
@ -301,9 +322,13 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
const elParentIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'#parent-checkbox-indeterminate',
|
||||
));
|
||||
const elNestedSubCheckboxes = getProtectedMembers(elNestedIndeterminate);
|
||||
const elParentSubCheckboxes = getProtectedMembers(elParentIndeterminate);
|
||||
|
||||
// Act - check a nested checkbox
|
||||
elNestedIndeterminate?._subCheckboxes[0]._inputNode.click();
|
||||
if (elNestedIndeterminate) {
|
||||
elNestedSubCheckboxes.subCheckboxes[0]._inputNode.click();
|
||||
}
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
@ -311,8 +336,8 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
|
||||
// Act - check all nested checkbox
|
||||
elNestedIndeterminate?._subCheckboxes[1]._inputNode.click();
|
||||
elNestedIndeterminate?._subCheckboxes[2]._inputNode.click();
|
||||
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[1]._inputNode.click();
|
||||
if (elNestedIndeterminate) elNestedSubCheckboxes.subCheckboxes[2]._inputNode.click();
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
@ -322,8 +347,12 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
expect(elParentIndeterminate?.hasAttribute('indeterminate')).to.be.true;
|
||||
|
||||
// Act - finally check all remaining checkbox
|
||||
elParentIndeterminate?._subCheckboxes[0]._inputNode.click();
|
||||
elParentIndeterminate?._subCheckboxes[1]._inputNode.click();
|
||||
if (elParentIndeterminate) {
|
||||
elParentSubCheckboxes.subCheckboxes[0]._inputNode.click();
|
||||
}
|
||||
if (elParentIndeterminate) {
|
||||
elParentSubCheckboxes.subCheckboxes[1]._inputNode.click();
|
||||
}
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
@ -354,11 +383,12 @@ describe('<lion-checkbox-indeterminate>', () => {
|
|||
const elIndeterminate = /** @type {LionCheckboxIndeterminate} */ (el.querySelector(
|
||||
'lion-checkbox-indeterminate',
|
||||
));
|
||||
const { subCheckboxes } = getProtectedMembers(elIndeterminate);
|
||||
|
||||
// Act
|
||||
elIndeterminate._subCheckboxes[0].checked = true;
|
||||
elIndeterminate._subCheckboxes[1].checked = true;
|
||||
elIndeterminate._subCheckboxes[2].checked = true;
|
||||
subCheckboxes[0].checked = true;
|
||||
subCheckboxes[1].checked = true;
|
||||
subCheckboxes[2].checked = true;
|
||||
await el.updateComplete;
|
||||
|
||||
// Assert
|
||||
|
|
|
|||
|
|
@ -127,18 +127,27 @@ export class LionCollapsible extends LitElement {
|
|||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
||||
async _hideAnimation(opts) {}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
get _invokerNode() {
|
||||
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
||||
child => child.slot === 'invoker',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
get _contentNode() {
|
||||
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
||||
child => child.slot === 'content',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
get _contentHeight() {
|
||||
const size = this._contentNode?.getBoundingClientRect().height || 0;
|
||||
return `${size}px`;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,16 @@ const collapsibleWithEvents = html`
|
|||
</lion-collapsible>
|
||||
`;
|
||||
|
||||
/**
|
||||
* @param {LionCollapsible} el
|
||||
*/
|
||||
function getProtectedMembers(el) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
contentHeight: el._contentHeight,
|
||||
};
|
||||
}
|
||||
|
||||
describe('<lion-collapsible>', () => {
|
||||
describe('Collapsible', () => {
|
||||
it('sets opened to false by default', async () => {
|
||||
|
|
@ -49,10 +59,12 @@ describe('<lion-collapsible>', () => {
|
|||
|
||||
it('should return content node height before and after collapsing', async () => {
|
||||
const collapsible = await fixture(defaultCollapsible);
|
||||
expect(collapsible._contentHeight).to.equal('0px');
|
||||
const collHeight1 = getProtectedMembers(collapsible);
|
||||
expect(collHeight1.contentHeight).to.equal('0px');
|
||||
collapsible.show();
|
||||
await collapsible.requestUpdate();
|
||||
expect(collapsible._contentHeight).to.equal('32px');
|
||||
const collHeight2 = getProtectedMembers(collapsible);
|
||||
expect(collHeight2.contentHeight).to.equal('32px');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @enhance FormControlMixin - add slot[name=selection-display]
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupInputTemplate() {
|
||||
|
|
@ -83,6 +84,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_overlayListboxTemplate() {
|
||||
// TODO: Localize the aria-label
|
||||
|
|
@ -96,6 +100,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @enhance FormControlMixin - add overlay
|
||||
* @protected
|
||||
*/
|
||||
_groupTwoTemplate() {
|
||||
return html` ${super._groupTwoTemplate()} ${this._overlayListboxTemplate()}`;
|
||||
|
|
@ -157,6 +162,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
/**
|
||||
* Wrapper with combobox role for the text input that the end user controls the listbox with.
|
||||
* @type {HTMLElement}
|
||||
* @protected
|
||||
*/
|
||||
get _comboboxNode() {
|
||||
return /** @type {HTMLElement} */ (this.querySelector('[slot="input"]'));
|
||||
|
|
@ -164,6 +170,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @type {SelectionDisplay | null}
|
||||
* @protected
|
||||
*/
|
||||
get _selectionDisplayNode() {
|
||||
return this.querySelector('[slot="selection-display"]');
|
||||
|
|
@ -173,6 +180,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* @configure FormControlMixin
|
||||
* Will tell FormControlMixin that a11y wrt labels / descriptions / feedback
|
||||
* should be applied here.
|
||||
* @protected
|
||||
*/
|
||||
get _inputNode() {
|
||||
if (this._ariaVersion === '1.1') {
|
||||
|
|
@ -183,6 +191,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
get _overlayContentNode() {
|
||||
return this._listboxNode;
|
||||
|
|
@ -190,6 +199,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
get _overlayReferenceNode() {
|
||||
return /** @type {ShadowRoot} */ (this.shadowRoot).querySelector('.input-group__container');
|
||||
|
|
@ -197,6 +207,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
get _overlayInvokerNode() {
|
||||
return this._inputNode;
|
||||
|
|
@ -204,6 +215,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @configure ListboxMixin
|
||||
* @protected
|
||||
*/
|
||||
get _listboxNode() {
|
||||
return /** @type {LionOptions} */ ((this._overlayCtrl && this._overlayCtrl.contentNode) ||
|
||||
|
|
@ -212,6 +224,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @configure ListboxMixin
|
||||
* @protected
|
||||
*/
|
||||
get _activeDescendantOwnerNode() {
|
||||
return this._inputNode;
|
||||
|
|
@ -253,22 +266,36 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
/**
|
||||
* For optimal support, we allow aria v1.1 on newer browsers
|
||||
* @type {'1.1'|'1.0'}
|
||||
* @protected
|
||||
*/
|
||||
this._ariaVersion = browserDetection.isChromium ? '1.1' : '1.0';
|
||||
|
||||
/**
|
||||
* @configure ListboxMixin
|
||||
* @protected
|
||||
*/
|
||||
this._listboxReceivesNoFocus = true;
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.__prevCboxValueNonSelected = '';
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
this.__prevCboxValue = '';
|
||||
|
||||
/** @type {EventListener} */
|
||||
/** @type {EventListener}
|
||||
* @private
|
||||
*/
|
||||
this.__requestShowOverlay = this.__requestShowOverlay.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/** @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._textboxOnInput = this._textboxOnInput.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/** @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._textboxOnKeydown = this._textboxOnKeydown.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -387,6 +414,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* }
|
||||
*
|
||||
* @param {{ currentValue: string, lastKey:string }} options
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_showOverlayCondition({ lastKey }) {
|
||||
|
|
@ -403,6 +431,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_textboxOnInput(ev) {
|
||||
|
|
@ -412,6 +441,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_textboxOnKeydown(ev) {
|
||||
if (ev.key === 'Tab') {
|
||||
|
|
@ -421,6 +451,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_listboxOnClick(ev) {
|
||||
super._listboxOnClick(ev);
|
||||
|
|
@ -433,6 +464,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @param {string} v
|
||||
* @protected
|
||||
*/
|
||||
_setTextboxValue(v) {
|
||||
// Make sure that we don't loose inputNode.selectionStart and inputNode.selectionEnd
|
||||
|
|
@ -441,6 +473,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__onOverlayClose() {
|
||||
if (!this.multipleChoice) {
|
||||
if (
|
||||
|
|
@ -471,6 +506,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* model-value-changed event that gets received, and we should repropagate it.
|
||||
*
|
||||
* @param {EventTarget & import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} target
|
||||
* @protected
|
||||
*/
|
||||
_repropagationCondition(target) {
|
||||
return super._repropagationCondition(target) || this.formElements.every(el => !el.checked);
|
||||
|
|
@ -481,6 +517,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* @overridable
|
||||
* @param {LionOption & {__originalInnerHTML?:string}} option
|
||||
* @param {string} matchingString
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_onFilterMatch(option, matchingString) {
|
||||
|
|
@ -498,6 +535,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* @param {LionOption & {__originalInnerHTML?:string}} option
|
||||
* @param {string} [curValue]
|
||||
* @param {string} [prevValue]
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||
_onFilterUnmatch(option, curValue, prevValue) {
|
||||
|
|
@ -512,6 +550,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
/**
|
||||
* Computes whether a user intends to autofill (inline autocomplete textbox)
|
||||
* @param {{ prevValue:string, curValue:string }} config
|
||||
* @private
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__computeUserIntendsAutoFill({ prevValue, curValue }) {
|
||||
|
|
@ -535,6 +574,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* - complete: completes the textbox value inline (the 'missing characters' will be added as
|
||||
* selected text)
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_handleAutocompletion() {
|
||||
// TODO: this is captured by 'noFilter'
|
||||
|
|
@ -660,6 +700,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__textboxInlineComplete(option = this.formElements[this.activeIndex]) {
|
||||
const prevLen = this._inputNode.value.length;
|
||||
this._inputNode.value = option.choiceValue;
|
||||
|
|
@ -672,6 +715,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* option from the list (by default when autocomplete is 'none' or 'list').
|
||||
* For autocomplete 'both' or 'inline', it will automatically select on a match.
|
||||
* @overridable
|
||||
* @protected
|
||||
*/
|
||||
_autoSelectCondition() {
|
||||
return this.autocomplete === 'both' || this.autocomplete === 'inline';
|
||||
|
|
@ -679,6 +723,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @enhance ListboxMixin
|
||||
* @protected
|
||||
*/
|
||||
_setupListboxNode() {
|
||||
super._setupListboxNode();
|
||||
|
|
@ -688,6 +733,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_defineOverlayConfig() {
|
||||
|
|
@ -699,6 +745,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @enhance OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
_setupOverlayCtrl() {
|
||||
super._setupOverlayCtrl();
|
||||
|
|
@ -708,6 +755,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @enhance OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
_setupOpenCloseListeners() {
|
||||
super._setupOpenCloseListeners();
|
||||
|
|
@ -716,6 +764,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @enhance OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
_teardownOpenCloseListeners() {
|
||||
super._teardownOpenCloseListeners();
|
||||
|
|
@ -725,6 +774,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
/**
|
||||
* @enhance ListboxMixin
|
||||
* @param {KeyboardEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_listboxOnKeyDown(ev) {
|
||||
super._listboxOnKeyDown(ev);
|
||||
|
|
@ -750,6 +800,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* @overridable
|
||||
* @param {string|string[]} modelValue
|
||||
* @param {string|string[]} oldModelValue
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_syncToTextboxCondition(modelValue, oldModelValue, { phase } = {}) {
|
||||
|
|
@ -763,6 +814,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
* Allows to control what happens when checkedIndexes change
|
||||
* @param {string[]} modelValue
|
||||
* @param {string[]} oldModelValue
|
||||
* @protected
|
||||
*/
|
||||
_syncToTextboxMultiple(modelValue, oldModelValue = []) {
|
||||
const diff = modelValue.filter(x => !oldModelValue.includes(x));
|
||||
|
|
@ -771,6 +823,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @override FormControlMixin - add form-control to [slot=input] instead of _inputNode
|
||||
* @protected
|
||||
*/
|
||||
_enhanceLightDomClasses() {
|
||||
if (this.querySelector('[slot=input]')) {
|
||||
|
|
@ -778,10 +831,16 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__initFilterListbox() {
|
||||
this._handleAutocompletion();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setComboboxDisabledAndReadOnly() {
|
||||
if (this._comboboxNode) {
|
||||
this._comboboxNode.setAttribute('disabled', `${this.disabled}`);
|
||||
|
|
@ -789,6 +848,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupCombobox() {
|
||||
// With regard to accessibility: aria-expanded and -labelledby will
|
||||
// be handled by OverlayMixin and FormControlMixin respectively.
|
||||
|
|
@ -811,6 +873,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
this._inputNode.addEventListener('keydown', this._textboxOnKeydown);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__teardownCombobox() {
|
||||
this._inputNode.removeEventListener('keydown', this._listboxOnKeyDown);
|
||||
this._inputNode.removeEventListener('input', this._textboxOnInput);
|
||||
|
|
@ -819,6 +884,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} [ev]
|
||||
* @private
|
||||
*/
|
||||
__requestShowOverlay(ev) {
|
||||
this.opened = this._showOverlayCondition({
|
||||
|
|
|
|||
|
|
@ -11,17 +11,42 @@ import { LionCombobox } from '../src/LionCombobox.js';
|
|||
* @typedef {import('../types/SelectionDisplay').SelectionDisplay} SelectionDisplay
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LionCombobox} el
|
||||
*/
|
||||
|
||||
function getProtectedMembers(el) {
|
||||
// @ts-ignore
|
||||
const {
|
||||
_comboboxNode: comboboxNode,
|
||||
_inputNode: inputNode,
|
||||
_listboxNode: listboxNode,
|
||||
_selectionDisplayNode: selectionDisplayNode,
|
||||
_activeDescendantOwnerNode: activeDescendantOwnerNode,
|
||||
_ariaVersion: ariaVersion,
|
||||
} = el;
|
||||
return {
|
||||
comboboxNode,
|
||||
inputNode,
|
||||
listboxNode,
|
||||
selectionDisplayNode,
|
||||
activeDescendantOwnerNode,
|
||||
ariaVersion,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LionCombobox} el
|
||||
* @param {string} value
|
||||
*/
|
||||
function mimicUserTyping(el, value) {
|
||||
el._inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
el._inputNode.value = value;
|
||||
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
el._inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
|
||||
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
|
||||
inputNode.value = value;
|
||||
inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
inputNode.dispatchEvent(new KeyboardEvent('keyup', { key: value }));
|
||||
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: value }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,35 +63,49 @@ function mimicKeyPress(el, key) {
|
|||
* @param {string[]} values
|
||||
*/
|
||||
async function mimicUserTypingAdvanced(el, values) {
|
||||
const inputNode = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (el._inputNode);
|
||||
inputNode.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
const inputNodeLoc = /** @type {HTMLInputElement & {selectionStart:number, selectionEnd:number}} */ (inputNode);
|
||||
inputNodeLoc.dispatchEvent(new Event('focusin', { bubbles: true }));
|
||||
|
||||
for (const key of values) {
|
||||
// eslint-disable-next-line no-await-in-loop, no-loop-func
|
||||
await new Promise(resolve => {
|
||||
const hasSelection = inputNode.selectionStart !== inputNode.selectionEnd;
|
||||
const hasSelection = inputNodeLoc.selectionStart !== inputNodeLoc.selectionEnd;
|
||||
|
||||
if (key === 'Backspace') {
|
||||
if (hasSelection) {
|
||||
inputNode.value =
|
||||
inputNode.value.slice(0, inputNode.selectionStart) +
|
||||
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
|
||||
inputNodeLoc.value =
|
||||
inputNodeLoc.value.slice(
|
||||
0,
|
||||
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
|
||||
) +
|
||||
inputNodeLoc.value.slice(
|
||||
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
|
||||
inputNodeLoc.value.length,
|
||||
);
|
||||
} else {
|
||||
inputNode.value = inputNode.value.slice(0, -1);
|
||||
inputNodeLoc.value = inputNodeLoc.value.slice(0, -1);
|
||||
}
|
||||
} else if (hasSelection) {
|
||||
inputNode.value =
|
||||
inputNode.value.slice(0, inputNode.selectionStart) +
|
||||
inputNodeLoc.value =
|
||||
inputNodeLoc.value.slice(
|
||||
0,
|
||||
inputNodeLoc.selectionStart ? inputNodeLoc.selectionStart : undefined,
|
||||
) +
|
||||
key +
|
||||
inputNode.value.slice(inputNode.selectionEnd, inputNode.value.length);
|
||||
inputNodeLoc.value.slice(
|
||||
inputNodeLoc.selectionEnd ? inputNodeLoc.selectionEnd : undefined,
|
||||
inputNodeLoc.value.length,
|
||||
);
|
||||
} else {
|
||||
inputNode.value += key;
|
||||
inputNodeLoc.value += key;
|
||||
}
|
||||
|
||||
mimicKeyPress(inputNode, key);
|
||||
el._inputNode.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
mimicKeyPress(inputNodeLoc, key);
|
||||
inputNodeLoc.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
||||
|
||||
el.updateComplete.then(() => {
|
||||
// @ts-ignore
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
|
@ -187,9 +226,10 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.opened).to.be.false;
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.be.true;
|
||||
});
|
||||
|
|
@ -204,9 +244,11 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._listboxNode).to.exist;
|
||||
expect(el._listboxNode).to.be.instanceOf(LionOptions);
|
||||
expect(el.querySelector('[role=listbox]')).to.equal(el._listboxNode);
|
||||
const { listboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(listboxNode).to.exist;
|
||||
expect(listboxNode).to.be.instanceOf(LionOptions);
|
||||
expect(el.querySelector('[role=listbox]')).to.equal(listboxNode);
|
||||
});
|
||||
|
||||
it('has a textbox element', async () => {
|
||||
|
|
@ -216,8 +258,10 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._comboboxNode).to.exist;
|
||||
expect(el.querySelector('[role=combobox]')).to.equal(el._comboboxNode);
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(comboboxNode).to.exist;
|
||||
expect(el.querySelector('[role=combobox]')).to.equal(comboboxNode);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -229,11 +273,13 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._inputNode.value).to.equal('10');
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(inputNode.value).to.equal('10');
|
||||
|
||||
el.modelValue = '20';
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('20');
|
||||
expect(inputNode.value).to.equal('20');
|
||||
});
|
||||
|
||||
it('sets modelValue to empty string if no option is selected', async () => {
|
||||
|
|
@ -283,9 +329,10 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.opened).to.equal(false);
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(false);
|
||||
});
|
||||
|
|
@ -308,10 +355,12 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
const options = el.formElements;
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
// step [1]
|
||||
el._inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
inputNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
|
|
@ -324,7 +373,7 @@ describe('lion-combobox', () => {
|
|||
options[0].click();
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(false);
|
||||
expect(document.activeElement).to.equal(el._inputNode);
|
||||
expect(document.activeElement).to.equal(inputNode);
|
||||
|
||||
// step [4]
|
||||
await el.updateComplete;
|
||||
|
|
@ -343,17 +392,19 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
|
||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
||||
|
||||
// open
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
|
||||
el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||
expect(el.opened).to.equal(false);
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
expect(inputNode.value).to.equal('');
|
||||
});
|
||||
|
||||
it('hides overlay on [Tab]', async () => {
|
||||
|
|
@ -366,17 +417,19 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
|
||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
||||
|
||||
// open
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
|
||||
mimicKeyPress(el._inputNode, 'Tab');
|
||||
mimicKeyPress(inputNode, 'Tab');
|
||||
expect(el.opened).to.equal(false);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
});
|
||||
|
||||
it('clears checkedIndex on empty text', async () => {
|
||||
|
|
@ -389,13 +442,15 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
|
||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
||||
|
||||
// open
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
expect(el.checkedIndex).to.equal(0);
|
||||
|
||||
mimicUserTyping(el, '');
|
||||
|
|
@ -425,9 +480,10 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</${tag}>
|
||||
`));
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(el.opened).to.equal(false);
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
await el.updateComplete;
|
||||
expect(el.opened).to.equal(true);
|
||||
});
|
||||
|
|
@ -517,9 +573,10 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
const options = el.formElements;
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
await el.updateComplete;
|
||||
|
|
@ -557,9 +614,10 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
const options = el.formElements;
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
expect(el.opened).to.equal(false);
|
||||
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
|
||||
mimicUserTyping(el, 'art');
|
||||
expect(el.opened).to.equal(true);
|
||||
|
|
@ -584,14 +642,15 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
expect(el.checkedIndex).to.equal(0);
|
||||
|
||||
// Simulate backspace deleting the char at the end of the string
|
||||
mimicKeyPress(el._inputNode, 'Backspace');
|
||||
el._inputNode.dispatchEvent(new Event('input'));
|
||||
const arr = el._inputNode.value.split('');
|
||||
arr.splice(el._inputNode.value.length - 1, 1);
|
||||
el._inputNode.value = arr.join('');
|
||||
mimicKeyPress(inputNode, 'Backspace');
|
||||
inputNode.dispatchEvent(new Event('input'));
|
||||
const arr = inputNode.value.split('');
|
||||
arr.splice(inputNode.value.length - 1, 1);
|
||||
inputNode.value = arr.join('');
|
||||
await el.updateComplete;
|
||||
el.dispatchEvent(new Event('blur'));
|
||||
|
||||
|
|
@ -614,7 +673,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._comboboxNode.getAttribute('role')).to.equal('combobox');
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(comboboxNode.getAttribute('role')).to.equal('combobox');
|
||||
});
|
||||
|
||||
it('makes sure listbox node is not focusable', async () => {
|
||||
|
|
@ -624,7 +685,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'20'}">Item 2</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._listboxNode.hasAttribute('tabindex')).to.be.false;
|
||||
const { listboxNode } = getProtectedMembers(el);
|
||||
|
||||
expect(listboxNode.hasAttribute('tabindex')).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -653,7 +716,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._selectionDisplayNode).to.equal(el.querySelector('[slot=selection-display]'));
|
||||
const { selectionDisplayNode } = getProtectedMembers(el);
|
||||
|
||||
expect(selectionDisplayNode).to.equal(el.querySelector('[slot=selection-display]'));
|
||||
});
|
||||
|
||||
it('sets a reference to combobox element in _selectionDisplayNode', async () => {
|
||||
|
|
@ -781,14 +846,16 @@ describe('lion-combobox', () => {
|
|||
`));
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('Chard');
|
||||
expect(el._inputNode.selectionStart).to.equal(2);
|
||||
expect(el._inputNode.selectionEnd).to.equal(el._inputNode.value.length);
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(inputNode.value).to.equal('Chard');
|
||||
expect(inputNode.selectionStart).to.equal(2);
|
||||
expect(inputNode.selectionEnd).to.equal(inputNode.value.length);
|
||||
|
||||
// We don't autocomplete when characters are removed
|
||||
mimicUserTyping(el, 'c'); // The user pressed backspace (number of chars decreased)
|
||||
expect(el._inputNode.value).to.equal('c');
|
||||
expect(el._inputNode.selectionStart).to.equal(el._inputNode.value.length);
|
||||
expect(inputNode.value).to.equal('c');
|
||||
expect(inputNode.selectionStart).to.equal(inputNode.value.length);
|
||||
});
|
||||
|
||||
it('filters options when autocomplete is "list"', async () => {
|
||||
|
|
@ -800,10 +867,12 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
expect(getFilteredOptionValues(el)).to.eql(['Artichoke', 'Chard', 'Chicory']);
|
||||
expect(el._inputNode.value).to.equal('ch');
|
||||
expect(inputNode.value).to.equal('ch');
|
||||
});
|
||||
|
||||
it('does not filter options when autocomplete is "none"', async () => {
|
||||
|
|
@ -891,25 +960,26 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('Chard');
|
||||
expect(el._inputNode.selectionStart).to.equal('ch'.length);
|
||||
expect(el._inputNode.selectionEnd).to.equal('Chard'.length);
|
||||
expect(inputNode.value).to.equal('Chard');
|
||||
expect(inputNode.selectionStart).to.equal('ch'.length);
|
||||
expect(inputNode.selectionEnd).to.equal('Chard'.length);
|
||||
|
||||
await mimicUserTypingAdvanced(el, ['i', 'c']);
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('Chicory');
|
||||
expect(el._inputNode.selectionStart).to.equal('chic'.length);
|
||||
expect(el._inputNode.selectionEnd).to.equal('Chicory'.length);
|
||||
expect(inputNode.value).to.equal('Chicory');
|
||||
expect(inputNode.selectionStart).to.equal('chic'.length);
|
||||
expect(inputNode.selectionEnd).to.equal('Chicory'.length);
|
||||
|
||||
// Diminishing chars, but autocompleting
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('ch');
|
||||
expect(el._inputNode.selectionStart).to.equal('ch'.length);
|
||||
expect(el._inputNode.selectionEnd).to.equal('ch'.length);
|
||||
expect(inputNode.value).to.equal('ch');
|
||||
expect(inputNode.selectionStart).to.equal('ch'.length);
|
||||
expect(inputNode.selectionEnd).to.equal('ch'.length);
|
||||
});
|
||||
|
||||
it('synchronizes textbox on overlay close', async () => {
|
||||
|
|
@ -921,7 +991,8 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
/**
|
||||
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
|
||||
|
|
@ -937,7 +1008,7 @@ describe('lion-combobox', () => {
|
|||
el.setCheckedIndex(index);
|
||||
el.opened = false;
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal(valueOnClose);
|
||||
expect(inputNode.value).to.equal(valueOnClose);
|
||||
}
|
||||
|
||||
await performChecks('none', 0, 'Artichoke');
|
||||
|
|
@ -961,7 +1032,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
/**
|
||||
* @param {'none' | 'list' | 'inline' | 'both'} autocomplete
|
||||
|
|
@ -977,7 +1050,7 @@ describe('lion-combobox', () => {
|
|||
el.setCheckedIndex(index);
|
||||
el.opened = false;
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal(valueOnClose);
|
||||
expect(inputNode.value).to.equal(valueOnClose);
|
||||
}
|
||||
|
||||
await performChecks('none', 0, '');
|
||||
|
|
@ -1054,20 +1127,21 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('Chard');
|
||||
expect(el._inputNode.selectionStart).to.equal('Ch'.length);
|
||||
expect(el._inputNode.selectionEnd).to.equal('Chard'.length);
|
||||
expect(inputNode.value).to.equal('Chard');
|
||||
expect(inputNode.selectionStart).to.equal('Ch'.length);
|
||||
expect(inputNode.selectionEnd).to.equal('Chard'.length);
|
||||
|
||||
// Autocompletion happened. When we go backwards ('Ch[ard]' => 'Ch'), we should not
|
||||
// autocomplete to 'Chard' anymore.
|
||||
await mimicUserTypingAdvanced(el, ['Backspace']);
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.value).to.equal('Ch'); // so not 'Chard'
|
||||
expect(el._inputNode.selectionStart).to.equal('Ch'.length);
|
||||
expect(el._inputNode.selectionEnd).to.equal('Ch'.length);
|
||||
expect(inputNode.value).to.equal('Ch'); // so not 'Chard'
|
||||
expect(inputNode.selectionStart).to.equal('Ch'.length);
|
||||
expect(inputNode.selectionEnd).to.equal('Ch'.length);
|
||||
});
|
||||
|
||||
describe('Subclassers', () => {
|
||||
|
|
@ -1133,27 +1207,29 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'none';
|
||||
el.setCheckedIndex(0);
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'list';
|
||||
el.setCheckedIndex(0);
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'inline';
|
||||
el.setCheckedIndex(0);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'both';
|
||||
el.setCheckedIndex(0);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
});
|
||||
|
||||
it('synchronizes last index to textbox when autocomplete is "inline" or "both" when multipleChoice', async () => {
|
||||
|
|
@ -1165,33 +1241,35 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._inputNode.value).to.eql('');
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(inputNode.value).to.eql('');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'none';
|
||||
el.setCheckedIndex([0]);
|
||||
el.setCheckedIndex([1]);
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'list';
|
||||
el.setCheckedIndex([0]);
|
||||
el.setCheckedIndex([1]);
|
||||
expect(el._inputNode.value).to.equal('');
|
||||
expect(inputNode.value).to.equal('');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'inline';
|
||||
el.setCheckedIndex([0]);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
el.setCheckedIndex([1]);
|
||||
expect(el._inputNode.value).to.equal('Chard');
|
||||
expect(inputNode.value).to.equal('Chard');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'both';
|
||||
el.setCheckedIndex([0]);
|
||||
expect(el._inputNode.value).to.equal('Artichoke');
|
||||
expect(inputNode.value).to.equal('Artichoke');
|
||||
el.setCheckedIndex([1]);
|
||||
expect(el._inputNode.value).to.equal('Chard');
|
||||
expect(inputNode.value).to.equal('Chard');
|
||||
});
|
||||
|
||||
describe('Subclassers', () => {
|
||||
|
|
@ -1224,26 +1302,27 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</${tag}>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'none';
|
||||
el.setCheckedIndex([0]);
|
||||
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'list';
|
||||
el.setCheckedIndex([0]);
|
||||
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'inline';
|
||||
el.setCheckedIndex([0]);
|
||||
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
||||
|
||||
el.setCheckedIndex(-1);
|
||||
el.autocomplete = 'both';
|
||||
el.setCheckedIndex([0]);
|
||||
expect(el._inputNode.value).to.equal('Artichoke--multi');
|
||||
expect(inputNode.value).to.equal('Artichoke--multi');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1277,6 +1356,7 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
/**
|
||||
* @param {LionCombobox} elm
|
||||
|
|
@ -1304,7 +1384,7 @@ describe('lion-combobox', () => {
|
|||
expect(el.activeIndex).to.equal(-1);
|
||||
expect(el.opened).to.be.true;
|
||||
|
||||
mimicKeyPress(el._inputNode, 'Enter');
|
||||
mimicKeyPress(inputNode, 'Enter');
|
||||
expect(el.opened).to.be.false;
|
||||
expect(el.activeIndex).to.equal(-1);
|
||||
|
||||
|
|
@ -1318,7 +1398,7 @@ describe('lion-combobox', () => {
|
|||
expect(el.opened).to.be.true;
|
||||
expect(el.activeIndex).to.equal(-1);
|
||||
|
||||
mimicKeyPress(el._inputNode, 'Enter');
|
||||
mimicKeyPress(inputNode, 'Enter');
|
||||
expect(el.activeIndex).to.equal(-1);
|
||||
expect(el.opened).to.be.false;
|
||||
|
||||
|
|
@ -1335,7 +1415,7 @@ describe('lion-combobox', () => {
|
|||
|
||||
expect(el.activeIndex).to.equal(1);
|
||||
|
||||
mimicKeyPress(el._inputNode, 'Enter');
|
||||
mimicKeyPress(inputNode, 'Enter');
|
||||
await el.updateComplete;
|
||||
await el.updateComplete;
|
||||
|
||||
|
|
@ -1349,7 +1429,7 @@ describe('lion-combobox', () => {
|
|||
await el.updateComplete;
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||
await el.updateComplete;
|
||||
mimicKeyPress(el._inputNode, 'Enter');
|
||||
mimicKeyPress(inputNode, 'Enter');
|
||||
expect(el.activeIndex).to.equal(1);
|
||||
expect(el.opened).to.be.false;
|
||||
});
|
||||
|
|
@ -1363,6 +1443,8 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||
await el.updateComplete;
|
||||
expect(el.activeIndex).to.equal(1);
|
||||
|
|
@ -1378,7 +1460,7 @@ describe('lion-combobox', () => {
|
|||
// select artichoke
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'artichoke');
|
||||
await el.updateComplete;
|
||||
mimicKeyPress(el._inputNode, 'Enter');
|
||||
mimicKeyPress(inputNode, 'Enter');
|
||||
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), '');
|
||||
await el.updateComplete;
|
||||
|
|
@ -1397,15 +1479,17 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
|
||||
// Select something
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'cha');
|
||||
await el.updateComplete;
|
||||
mimicKeyPress(el._inputNode, 'Enter');
|
||||
mimicKeyPress(inputNode, 'Enter');
|
||||
expect(el.activeIndex).to.equal(1);
|
||||
|
||||
mimicKeyPress(el._inputNode, 'Escape');
|
||||
mimicKeyPress(inputNode, 'Escape');
|
||||
await el.updateComplete;
|
||||
expect(el._inputNode.textContent).to.equal('');
|
||||
expect(inputNode.textContent).to.equal('');
|
||||
|
||||
el.formElements.forEach(option => expect(option.active).to.be.false);
|
||||
|
||||
|
|
@ -1419,19 +1503,21 @@ describe('lion-combobox', () => {
|
|||
describe('Accessibility', () => {
|
||||
it('synchronizes autocomplete option to textbox', async () => {
|
||||
let el;
|
||||
|
||||
[el] = await fruitFixture({ autocomplete: 'both' });
|
||||
// @ts-expect-error
|
||||
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('both');
|
||||
|
||||
[el] = await fruitFixture({ autocomplete: 'list' });
|
||||
// @ts-expect-error
|
||||
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('list');
|
||||
|
||||
[el] = await fruitFixture({ autocomplete: 'none' });
|
||||
// @ts-expect-error
|
||||
expect(el._inputNode.getAttribute('aria-autocomplete')).to.equal('none');
|
||||
});
|
||||
|
||||
it('updates aria-activedescendant on textbox node', async () => {
|
||||
let el = /** @type {LionCombobox} */ (await fixture(html`
|
||||
const el = /** @type {LionCombobox} */ (await fixture(html`
|
||||
<lion-combobox name="foo" autocomplete="none">
|
||||
<lion-option .choiceValue="${'Artichoke'}" id="artichoke-option">Artichoke</lion-option>
|
||||
<lion-option .choiceValue="${'Chard'}" id="chard-option">Chard</lion-option>
|
||||
|
|
@ -1440,20 +1526,26 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
|
||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(null);
|
||||
const elProts = getProtectedMembers(el);
|
||||
|
||||
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
null,
|
||||
);
|
||||
expect(el.formElements[1].active).to.equal(false);
|
||||
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(null);
|
||||
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
null,
|
||||
);
|
||||
// el._inputNode.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
|
||||
mimicKeyPress(el._inputNode, 'ArrowDown');
|
||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
mimicKeyPress(elProts.inputNode, 'ArrowDown');
|
||||
expect(elProts.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
'artichoke-option',
|
||||
);
|
||||
expect(el.formElements[1].active).to.equal(false);
|
||||
|
||||
el = /** @type {LionCombobox} */ (await fixture(html`
|
||||
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
||||
<lion-combobox name="foo" autocomplete="both" match-mode="begin">
|
||||
<lion-option .choiceValue="${'Artichoke'}">Artichoke</lion-option>
|
||||
<lion-option .choiceValue="${'Chard'}">Chard</lion-option>
|
||||
|
|
@ -1461,20 +1553,23 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'Victoria Plum'}">Victoria Plum</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
el.formElements[1].id,
|
||||
);
|
||||
expect(el.formElements[1].active).to.equal(true);
|
||||
|
||||
el.autocomplete = 'list';
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||
await el.updateComplete;
|
||||
expect(el._activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
el.formElements[1].id,
|
||||
const el2Prots = getProtectedMembers(el2);
|
||||
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el2), 'ch');
|
||||
await el2.updateComplete;
|
||||
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
el2.formElements[1].id,
|
||||
);
|
||||
expect(el.formElements[1].active).to.equal(true);
|
||||
expect(el2.formElements[1].active).to.equal(true);
|
||||
|
||||
el2.autocomplete = 'list';
|
||||
mimicUserTyping(/** @type {LionCombobox} */ (el), 'ch');
|
||||
await el2.updateComplete;
|
||||
expect(el2Prots.activeDescendantOwnerNode.getAttribute('aria-activedescendant')).to.equal(
|
||||
el2.formElements[1].id,
|
||||
);
|
||||
expect(el2.formElements[1].active).to.equal(true);
|
||||
});
|
||||
|
||||
it('adds aria-label to highlighted options', async () => {
|
||||
|
|
@ -1496,7 +1591,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._comboboxNode.contains(el._inputNode)).to.be.true;
|
||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(comboboxNode.contains(inputNode)).to.be.true;
|
||||
});
|
||||
|
||||
it('has one input node with [role=combobox] in v1.0', async () => {
|
||||
|
|
@ -1505,7 +1602,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._comboboxNode).to.equal(el._inputNode);
|
||||
const { comboboxNode, inputNode } = getProtectedMembers(el);
|
||||
|
||||
expect(comboboxNode).to.equal(inputNode);
|
||||
});
|
||||
|
||||
it('autodetects aria version and sets it to 1.1 on Chromium browsers', async () => {
|
||||
|
|
@ -1517,7 +1616,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el._ariaVersion).to.equal('1.1');
|
||||
const elProts = getProtectedMembers(el);
|
||||
|
||||
expect(elProts.ariaVersion).to.equal('1.1');
|
||||
|
||||
browserDetection.isChromium = false;
|
||||
const el2 = /** @type {LionCombobox} */ (await fixture(html`
|
||||
|
|
@ -1525,7 +1626,9 @@ describe('lion-combobox', () => {
|
|||
<lion-option .choiceValue="${'10'}" checked>Item 1</lion-option>
|
||||
</lion-combobox>
|
||||
`));
|
||||
expect(el2._ariaVersion).to.equal('1.0');
|
||||
const el2Prots = getProtectedMembers(el2);
|
||||
|
||||
expect(el2Prots.ariaVersion).to.equal('1.0');
|
||||
|
||||
// restore...
|
||||
browserDetection.isChromium = browserDetectionIsChromiumOriginal;
|
||||
|
|
@ -1545,8 +1648,10 @@ describe('lion-combobox', () => {
|
|||
</lion-combobox>
|
||||
`));
|
||||
|
||||
const { comboboxNode } = getProtectedMembers(el);
|
||||
|
||||
// activate opened listbox
|
||||
el._comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
comboboxNode.dispatchEvent(new Event('focusin', { bubbles: true, composed: true }));
|
||||
mimicUserTyping(el, 'ch');
|
||||
await el.updateComplete;
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,18 @@ const DelegateMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
/** @type {DelegateEvent[]} */
|
||||
/**
|
||||
* @type {DelegateEvent[]}
|
||||
* @private
|
||||
*/
|
||||
this.__eventsQueue = [];
|
||||
|
||||
/** @type {Object.<string,?>} */
|
||||
/**
|
||||
* @type {Object.<string,?>}
|
||||
* @private
|
||||
*/
|
||||
this.__propertiesQueue = {};
|
||||
/** @private */
|
||||
this.__setupPropertyDelegation();
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +108,9 @@ const DelegateMixinImplementation = superclass =>
|
|||
super.removeAttribute(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_connectDelegateMixin() {
|
||||
if (this.__connectedDelegateMixin) return;
|
||||
|
||||
|
|
@ -117,6 +127,9 @@ const DelegateMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupPropertyDelegation() {
|
||||
const propertyNames = this.delegations.properties.concat(this.delegations.methods);
|
||||
propertyNames.forEach(propertyName => {
|
||||
|
|
@ -153,6 +166,9 @@ const DelegateMixinImplementation = superclass =>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__initialAttributeDelegation() {
|
||||
const attributeNames = this.delegations.attributes;
|
||||
attributeNames.forEach(attributeName => {
|
||||
|
|
@ -164,12 +180,18 @@ const DelegateMixinImplementation = superclass =>
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__emptyEventListenerQueue() {
|
||||
this.__eventsQueue.forEach(ev => {
|
||||
this.delegationTarget.addEventListener(ev.type, ev.handler, ev.opts);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__emptyPropertiesQueue() {
|
||||
Object.keys(this.__propertiesQueue).forEach(propName => {
|
||||
this.delegationTarget[propName] = this.__propertiesQueue[propName];
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ const DisabledMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/** @protected */
|
||||
this._requestedToBeDisabled = false;
|
||||
/** @private */
|
||||
this.__isUserSettingDisabled = true;
|
||||
/** @private */
|
||||
this.__restoreDisabledTo = false;
|
||||
this.disabled = false;
|
||||
}
|
||||
|
|
@ -43,7 +46,10 @@ const DisabledMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/** @param {boolean} value */
|
||||
/**
|
||||
* @param {boolean} value
|
||||
* @private
|
||||
*/
|
||||
__internalSetDisabled(value) {
|
||||
this.__isUserSettingDisabled = false;
|
||||
this.disabled = value;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ const DisabledWithTabIndexMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/** @private */
|
||||
this.__isUserSettingTabIndex = true;
|
||||
/** @private */
|
||||
this.__restoreTabIndexTo = 0;
|
||||
this.__internalSetTabIndex(0);
|
||||
}
|
||||
|
|
@ -48,6 +50,7 @@ const DisabledWithTabIndexMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {number} value
|
||||
* @private
|
||||
*/
|
||||
__internalSetTabIndex(value) {
|
||||
this.__isUserSettingTabIndex = false;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const SlotMixinImplementation = superclass =>
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/** @private */
|
||||
this.__privateSlots = new Set(null);
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +35,9 @@ const SlotMixinImplementation = superclass =>
|
|||
this._connectSlotMixin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_connectSlotMixin() {
|
||||
if (!this.__isConnectedSlotMixin) {
|
||||
Object.keys(this.slots).forEach(slotName => {
|
||||
|
|
@ -55,6 +59,7 @@ const SlotMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {string} slotName Name of the slot
|
||||
* @return {boolean} true if given slot name been created by SlotMixin
|
||||
* @protected
|
||||
*/
|
||||
_isPrivateSlot(slotName) {
|
||||
return this.__privateSlots.has(slotName);
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ import { OverlayMixin, withModalDialogConfig } from '@lion/overlays';
|
|||
export class LionDialog extends OverlayMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
/** @private */
|
||||
this.__toggle = () => {
|
||||
this.opened = !this.opened;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_defineOverlayConfig() {
|
||||
return {
|
||||
|
|
@ -16,6 +20,9 @@ export class LionDialog extends OverlayMixin(LitElement) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_setupOpenCloseListeners() {
|
||||
super._setupOpenCloseListeners();
|
||||
if (this._overlayInvokerNode) {
|
||||
|
|
@ -23,6 +30,9 @@ export class LionDialog extends OverlayMixin(LitElement) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_teardownOpenCloseListeners() {
|
||||
super._teardownOpenCloseListeners();
|
||||
if (this._overlayInvokerNode) {
|
||||
|
|
|
|||
|
|
@ -22,11 +22,15 @@ import { FormGroupMixin } from '@lion/form-core';
|
|||
export class LionFieldset extends FormGroupMixin(LitElement) {
|
||||
constructor() {
|
||||
super();
|
||||
/** @override FormRegistrarMixin */
|
||||
/**
|
||||
* @override FormRegistrarMixin
|
||||
* @protected
|
||||
*/
|
||||
this._isFormOrFieldset = true;
|
||||
/**
|
||||
* @type {'child' | 'choice-group' | 'fieldset'}
|
||||
* @override FormControlMixin
|
||||
* @protected
|
||||
*/
|
||||
this._repropagationRole = 'fieldset';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,14 +46,23 @@ const FocusMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__onFocus() {
|
||||
this.focused = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__onBlur() {
|
||||
this.focused = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__registerEventsForFocusMixin() {
|
||||
/**
|
||||
* focus
|
||||
|
|
@ -98,6 +107,9 @@ const FocusMixinImplementation = superclass =>
|
|||
this._inputNode.addEventListener('focusout', this.__redispatchFocusout);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__teardownEventsForFocusMixin() {
|
||||
this._inputNode.removeEventListener(
|
||||
'focus',
|
||||
|
|
|
|||
|
|
@ -274,16 +274,19 @@ const FormControlMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_triggerInitialModelValueChangedEvent() {
|
||||
this.__dispatchInitialModelValueChangedEvent();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_enhanceLightDomClasses() {
|
||||
if (this._inputNode) {
|
||||
this._inputNode.classList.add('form-control');
|
||||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_enhanceLightDomA11y() {
|
||||
const { _inputNode, _labelNode, _helpTextNode, _feedbackNode } = this;
|
||||
|
||||
|
|
@ -310,6 +313,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
* When boolean attribute data-label or data-description is found,
|
||||
* the slot element will be connected to the input via aria-labelledby or aria-describedby
|
||||
* @param {string[]} additionalSlots
|
||||
* @protected
|
||||
*/
|
||||
_enhanceLightDomA11yForAdditionalSlots(
|
||||
additionalSlots = ['prefix', 'suffix', 'before', 'after'],
|
||||
|
|
@ -335,6 +339,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
* @param {string} attrName
|
||||
* @param {HTMLElement[]} nodes
|
||||
* @param {boolean|undefined} reorder
|
||||
* @private
|
||||
*/
|
||||
__reflectAriaAttr(attrName, nodes, reorder) {
|
||||
if (this._inputNode) {
|
||||
|
|
@ -393,6 +398,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_groupOneTemplate() {
|
||||
return html` ${this._labelTemplate()} ${this._helpTextTemplate()} `;
|
||||
|
|
@ -400,6 +406,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_groupTwoTemplate() {
|
||||
return html` ${this._inputGroupTemplate()} ${this._feedbackTemplate()} `;
|
||||
|
|
@ -407,6 +414,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_labelTemplate() {
|
||||
|
|
@ -419,6 +427,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_helpTextTemplate() {
|
||||
|
|
@ -431,6 +440,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_inputGroupTemplate() {
|
||||
return html`
|
||||
|
|
@ -447,6 +457,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupBeforeTemplate() {
|
||||
|
|
@ -459,6 +470,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult | nothing}
|
||||
* @protected
|
||||
*/
|
||||
_inputGroupPrefixTemplate() {
|
||||
return !Array.from(this.children).find(child => child.slot === 'prefix')
|
||||
|
|
@ -472,6 +484,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupInputTemplate() {
|
||||
|
|
@ -484,6 +497,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult | nothing}
|
||||
* @protected
|
||||
*/
|
||||
_inputGroupSuffixTemplate() {
|
||||
return !Array.from(this.children).find(child => child.slot === 'suffix')
|
||||
|
|
@ -497,6 +511,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupAfterTemplate() {
|
||||
|
|
@ -509,6 +524,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_feedbackTemplate() {
|
||||
|
|
@ -522,6 +538,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {?} modelValue
|
||||
* @return {boolean}
|
||||
* @protected
|
||||
*/
|
||||
// @ts-ignore FIXME: Move to FormatMixin? Since there we have access to modelValue prop
|
||||
_isEmpty(modelValue = this.modelValue) {
|
||||
|
|
@ -675,6 +692,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {Array.<HTMLElement|undefined>}
|
||||
* @protected
|
||||
*/
|
||||
// Returns dom references to all elements that should be referred to by field(s)
|
||||
_getAriaDescriptionElements() {
|
||||
|
|
@ -727,6 +745,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {string} slotName
|
||||
* @return {HTMLElement | undefined}
|
||||
* @private
|
||||
*/
|
||||
__getDirectSlotChild(slotName) {
|
||||
return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
|
||||
|
|
@ -734,6 +753,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__dispatchInitialModelValueChangedEvent() {
|
||||
// When we are not a fieldset / choice-group, we don't need to wait for our children
|
||||
// to send a unified event
|
||||
|
|
@ -762,12 +782,14 @@ const FormControlMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
||||
_onBeforeRepropagateChildrenValues(ev) {}
|
||||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @private
|
||||
*/
|
||||
__repropagateChildrenValues(ev) {
|
||||
// Allows sub classes to internally listen to the children change events
|
||||
|
|
@ -841,6 +863,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
* TODO: Extend this in choice group so that target is always a choice input and multipleChoice exists.
|
||||
* This will fix the types and reduce the need for ignores/expect-errors
|
||||
* @param {EventTarget & import('../types/choice-group/ChoiceInputMixinTypes').ChoiceInputHost} target
|
||||
* @protected
|
||||
*/
|
||||
_repropagationCondition(target) {
|
||||
return !(
|
||||
|
|
@ -861,6 +884,7 @@ const FormControlMixinImplementation = superclass =>
|
|||
* _onLabelClick() {
|
||||
* this._invokerNode.focus();
|
||||
* }
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_onLabelClick() {}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ const FormatMixinImplementation = superclass =>
|
|||
* @param {{source:'model'|'serialized'|'formatted'|null}} config - the type of value that triggered this method. It should not be
|
||||
* set again, so that its observer won't be triggered. Can be:
|
||||
* 'model'|'formatted'|'serialized'.
|
||||
* @protected
|
||||
*/
|
||||
_calculateValues({ source } = { source: null }) {
|
||||
if (this.__preventRecursiveTrigger) return; // prevent infinite loops
|
||||
|
|
@ -243,6 +244,7 @@ const FormatMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {string|undefined} value
|
||||
* @return {?}
|
||||
* @private
|
||||
*/
|
||||
__callParser(value = this.formattedValue) {
|
||||
// A) check if we need to parse at all
|
||||
|
|
@ -280,6 +282,7 @@ const FormatMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @returns {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
__callFormatter() {
|
||||
// - Why check for this.hasError?
|
||||
|
|
@ -316,6 +319,7 @@ const FormatMixinImplementation = superclass =>
|
|||
/**
|
||||
* Observer Handlers
|
||||
* @param {{ modelValue: unknown; }[]} args
|
||||
* @protected
|
||||
*/
|
||||
_onModelValueChanged(...args) {
|
||||
this._calculateValues({ source: 'model' });
|
||||
|
|
@ -326,6 +330,7 @@ const FormatMixinImplementation = superclass =>
|
|||
* @param {{ modelValue: unknown; }[]} args
|
||||
* This is wrapped in a distinct method, so that parents can control when the changed event
|
||||
* is fired. For objects, a deep comparison might be needed.
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_dispatchModelValueChangedEvent(...args) {
|
||||
|
|
@ -346,6 +351,7 @@ const FormatMixinImplementation = superclass =>
|
|||
* Downwards syncing should only happen for `LionField`.value changes from 'above'.
|
||||
* This triggers _onModelValueChanged and connects user input
|
||||
* to the parsing/formatting/serializing loop.
|
||||
* @protected
|
||||
*/
|
||||
_syncValueUpwards() {
|
||||
this.value = this.__callPreprocessor(this.value);
|
||||
|
|
@ -357,6 +363,7 @@ const FormatMixinImplementation = superclass =>
|
|||
* - flow [1] will always be reflected back
|
||||
* - flow [2] will not be reflected back when this flow was triggered via
|
||||
* `@user-input-changed` (this will happen later, when `formatOn` condition is met)
|
||||
* @protected
|
||||
*/
|
||||
_reflectBackFormattedValueToUser() {
|
||||
if (this._reflectBackOn()) {
|
||||
|
|
@ -367,6 +374,7 @@ const FormatMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @return {boolean}
|
||||
* @protected
|
||||
*/
|
||||
_reflectBackOn() {
|
||||
return !this.__isHandlingUserInput;
|
||||
|
|
@ -376,6 +384,7 @@ const FormatMixinImplementation = superclass =>
|
|||
// ("input" for <input> or "change" for <select>(mainly for IE)) a different event should be
|
||||
// used as source for the "user-input-changed" event (which can be seen as an abstraction
|
||||
// layer on top of other events (input, change, whatever))
|
||||
/** @protected */
|
||||
_proxyInputEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
|
|
@ -385,6 +394,7 @@ const FormatMixinImplementation = superclass =>
|
|||
);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_onUserInputChanged() {
|
||||
// Upwards syncing. Most properties are delegated right away, value is synced to
|
||||
// `LionField`, to be able to act on (imperatively set) value changes
|
||||
|
|
|
|||
|
|
@ -157,10 +157,18 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
this.prefilled = !this._isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches custom event on touched state change
|
||||
* @protected
|
||||
*/
|
||||
_onTouchedChanged() {
|
||||
this.dispatchEvent(new CustomEvent('touched-changed', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches custom event on touched state change
|
||||
* @protected
|
||||
*/
|
||||
_onDirtyChanged() {
|
||||
this.dispatchEvent(new CustomEvent('dirty-changed', { bubbles: true, composed: true }));
|
||||
}
|
||||
|
|
@ -181,6 +189,7 @@ const InteractionStateMixinImplementation = superclass =>
|
|||
* (a user leaves(blurs) a field).
|
||||
* When a user enters a field without altering the value(making it `dirty`),
|
||||
* an error message shouldn't be shown either.
|
||||
* @protected
|
||||
*/
|
||||
_showFeedbackConditionFor() {
|
||||
return (this.touched && this.dirty) || this.prefilled || this.submitted;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ export class LionField extends FormControlMixin(
|
|||
this.submitted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets modelValue to initial value.
|
||||
* Interaction states are cleared
|
||||
*/
|
||||
reset() {
|
||||
this.modelValue = this._initialModelValue;
|
||||
this.resetInteractionState();
|
||||
|
|
@ -84,6 +88,10 @@ export class LionField extends FormControlMixin(
|
|||
this.modelValue = ''; // can't set null here, because IE11 treats it as a string
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches custom bubble event
|
||||
* @protected
|
||||
*/
|
||||
_onChange() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ const NativeTextFieldMixinImplementation = superclass =>
|
|||
/**
|
||||
* Restores the cursor to its original position after updating the value.
|
||||
* @param {string} newValue The value that should be saved.
|
||||
* @protected
|
||||
*/
|
||||
_setValueAndPreserveCaret(newValue) {
|
||||
// Only preserve caret if focused (changing selectionStart will move focus in Safari)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get modelValue() {
|
||||
const elems = this._getCheckedElements();
|
||||
if (this.multipleChoice) {
|
||||
|
|
@ -127,15 +128,21 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
super();
|
||||
this.multipleChoice = false;
|
||||
/** @type {'child'|'choice-group'|'fieldset'} */
|
||||
/** @type {'child'|'choice-group'|'fieldset'}
|
||||
* @protected
|
||||
*/
|
||||
this._repropagationRole = 'choice-group'; // configures event propagation logic of FormControlMixin
|
||||
|
||||
/** @private */
|
||||
this.__isInitialModelValue = true;
|
||||
/** @private */
|
||||
this.__isInitialSerializedValue = true;
|
||||
/** @private */
|
||||
this.__isInitialFormattedValue = true;
|
||||
/** @type {Promise<any> & {done?:boolean}} */
|
||||
this.registrationComplete = new Promise((resolve, reject) => {
|
||||
/** @private */
|
||||
this.__resolveRegistrationComplete = resolve;
|
||||
/** @private */
|
||||
this.__rejectRegistrationComplete = reject;
|
||||
});
|
||||
this.registrationComplete.done = false;
|
||||
|
|
@ -156,6 +163,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
super.connectedCallback();
|
||||
// Double microtask queue to account for Webkit race condition
|
||||
Promise.resolve().then(() =>
|
||||
// @ts-ignore
|
||||
Promise.resolve().then(() => this.__resolveRegistrationComplete()),
|
||||
);
|
||||
|
||||
|
|
@ -203,6 +211,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @override from FormControlMixin
|
||||
* @protected
|
||||
*/
|
||||
_triggerInitialModelValueChangedEvent() {
|
||||
this.registrationComplete.then(() => {
|
||||
|
|
@ -213,6 +222,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
/**
|
||||
* @override
|
||||
* @param {string} property
|
||||
* @protected
|
||||
*/
|
||||
_getFromAllFormElements(property, filterCondition = () => true) {
|
||||
// For modelValue, serializedValue and formattedValue, an exception should be made,
|
||||
|
|
@ -229,6 +239,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {FormControl} child
|
||||
* @protected
|
||||
*/
|
||||
_throwWhenInvalidChildModelValue(child) {
|
||||
if (
|
||||
|
|
@ -246,6 +257,9 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_isEmpty() {
|
||||
if (this.multipleChoice) {
|
||||
return this.modelValue.length === 0;
|
||||
|
|
@ -262,6 +276,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent & {target:FormControl}} ev
|
||||
* @protected
|
||||
*/
|
||||
_checkSingleChoiceElements(ev) {
|
||||
const { target } = ev;
|
||||
|
|
@ -278,6 +293,9 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
// this.__triggerCheckedValueChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_getCheckedElements() {
|
||||
// We want to filter out disabled values by default
|
||||
return this.formElements.filter(el => el.checked && !el.disabled);
|
||||
|
|
@ -286,6 +304,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {string | any[]} value
|
||||
* @param {Function} check
|
||||
* @protected
|
||||
*/
|
||||
_setCheckedElements(value, check) {
|
||||
for (let i = 0; i < this.formElements.length; i += 1) {
|
||||
|
|
@ -309,6 +328,9 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setChoiceGroupTouched() {
|
||||
const value = this.modelValue;
|
||||
if (value != null && value !== this.__previousCheckedValue) {
|
||||
|
|
@ -321,6 +343,7 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|||
/**
|
||||
* @override FormControlMixin
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_onBeforeRepropagateChildrenValues(ev) {
|
||||
// Normalize target, since we might receive 'portal events' (from children in a modal,
|
||||
|
|
|
|||
|
|
@ -127,7 +127,9 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
super();
|
||||
this.modelValue = { value: '', checked: false };
|
||||
this.disabled = false;
|
||||
/** @protected */
|
||||
this._preventDuplicateLabelClick = this._preventDuplicateLabelClick.bind(this);
|
||||
/** @protected */
|
||||
this._toggleChecked = this._toggleChecked.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -177,10 +179,16 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_choiceGraphicTemplate() {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_afterTemplate() {
|
||||
return nothing;
|
||||
}
|
||||
|
|
@ -208,6 +216,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
* This method prevents the duplicate click and ensures the correct isTrusted event
|
||||
* with the correct event.target arrives at the host.
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_preventDuplicateLabelClick(ev) {
|
||||
|
|
@ -218,7 +227,10 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
this._inputNode.addEventListener('click', __inputClickHandler);
|
||||
}
|
||||
|
||||
/** @param {Event} ev */
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_toggleChecked(ev) {
|
||||
if (this.disabled) {
|
||||
|
|
@ -234,6 +246,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
* to sync differently with parent form group name
|
||||
* Right now it checks tag name match where the parent form group tagname
|
||||
* should include the child field tagname ('checkbox' is included in 'checkbox-group')
|
||||
* @protected
|
||||
*/
|
||||
_syncNameToParentFormGroup() {
|
||||
// @ts-expect-error not all choice inputs have a name prop, because this mixin does not have a strict contract with form control mixin
|
||||
|
|
@ -245,6 +258,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {boolean} checked
|
||||
* @private
|
||||
*/
|
||||
__syncModelCheckedToChecked(checked) {
|
||||
this.checked = checked;
|
||||
|
|
@ -252,11 +266,15 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {any} checked
|
||||
* @private
|
||||
*/
|
||||
__syncCheckedToModel(checked) {
|
||||
this.modelValue = { value: this.choiceValue, checked };
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__syncCheckedToInputElement() {
|
||||
// ._inputNode might not be available yet(slot content)
|
||||
// or at all (no reliance on platform construct, in case of [role=option])
|
||||
|
|
@ -273,6 +291,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
* However on Chrome on Mac whenever you use the keyboard
|
||||
* it fires the input AND change event. Other Browsers only fires the change event.
|
||||
* Therefore we disable the input event here.
|
||||
* @protected
|
||||
*/
|
||||
_proxyInputEvent() {}
|
||||
|
||||
|
|
@ -282,6 +301,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
* (requestUpdateInternal) callback
|
||||
* @param {{ modelValue:unknown }} newV
|
||||
* @param {{ modelValue:unknown }} [old]
|
||||
* @protected
|
||||
*/
|
||||
_onModelValueChanged({ modelValue }, old) {
|
||||
let _old;
|
||||
|
|
@ -321,6 +341,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* Used for required validator.
|
||||
* @protected
|
||||
*/
|
||||
_isEmpty() {
|
||||
return !this.checked;
|
||||
|
|
@ -330,6 +351,7 @@ const ChoiceInputMixinImplementation = superclass =>
|
|||
* @override
|
||||
* Overridden from FormatMixin, since a different modelValue is used for choice inputs.
|
||||
* Synchronization from user input is already arranged in this Mixin.
|
||||
* @protected
|
||||
*/
|
||||
_syncValueUpwards() {}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
return this;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get modelValue() {
|
||||
return this._getFromAllFormElements('modelValue');
|
||||
}
|
||||
|
|
@ -167,6 +168,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.setAttribute('role', 'group');
|
||||
// @ts-ignore
|
||||
Promise.resolve().then(() => this.__resolveRegistrationComplete());
|
||||
|
||||
this.registrationComplete.then(() => {
|
||||
|
|
@ -325,6 +327,7 @@ const FormGroupMixinImplementation = superclass =>
|
|||
*/
|
||||
_getFromAllFormElements(property, filterFn = (/** @type {FormControl} */ el) => !el.disabled) {
|
||||
const result = {};
|
||||
// @ts-ignore
|
||||
this.formElements._keys().forEach(name => {
|
||||
const elem = this.formElements[name];
|
||||
if (elem instanceof FormControlsCollection) {
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export class FormControlsCollection extends Array {
|
|||
/**
|
||||
* @desc Gives back the named keys and filters out array indexes
|
||||
* @return {string[]}
|
||||
* @protected
|
||||
*/
|
||||
_keys() {
|
||||
return Object.keys(this).filter(k => Number.isNaN(Number(k)));
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_onRequestToAddFormElement(ev) {
|
||||
const child = ev.detail.element;
|
||||
|
|
@ -170,6 +171,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_onRequestToChangeFormElementName(ev) {
|
||||
const element = this.formElements[ev.detail.oldName];
|
||||
|
|
@ -181,6 +183,7 @@ const FormRegistrarMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
_onRequestToRemoveFormElement(ev) {
|
||||
const child = ev.detail.element;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const FormRegistrarPortalMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent} ev
|
||||
* @private
|
||||
*/
|
||||
__redispatchEventForFormRegistrarPortalMixin(ev) {
|
||||
ev.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export class AsyncQueue {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
async __run() {
|
||||
this.__running = true;
|
||||
await this.__queue[0]();
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
|||
* @param {string} name
|
||||
* @param {*} newValue
|
||||
* @param {*} oldValue
|
||||
* @private
|
||||
*/
|
||||
static __syncUpdatableHasChanged(name, newValue, oldValue) {
|
||||
// @ts-expect-error accessing private lit property
|
||||
|
|
@ -69,6 +70,7 @@ const SyncUpdatableMixinImplementation = superclass =>
|
|||
return newValue !== oldValue;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__syncUpdatableInitialize() {
|
||||
const ns = this.__SyncUpdatableNamespace;
|
||||
const ctor = /** @type {typeof SyncUpdatableMixin & typeof import('../../types/utils/SyncUpdatableMixinTypes').SyncUpdatableHost} */ (this
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export class LionValidationFeedback extends LitElement {
|
|||
* @param {string | Node | TemplateResult } opts.message message or feedback node or TemplateResult
|
||||
* @param {string} [opts.type]
|
||||
* @param {Validator} [opts.validator]
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_messageTemplate({ message }) {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @overridable
|
||||
* Adds "._feedbackNode" as described below
|
||||
*/
|
||||
// @ts-ignore
|
||||
get slots() {
|
||||
/**
|
||||
* FIXME: Ugly workaround https://github.com/microsoft/TypeScript/issues/40110
|
||||
|
|
@ -141,6 +142,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/** @type {Object.<string, Object.<string, boolean>>} */
|
||||
this.validationStates = {};
|
||||
|
||||
/** @protected */
|
||||
this._visibleMessagesAmount = 1;
|
||||
|
||||
this.isPending = false;
|
||||
|
|
@ -150,23 +152,35 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/** @type {Validator[]} */
|
||||
this.defaultValidators = [];
|
||||
|
||||
/** @type {Validator[]} */
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__syncValidationResult = [];
|
||||
|
||||
/** @type {Validator[]} */
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__asyncValidationResult = [];
|
||||
|
||||
/**
|
||||
* @desc contains results from sync Validators, async Validators and ResultValidators
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__validationResult = [];
|
||||
/** @type {Validator[]} */
|
||||
/**
|
||||
* @type {Validator[]}
|
||||
* @private
|
||||
*/
|
||||
this.__prevValidationResult = [];
|
||||
/** @type {Validator[]} */
|
||||
this.__prevShownValidationResult = [];
|
||||
|
||||
/** @private */
|
||||
this.__onValidatorUpdated = this.__onValidatorUpdated.bind(this);
|
||||
/** @protected */
|
||||
this._updateFeedbackComponent = this._updateFeedbackComponent.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -358,6 +372,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @param {Validator[]} syncValidators
|
||||
* @param {unknown} value
|
||||
* @param {{ hasAsync: boolean }} opts
|
||||
* @private
|
||||
*/
|
||||
__executeSyncValidators(syncValidators, value, { hasAsync }) {
|
||||
if (syncValidators.length) {
|
||||
|
|
@ -372,6 +387,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @desc step A3, calls __finishValidation
|
||||
* @param {Validator[]} asyncValidators all Validators except required and ResultValidators
|
||||
* @param {?} value
|
||||
* @private
|
||||
*/
|
||||
async __executeAsyncValidators(asyncValidators, value) {
|
||||
if (asyncValidators.length) {
|
||||
|
|
@ -389,6 +405,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/**
|
||||
* @desc step B, called by __finishValidation
|
||||
* @param {Validator[]} regularValidationResult result of steps 1-3
|
||||
* @private
|
||||
*/
|
||||
__executeResultValidators(regularValidationResult) {
|
||||
const resultValidators = /** @type {ResultValidator[]} */ (this._allValidators.filter(v => {
|
||||
|
|
@ -409,6 +426,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* @param {object} options
|
||||
* @param {'sync'|'async'} options.source
|
||||
* @param {boolean} [options.hasAsync] whether async validators are configured in this run.
|
||||
* @private
|
||||
* If not, we have nothing left to wait for.
|
||||
*/
|
||||
__finishValidation({ source, hasAsync }) {
|
||||
|
|
@ -442,11 +460,15 @@ export const ValidateMixinImplementation = superclass =>
|
|||
this.dispatchEvent(new Event('validate-performed', { bubbles: true }));
|
||||
if (source === 'async' || !hasAsync) {
|
||||
if (this.__validateCompleteResolve) {
|
||||
// @ts-ignore
|
||||
this.__validateCompleteResolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__clearValidationResults() {
|
||||
this.__syncValidationResult = [];
|
||||
this.__asyncValidationResult = [];
|
||||
|
|
@ -454,6 +476,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {Event|CustomEvent} e
|
||||
* @private
|
||||
*/
|
||||
__onValidatorUpdated(e) {
|
||||
if (e.type === 'param-changed' || e.type === 'config-changed') {
|
||||
|
|
@ -461,6 +484,9 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__setupValidators() {
|
||||
const events = ['param-changed', 'config-changed'];
|
||||
if (this.__prevValidators) {
|
||||
|
|
@ -504,6 +530,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {?} v
|
||||
* @private
|
||||
*/
|
||||
__isEmpty(v) {
|
||||
if (typeof this._isEmpty === 'function') {
|
||||
|
|
@ -533,6 +560,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {Validator[]} validators list of objects having a .getMessage method
|
||||
* @return {Promise.<FeedbackMessage[]>}
|
||||
* @private
|
||||
*/
|
||||
async __getFeedbackMessages(validators) {
|
||||
let fieldName = await this.fieldName;
|
||||
|
|
@ -541,6 +569,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
if (validator.config.fieldName) {
|
||||
fieldName = await validator.config.fieldName;
|
||||
}
|
||||
// @ts-ignore
|
||||
const message = await validator._getMessage({
|
||||
modelValue: this.modelValue,
|
||||
formControl: this,
|
||||
|
|
@ -564,6 +593,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* - we compute the 'show' flag (like 'hasErrorVisible') for all types
|
||||
* - we set the customValidity message of the highest prio Validator
|
||||
* - we set aria-invalid="true" in case hasErrorVisible is true
|
||||
* @protected
|
||||
*/
|
||||
_updateFeedbackComponent() {
|
||||
const { _feedbackNode } = this;
|
||||
|
|
@ -600,6 +630,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
/**
|
||||
* Show the validity feedback when returning true, don't show when false
|
||||
* @param {string} type
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
_showFeedbackConditionFor(type) {
|
||||
|
|
@ -608,6 +639,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {string} type
|
||||
* @protected
|
||||
*/
|
||||
_hasFeedbackVisibleFor(type) {
|
||||
return (
|
||||
|
|
@ -636,6 +668,9 @@ export const ValidateMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
*/
|
||||
_updateShouldShowFeedbackFor() {
|
||||
const ctor = /** @type {typeof import('../../types/validate/ValidateMixinTypes').ValidateHost} */ (this
|
||||
.constructor);
|
||||
|
|
@ -656,6 +691,7 @@ export const ValidateMixinImplementation = superclass =>
|
|||
* also filter out occurrences (based on interaction states)
|
||||
* @param {{ validationResult: Validator[] }} opts
|
||||
* @return {Validator[]} ordered list of Validators with feedback messages visible to the
|
||||
* @protected
|
||||
* end user
|
||||
*/
|
||||
_prioritizeAndFilterFeedback({ validationResult }) {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ export class Validator {
|
|||
* @overridable
|
||||
* @param {MessageData} [data]
|
||||
* @returns {Promise<string|Node>}
|
||||
* @protected
|
||||
*/
|
||||
async _getMessage(data) {
|
||||
const ctor = /** @type {typeof Validator} */ (this.constructor);
|
||||
|
|
@ -130,6 +131,9 @@ export class Validator {
|
|||
*/
|
||||
abortExecution() {} // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
__fakeExtendsEventTarget() {
|
||||
const delegate = document.createDocumentFragment();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { LitElement } from '@lion/core';
|
||||
// @ts-ignore
|
||||
import { localizeTearDown } from '@lion/localize/test-helpers';
|
||||
import {
|
||||
defineCE,
|
||||
|
|
@ -10,6 +11,7 @@ import {
|
|||
aTimeout,
|
||||
} from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
// @ts-ignore
|
||||
import { IsNumber, Validator, LionField } from '@lion/form-core';
|
||||
import '@lion/form-core/define';
|
||||
import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
||||
|
|
@ -19,6 +21,7 @@ import { FormGroupMixin } from '../../src/form-group/FormGroupMixin.js';
|
|||
*/
|
||||
export function runFormGroupMixinSuite(cfg = {}) {
|
||||
class FormChild extends LionField {
|
||||
// @ts-ignore
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
|
|
@ -85,9 +88,11 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
// TODO: Tests below belong to FormRegistrarMixin. Preferably run suite integration test
|
||||
it(`${tagString} has an up to date list of every form element in .formElements`, async () => {
|
||||
const el = /** @type {FormGroup} */ (await fixture(html`<${tag}>${inputSlots}</${tag}>`));
|
||||
// @ts-ignore
|
||||
expect(el.formElements._keys().length).to.equal(3);
|
||||
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
||||
el.removeChild(el.formElements['hobbies[]'][0]);
|
||||
// @ts-ignore
|
||||
expect(el.formElements._keys().length).to.equal(3);
|
||||
expect(el.formElements['hobbies[]'].length).to.equal(1);
|
||||
});
|
||||
|
|
@ -111,6 +116,7 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
el.formElements['hobbies[]'][0].modelValue = { checked: false, value: 'chess' };
|
||||
el.formElements['hobbies[]'][1].modelValue = { checked: false, value: 'rugby' };
|
||||
|
||||
// @ts-ignore
|
||||
expect(el.formElements._keys().length).to.equal(3);
|
||||
expect(el.formElements['hobbies[]'].length).to.equal(2);
|
||||
expect(el.formElements['hobbies[]'][0].modelValue.value).to.equal('chess');
|
||||
|
|
@ -202,12 +208,15 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
html`<${childTag} name="lastName"></${childTag}>`,
|
||||
));
|
||||
|
||||
// @ts-ignore
|
||||
expect(el.formElements._keys().length).to.equal(3);
|
||||
|
||||
el.appendChild(newField);
|
||||
// @ts-ignore
|
||||
expect(el.formElements._keys().length).to.equal(4);
|
||||
|
||||
el._inputNode.removeChild(newField);
|
||||
// @ts-ignore
|
||||
expect(el.formElements._keys().length).to.equal(3);
|
||||
});
|
||||
|
||||
|
|
@ -732,7 +741,9 @@ export function runFormGroupMixinSuite(cfg = {}) {
|
|||
newFieldset.formElements['gender[]'][1].modelValue = { checked: false, value: 'female' };
|
||||
newFieldset.formElements.color.modelValue = { checked: false, value: 'blue' };
|
||||
fieldset.formElements.comment.modelValue = 'Foo';
|
||||
// @ts-ignore
|
||||
expect(fieldset.formElements._keys().length).to.equal(2);
|
||||
// @ts-ignore
|
||||
expect(newFieldset.formElements._keys().length).to.equal(3);
|
||||
expect(fieldset.serializedValue).to.deep.equal({
|
||||
comment: 'Foo',
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ async function expectThrowsAsync(method, errorMessage) {
|
|||
expect(error.message).to.equal(errorMessage);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Validator} validatorEl
|
||||
*/
|
||||
function getProtectedMembers(validatorEl) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
getMessage: (...args) => validatorEl._getMessage(...args),
|
||||
};
|
||||
}
|
||||
|
||||
describe('Validator', () => {
|
||||
it('has an "execute" function returning "shown" state', async () => {
|
||||
|
|
@ -52,8 +61,11 @@ describe('Validator', () => {
|
|||
}
|
||||
}
|
||||
|
||||
const vali = new MyValidator({}, { getMessage: 'This is the custom error message' });
|
||||
const { getMessage } = getProtectedMembers(vali);
|
||||
|
||||
await expectThrowsAsync(
|
||||
() => new MyValidator({}, { getMessage: 'This is the custom error message' })._getMessage(),
|
||||
() => getMessage(),
|
||||
"You must provide a value for getMessage of type 'function', you provided a value of type: string",
|
||||
);
|
||||
});
|
||||
|
|
@ -76,7 +88,8 @@ describe('Validator', () => {
|
|||
}
|
||||
}
|
||||
const vali = new MyValidator('myParam', { my: 'config', getMessage: configSpy });
|
||||
vali._getMessage();
|
||||
const { getMessage } = getProtectedMembers(vali);
|
||||
getMessage();
|
||||
|
||||
expect(configSpy.args[0][0]).to.deep.equal({
|
||||
name: 'MyValidator',
|
||||
|
|
@ -102,7 +115,8 @@ describe('Validator', () => {
|
|||
}
|
||||
}
|
||||
const vali = new MyValidator('myParam', { my: 'config' });
|
||||
vali._getMessage();
|
||||
const { getMessage } = getProtectedMembers(vali);
|
||||
getMessage();
|
||||
|
||||
expect(data).to.deep.equal({
|
||||
name: 'MyValidator',
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export declare class FormControlHost {
|
|||
/**
|
||||
* Based on the role, details of handling model-value-changed repropagation differ.
|
||||
*/
|
||||
_repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
||||
protected _repropagationRole: 'child' | 'choice-group' | 'fieldset';
|
||||
/**
|
||||
* By default, a field with _repropagationRole 'choice-group' will act as an
|
||||
* 'endpoint'. This means it will be considered as an individual field: for
|
||||
|
|
@ -129,24 +129,24 @@ export declare class FormControlHost {
|
|||
updated(changedProperties: import('@lion/core').PropertyValues): void;
|
||||
|
||||
render(): TemplateResult;
|
||||
_groupOneTemplate(): TemplateResult;
|
||||
_groupTwoTemplate(): TemplateResult;
|
||||
protected _groupOneTemplate(): TemplateResult;
|
||||
protected _groupTwoTemplate(): TemplateResult;
|
||||
_labelTemplate(): TemplateResult;
|
||||
_helpTextTemplate(): TemplateResult;
|
||||
_inputGroupTemplate(): TemplateResult;
|
||||
protected _inputGroupTemplate(): TemplateResult;
|
||||
_inputGroupBeforeTemplate(): TemplateResult;
|
||||
_inputGroupPrefixTemplate(): TemplateResult | typeof nothing;
|
||||
_inputGroupInputTemplate(): TemplateResult;
|
||||
protected _inputGroupInputTemplate(): TemplateResult;
|
||||
_inputGroupSuffixTemplate(): TemplateResult | typeof nothing;
|
||||
_inputGroupAfterTemplate(): TemplateResult;
|
||||
_feedbackTemplate(): TemplateResult;
|
||||
|
||||
_triggerInitialModelValueChangedEvent(): void;
|
||||
protected _triggerInitialModelValueChangedEvent(): void;
|
||||
_enhanceLightDomClasses(): void;
|
||||
_enhanceLightDomA11y(): void;
|
||||
_enhanceLightDomA11yForAdditionalSlots(additionalSlots?: string[]): void;
|
||||
__reflectAriaAttr(attrName: string, nodes: HTMLElement[], reorder: boolean | undefined): void;
|
||||
_isEmpty(modelValue?: unknown): boolean;
|
||||
protected _isEmpty(modelValue?: unknown): boolean;
|
||||
_getAriaDescriptionElements(): HTMLElement[];
|
||||
public addToAriaLabelledBy(
|
||||
element: HTMLElement,
|
||||
|
|
@ -167,7 +167,7 @@ export declare class FormControlHost {
|
|||
__getDirectSlotChild(slotName: string): HTMLElement;
|
||||
__dispatchInitialModelValueChangedEvent(): void;
|
||||
__repropagateChildrenInitialized: boolean | undefined;
|
||||
_onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
|
||||
protected _onBeforeRepropagateChildrenValues(ev: CustomEvent): void;
|
||||
__repropagateChildrenValues(ev: CustomEvent): void;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ export declare class FormatHost {
|
|||
set value(value: string);
|
||||
|
||||
_calculateValues(opts: { source: 'model' | 'serialized' | 'formatted' | null }): void;
|
||||
__callParser(value: string | undefined): object;
|
||||
private __callParser(value: string | undefined): object;
|
||||
__callFormatter(): string;
|
||||
_onModelValueChanged(arg: { modelValue: unknown }): void;
|
||||
protected _onModelValueChanged(arg: { modelValue: unknown }): void;
|
||||
_dispatchModelValueChangedEvent(): void;
|
||||
_syncValueUpwards(): void;
|
||||
protected _syncValueUpwards(): void;
|
||||
_reflectBackFormattedValueToUser(): void;
|
||||
_reflectBackFormattedValueDebounced(): void;
|
||||
_reflectBackOn(): boolean;
|
||||
_proxyInputEvent(): void;
|
||||
protected _proxyInputEvent(): void;
|
||||
_onUserInputChanged(): void;
|
||||
|
||||
connectedCallback(): void;
|
||||
|
|
|
|||
|
|
@ -28,17 +28,17 @@ export declare class ChoiceGroupHost {
|
|||
|
||||
addFormElement(child: FormControlHost, indexToInsertAt: number): void;
|
||||
|
||||
_triggerInitialModelValueChangedEvent(): void;
|
||||
protected _triggerInitialModelValueChangedEvent(): void;
|
||||
|
||||
_getFromAllFormElements(property: string, filterCondition: Function): void;
|
||||
|
||||
_throwWhenInvalidChildModelValue(child: FormControlHost): void;
|
||||
|
||||
_isEmpty(): void;
|
||||
protected _isEmpty(): void;
|
||||
|
||||
_checkSingleChoiceElements(ev: Event): void;
|
||||
|
||||
_getCheckedElements(): void;
|
||||
protected _getCheckedElements(): void;
|
||||
|
||||
_setCheckedElements(value: any, check: boolean): void;
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ export declare class ChoiceGroupHost {
|
|||
|
||||
__delegateNameAttribute(child: FormControlHost): void;
|
||||
|
||||
_onBeforeRepropagateChildrenValues(ev: Event): void;
|
||||
protected _onBeforeRepropagateChildrenValues(ev: Event): void;
|
||||
}
|
||||
|
||||
export declare function ChoiceGroupImplementation<T extends Constructor<LitElement>>(
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export declare class ChoiceInputHost {
|
|||
render(): TemplateResult;
|
||||
|
||||
_choiceGraphicTemplate(): TemplateResult;
|
||||
_afterTemplate(): TemplateResult;
|
||||
protected _afterTemplate(): TemplateResult;
|
||||
|
||||
connectedCallback(): void;
|
||||
disconnectedCallback(): void;
|
||||
|
|
@ -54,9 +54,9 @@ export declare class ChoiceInputHost {
|
|||
|
||||
__isHandlingUserInput: boolean;
|
||||
|
||||
_proxyInputEvent(): void;
|
||||
protected _proxyInputEvent(): void;
|
||||
|
||||
_onModelValueChanged(
|
||||
protected _onModelValueChanged(
|
||||
newV: { modelValue: ChoiceInputModelValue },
|
||||
oldV: { modelValue: ChoiceInputModelValue },
|
||||
): void;
|
||||
|
|
@ -65,9 +65,9 @@ export declare class ChoiceInputHost {
|
|||
|
||||
formatter(modelValue: ChoiceInputModelValue): string;
|
||||
|
||||
_isEmpty(): void;
|
||||
protected _isEmpty(): void;
|
||||
|
||||
_syncValueUpwards(): void;
|
||||
protected _syncValueUpwards(): void;
|
||||
|
||||
type: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export declare class ElementWithParentFormGroup {
|
|||
}
|
||||
|
||||
export declare class FormRegistrarHost {
|
||||
_isFormOrFieldset: boolean;
|
||||
protected _isFormOrFieldset: boolean;
|
||||
formElements: FormControlsCollection & { [x: string]: any };
|
||||
addFormElement(
|
||||
child:
|
||||
|
|
|
|||
|
|
@ -35,6 +35,16 @@ import '@lion/form-core/define';
|
|||
* @typedef {import('@lion/listbox').LionOption} LionOption
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {FormControl} el
|
||||
*/
|
||||
function getProtectedMembers(el) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
repropagationRole: el._repropagationRole,
|
||||
};
|
||||
}
|
||||
|
||||
const featureName = 'model value';
|
||||
|
||||
const getFirstPaintTitle = /** @param {number} count */ count =>
|
||||
|
|
@ -386,13 +396,14 @@ describe('detail.isTriggeredByUser', () => {
|
|||
* @returns {'RegularField'|'ChoiceField'|'OptionChoiceField'|'ChoiceGroupField'|'FormOrFieldset'}
|
||||
*/
|
||||
function detectType(el) {
|
||||
if (el._repropagationRole === 'child') {
|
||||
const { repropagationRole } = getProtectedMembers(el);
|
||||
if (repropagationRole === 'child') {
|
||||
if (featureDetectChoiceField(el)) {
|
||||
return featureDetectOptionChoiceField(el) ? 'OptionChoiceField' : 'ChoiceField';
|
||||
}
|
||||
return 'RegularField';
|
||||
}
|
||||
return el._repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset';
|
||||
return repropagationRole === 'choice-group' ? 'ChoiceGroupField' : 'FormOrFieldset';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ const throwFormNodeError = () => {
|
|||
export class LionForm extends LionFieldset {
|
||||
constructor() {
|
||||
super();
|
||||
/** @protected */
|
||||
this._submit = this._submit.bind(this);
|
||||
/** @protected */
|
||||
this._reset = this._reset.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +50,7 @@ export class LionForm extends LionFieldset {
|
|||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
_submit(ev) {
|
||||
ev.preventDefault();
|
||||
|
|
@ -66,6 +69,7 @@ export class LionForm extends LionFieldset {
|
|||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @protected
|
||||
*/
|
||||
_reset(ev) {
|
||||
ev.preventDefault();
|
||||
|
|
@ -74,11 +78,13 @@ export class LionForm extends LionFieldset {
|
|||
this.dispatchEvent(new Event('reset', { bubbles: true }));
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__registerEventsForLionForm() {
|
||||
this._formNode.addEventListener('submit', this._submit);
|
||||
this._formNode.addEventListener('reset', this._reset);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__teardownEventsForLionForm() {
|
||||
this._formNode.removeEventListener('submit', this._submit);
|
||||
this._formNode.removeEventListener('reset', this._reset);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
export class IconManager {
|
||||
constructor() {
|
||||
/** @private */
|
||||
this.__iconResolvers = new Map();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export class LionIcon extends LitElement {
|
|||
this.role = 'img';
|
||||
this.ariaLabel = '';
|
||||
this.iconId = '';
|
||||
/** @private */
|
||||
this.__svg = nothing;
|
||||
}
|
||||
|
||||
|
|
@ -141,6 +142,7 @@ export class LionIcon extends LitElement {
|
|||
return this.__svg;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_onLabelChanged() {
|
||||
if (this.ariaLabel) {
|
||||
this.setAttribute('aria-hidden', 'false');
|
||||
|
|
@ -152,6 +154,7 @@ export class LionIcon extends LitElement {
|
|||
|
||||
/**
|
||||
* @param {TemplateResult | nothing} svgObject
|
||||
* @protected
|
||||
*/
|
||||
_renderSvg(svgObject) {
|
||||
validateSvg(svgObject);
|
||||
|
|
@ -161,6 +164,7 @@ export class LionIcon extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get _iconManager() {
|
||||
return icons;
|
||||
|
|
@ -168,6 +172,7 @@ export class LionIcon extends LitElement {
|
|||
|
||||
/**
|
||||
* @param {string} prevIconId
|
||||
* @protected
|
||||
*/
|
||||
async _onIconIdChanged(prevIconId) {
|
||||
if (!this.iconId) {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,27 @@ import { IconManager } from '../src/IconManager.js';
|
|||
* @typedef {import("lit-html").TemplateResult} TemplateResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {IconManager} iconManagerEl
|
||||
*/
|
||||
function getProtectedMembers(iconManagerEl) {
|
||||
// @ts-ignore
|
||||
const { __iconResolvers: iconResolvers } = iconManagerEl;
|
||||
return {
|
||||
iconResolvers,
|
||||
};
|
||||
}
|
||||
|
||||
describe('IconManager', () => {
|
||||
it('starts off with an empty map of resolvers', () => {
|
||||
const manager = new IconManager();
|
||||
expect(manager.__iconResolvers.size).to.equal(0);
|
||||
const { iconResolvers } = getProtectedMembers(manager);
|
||||
expect(iconResolvers.size).to.equal(0);
|
||||
});
|
||||
|
||||
it('allows adding an icon resolver', () => {
|
||||
const manager = new IconManager();
|
||||
const { iconResolvers } = getProtectedMembers(manager);
|
||||
/**
|
||||
* @param {string} iconset
|
||||
* @param {string} icon
|
||||
|
|
@ -24,7 +37,7 @@ describe('IconManager', () => {
|
|||
const resolver = (iconset, icon) => nothing;
|
||||
manager.addIconResolver('foo', resolver);
|
||||
|
||||
expect(manager.__iconResolvers.get('foo')).to.equal(resolver);
|
||||
expect(iconResolvers.get('foo')).to.equal(resolver);
|
||||
});
|
||||
|
||||
it('does not allow adding a resolve for the same namespace twice', () => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { parseAmount } from './parsers.js';
|
|||
*
|
||||
* @customElement lion-input-amount
|
||||
*/
|
||||
// @ts-ignore
|
||||
export class LionInputAmount extends LocalizeMixin(LionInput) {
|
||||
/** @type {any} */
|
||||
static get properties() {
|
||||
|
|
@ -68,10 +69,13 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
this.formatter = formatAmount;
|
||||
/** @type {string | undefined} */
|
||||
this.currency = undefined;
|
||||
/** @private */
|
||||
this.__isPasting = false;
|
||||
|
||||
this.addEventListener('paste', () => {
|
||||
/** @private */
|
||||
this.__isPasting = true;
|
||||
/** @private */
|
||||
this.__parserCallcountSincePaste = 0;
|
||||
});
|
||||
|
||||
|
|
@ -99,17 +103,20 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
|
||||
/**
|
||||
* @override of FormatMixin
|
||||
* @private
|
||||
*/
|
||||
__callParser(value = this.formattedValue) {
|
||||
// TODO: (@daKmor) input and change events both trigger parsing therefore we need to handle the second parse
|
||||
this.__parserCallcountSincePaste += 1;
|
||||
this.__isPasting = this.__parserCallcountSincePaste === 2;
|
||||
this.formatOptions.mode = this.__isPasting === true ? 'pasted' : 'auto';
|
||||
// @ts-ignore
|
||||
return super.__callParser(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override of FormatMixin
|
||||
* @protected
|
||||
*/
|
||||
_reflectBackOn() {
|
||||
return super._reflectBackOn() || this.__isPasting;
|
||||
|
|
@ -118,6 +125,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
/**
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.currency
|
||||
* @protected
|
||||
*/
|
||||
_onCurrencyChanged({ currency }) {
|
||||
if (this._isPrivateSlot('after') && this._currencyDisplayNode) {
|
||||
|
|
@ -128,6 +136,7 @@ export class LionInputAmount extends LocalizeMixin(LionInput) {
|
|||
this.__setCurrencyDisplayLabel();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setCurrencyDisplayLabel() {
|
||||
// TODO: (@erikkroes) for optimal a11y, abbreviations should be part of aria-label
|
||||
// example, for a language switch with text 'en', an aria-label of 'english' is not
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export class LionCalendarOverlayFrame extends LocalizeMixin(LitElement) {
|
|||
];
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__dispatchCloseEvent() {
|
||||
this.dispatchEvent(new Event('close-overlay'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,18 +169,26 @@ export class LionInputDatepicker extends ScopedElementsMixin(
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/** @private */
|
||||
this.__invokerId = this.__createUniqueIdForA11y();
|
||||
/** @protected */
|
||||
this._calendarInvokerSlot = 'suffix';
|
||||
|
||||
// Configuration flags for subclassers
|
||||
/** @protected */
|
||||
this._focusCentralDateOnCalendarOpen = true;
|
||||
/** @protected */
|
||||
this._hideOnUserSelect = true;
|
||||
/** @protected */
|
||||
this._syncOnUserSelect = true;
|
||||
|
||||
/** @private */
|
||||
this.__openCalendarOverlay = this.__openCalendarOverlay.bind(this);
|
||||
/** @protected */
|
||||
this._onCalendarUserSelectedChanged = this._onCalendarUserSelectedChanged.bind(this);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__createUniqueIdForA11y() {
|
||||
return `${this.localName}-${Math.random().toString(36).substr(2, 10)}`;
|
||||
}
|
||||
|
|
@ -197,6 +205,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__toggleInvokerDisabled() {
|
||||
if (this._invokerNode) {
|
||||
const invokerNode = /** @type {HTMLElement & {disabled: boolean}} */ (this._invokerNode);
|
||||
|
|
@ -226,6 +235,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
|
|||
* Defining this overlay as a templates from OverlayMixin
|
||||
* this is our source to give as .contentNode to OverlayController.
|
||||
* Important: do not change the name of this method.
|
||||
* @protected
|
||||
*/
|
||||
_overlayTemplate() {
|
||||
// TODO: add performance optimization to only render the calendar if needed
|
||||
|
|
|
|||
|
|
@ -13,6 +13,27 @@ import { LionInputDatepicker } from '../src/LionInputDatepicker.js';
|
|||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LionInputDatepicker} datepickerEl
|
||||
*/
|
||||
function getProtectedMembersDatepicker(datepickerEl) {
|
||||
// @ts-ignore
|
||||
const { __invokerId: invokerId } = datepickerEl;
|
||||
return {
|
||||
invokerId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {LionCalendar} calendarEl
|
||||
*/
|
||||
function getProtectedMembersCalendar(calendarEl) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
dateSelectedByUser: (...args) => calendarEl.__dateSelectedByUser(...args),
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = /** @type {(arg: TemplateResult) => Promise<LionInputDatepicker>} */ (_fixture);
|
||||
|
||||
describe('<lion-input-datepicker>', () => {
|
||||
|
|
@ -278,8 +299,10 @@ describe('<lion-input-datepicker>', () => {
|
|||
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector(
|
||||
'[data-tag-name="lion-calendar"]',
|
||||
));
|
||||
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
|
||||
// First set a fixed date as if selected by a user
|
||||
calendarEl.__dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
|
||||
dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
|
||||
|
||||
await el.updateComplete;
|
||||
const elObj = new DatepickerInputObject(el);
|
||||
|
||||
|
|
@ -303,8 +326,10 @@ describe('<lion-input-datepicker>', () => {
|
|||
const calendarEl = /** @type {LionCalendar} */ (el.shadowRoot?.querySelector(
|
||||
'[data-tag-name="lion-calendar"]',
|
||||
));
|
||||
const { dateSelectedByUser } = getProtectedMembersCalendar(calendarEl);
|
||||
|
||||
// First set a fixed date as if selected by a user
|
||||
calendarEl.__dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
|
||||
dateSelectedByUser(new Date('December 17, 2020 03:24:00 GMT+0000'));
|
||||
await el.updateComplete;
|
||||
const elObj = new DatepickerInputObject(el);
|
||||
|
||||
|
|
@ -411,6 +436,7 @@ describe('<lion-input-datepicker>', () => {
|
|||
|
||||
const myEl = await fixture(html`<${myTag}></${myTag}>`);
|
||||
const myElObj = new DatepickerInputObject(myEl);
|
||||
const { invokerId } = getProtectedMembersDatepicker(myEl);
|
||||
expect(myElObj.invokerEl.tagName.toLowerCase()).to.equal('my-button');
|
||||
|
||||
// All other tests will still pass. Small checkup:
|
||||
|
|
@ -419,7 +445,7 @@ describe('<lion-input-datepicker>', () => {
|
|||
expect(myElObj.invokerEl.getAttribute('aria-expanded')).to.equal('false');
|
||||
expect(myElObj.invokerEl.getAttribute('aria-haspopup')).to.equal('dialog');
|
||||
expect(myElObj.invokerEl.getAttribute('slot')).to.equal('suffix');
|
||||
expect(myElObj.invokerEl.getAttribute('id')).to.equal(myEl.__invokerId);
|
||||
expect(myElObj.invokerEl.getAttribute('id')).to.equal(invokerId);
|
||||
await myElObj.openCalendar();
|
||||
expect(myElObj.overlayController.isShown).to.equal(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export class LionInputRange extends LionInput {
|
|||
*/
|
||||
this.parser = modelValue => parseFloat(modelValue);
|
||||
this.scopedClass = `${this.localName}-${Math.floor(Math.random() * 10000)}`;
|
||||
/** @private */
|
||||
this.__styleTag = document.createElement('style');
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +115,7 @@ export class LionInputRange extends LionInput {
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_inputGroupTemplate() {
|
||||
return html`
|
||||
<div>
|
||||
|
|
@ -131,6 +133,7 @@ export class LionInputRange extends LionInput {
|
|||
`;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_inputGroupInputTemplate() {
|
||||
return html`
|
||||
<div class="input-group__input">
|
||||
|
|
@ -147,6 +150,7 @@ export class LionInputRange extends LionInput {
|
|||
`;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupStyleTag() {
|
||||
this.__styleTag.textContent = /** @type {typeof LionInputRange} */ (this.constructor)
|
||||
.rangeStyles(unsafeCSS(this.scopedClass))
|
||||
|
|
@ -154,6 +158,7 @@ export class LionInputRange extends LionInput {
|
|||
this.insertBefore(this.__styleTag, this.childNodes[0]);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__teardownStyleTag() {
|
||||
this.removeChild(this.__styleTag);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ export class LionInputStepper extends LionInput {
|
|||
* Get slotted element
|
||||
* @param {String} slotName - slot name
|
||||
* @returns {HTMLButtonElement|Object}
|
||||
* @private
|
||||
*/
|
||||
__getSlot(slotName) {
|
||||
return (
|
||||
|
|
@ -215,6 +216,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Get the increment button node
|
||||
* @returns {Element|null}
|
||||
* @private
|
||||
*/
|
||||
__getIncrementButtonNode() {
|
||||
const renderParent = document.createElement('div');
|
||||
|
|
@ -232,6 +234,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Get the decrement button node
|
||||
* @returns {Element|null}
|
||||
* @private
|
||||
*/
|
||||
__getDecrementButtonNode() {
|
||||
const renderParent = document.createElement('div');
|
||||
|
|
@ -249,6 +252,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Toggle +/- buttons on change
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
_onChange() {
|
||||
super._onChange();
|
||||
|
|
@ -258,6 +262,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Get the decrementor button sign template
|
||||
* @returns {String|import('@lion/core').TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_decrementorSignTemplate() {
|
||||
|
|
@ -267,6 +272,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Get the incrementor button sign template
|
||||
* @returns {String|import('@lion/core').TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_incrementorSignTemplate() {
|
||||
|
|
@ -276,6 +282,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Get the increment button template
|
||||
* @returns {import('@lion/core').TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_decrementorTemplate() {
|
||||
return html`
|
||||
|
|
@ -294,6 +301,7 @@ export class LionInputStepper extends LionInput {
|
|||
/**
|
||||
* Get the decrement button template
|
||||
* @returns {import('@lion/core').TemplateResult}
|
||||
* @protected
|
||||
*/
|
||||
_incrementorTemplate() {
|
||||
return html`
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export class LionInput extends NativeTextFieldMixin(
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__delegateReadOnly() {
|
||||
if (this._inputNode) {
|
||||
this._inputNode.readOnly = this.readOnly;
|
||||
|
|
|
|||
|
|
@ -66,7 +66,9 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
|||
constructor() {
|
||||
super();
|
||||
this.active = false;
|
||||
/** @private */
|
||||
this.__onClick = this.__onClick.bind(this);
|
||||
/** @private */
|
||||
this.__registerEventListeners();
|
||||
}
|
||||
|
||||
|
|
@ -109,14 +111,17 @@ export class LionOption extends DisabledMixin(ChoiceInputMixin(FormRegisteringMi
|
|||
this.setAttribute('role', 'option');
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__registerEventListeners() {
|
||||
this.addEventListener('click', this.__onClick);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__unRegisterEventListeners() {
|
||||
this.removeEventListener('click', this.__onClick);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__onClick() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @override FormControlMixin
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
_inputGroupInputTemplate() {
|
||||
|
|
@ -259,31 +260,58 @@ const ListboxMixinImplementation = superclass =>
|
|||
*/
|
||||
this.selectionFollowsFocus = false;
|
||||
|
||||
/** @type {number | null} */
|
||||
/**
|
||||
* @type {number | null}
|
||||
* @protected
|
||||
*/
|
||||
this._listboxActiveDescendant = null;
|
||||
/** @private */
|
||||
this.__hasInitialSelectedFormElement = false;
|
||||
/** @protected */
|
||||
this._repropagationRole = 'choice-group'; // configures FormControlMixin
|
||||
|
||||
/**
|
||||
* When listbox is coupled to a textbox (in case we are dealing with a combobox),
|
||||
* spaces should not select an element (they need to be put in the textbox)
|
||||
* @protected
|
||||
*/
|
||||
this._listboxReceivesNoFocus = false;
|
||||
|
||||
/** @type {string | string[] | undefined} */
|
||||
/**
|
||||
* @type {string | string[] | undefined}
|
||||
* @private
|
||||
*/
|
||||
this.__oldModelValue = undefined;
|
||||
|
||||
/** @type {EventListener} */
|
||||
/**
|
||||
* @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._listboxOnKeyDown = this._listboxOnKeyDown.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/**
|
||||
* @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._listboxOnClick = this._listboxOnClick.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/**
|
||||
* @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._listboxOnKeyUp = this._listboxOnKeyUp.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/**
|
||||
* @type {EventListener}
|
||||
* @protected
|
||||
*/
|
||||
this._onChildActiveChanged = this._onChildActiveChanged.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/**
|
||||
* @type {EventListener}
|
||||
* @private
|
||||
*/
|
||||
this.__proxyChildModelValueChanged = this.__proxyChildModelValueChanged.bind(this);
|
||||
/** @type {EventListener} */
|
||||
/**
|
||||
* @type {EventListener}
|
||||
* @private
|
||||
*/
|
||||
this.__preventScrollingWithArrowKeys = this.__preventScrollingWithArrowKeys.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -418,11 +446,13 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @override ChoiceGroupMixin: in the select disabled options are still going to a possible
|
||||
* value, for example when prefilling or programmatically setting it.
|
||||
* @protected
|
||||
*/
|
||||
_getCheckedElements() {
|
||||
return this.formElements.filter(el => el.checked);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_setupListboxNode() {
|
||||
if (this._listboxNode) {
|
||||
this.__setupListboxNodeInteractions();
|
||||
|
|
@ -439,6 +469,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_teardownListboxNode() {
|
||||
if (this._listboxNode) {
|
||||
this._listboxNode.removeEventListener('keydown', this._listboxOnKeyDown);
|
||||
|
|
@ -450,6 +481,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {number} currentIndex
|
||||
* @param {number} [offset=1]
|
||||
* @protected
|
||||
*/
|
||||
_getNextEnabledOption(currentIndex, offset = 1) {
|
||||
return this.__getEnabledOption(currentIndex, offset);
|
||||
|
|
@ -458,6 +490,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {number} currentIndex
|
||||
* @param {number} [offset=-1]
|
||||
* @protected
|
||||
*/
|
||||
_getPreviousEnabledOption(currentIndex, offset = -1) {
|
||||
return this.__getEnabledOption(currentIndex, offset);
|
||||
|
|
@ -466,6 +499,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @overridable
|
||||
* @param {Event & { target: LionOption }} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars, class-methods-use-this
|
||||
_onChildActiveChanged({ target }) {
|
||||
|
|
@ -480,6 +514,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
* an item.
|
||||
*
|
||||
* @param {KeyboardEvent} ev - the keydown event object
|
||||
* @protected
|
||||
*/
|
||||
_listboxOnKeyDown(ev) {
|
||||
if (this.disabled) {
|
||||
|
|
@ -565,6 +600,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @overridable
|
||||
* @param {MouseEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
||||
_listboxOnClick(ev) {
|
||||
|
|
@ -587,6 +623,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @overridable
|
||||
* @param {KeyboardEvent} ev
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this, no-unused-vars
|
||||
_listboxOnKeyUp(ev) {
|
||||
|
|
@ -615,11 +652,13 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @configure FormControlMixin
|
||||
* @protected
|
||||
*/
|
||||
_onLabelClick() {
|
||||
this._listboxNode.focus();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupEventListeners() {
|
||||
this._listboxNode.addEventListener(
|
||||
'active-changed',
|
||||
|
|
@ -631,6 +670,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__teardownEventListeners() {
|
||||
this._listboxNode.removeEventListener(
|
||||
'active-changed',
|
||||
|
|
@ -644,6 +684,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {LionOption | null} el
|
||||
* @private
|
||||
*/
|
||||
__setChildActive(el) {
|
||||
this.formElements.forEach(formElement => {
|
||||
|
|
@ -662,6 +703,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {LionOption|LionOption[]} [exclude]
|
||||
* @protected
|
||||
*/
|
||||
_uncheckChildren(exclude = []) {
|
||||
const excludes = Array.isArray(exclude) ? exclude : [exclude];
|
||||
|
|
@ -675,6 +717,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {Event & { target: LionOption }} cfgOrEvent
|
||||
* @private
|
||||
*/
|
||||
__onChildCheckedChanged(cfgOrEvent) {
|
||||
const { target } = cfgOrEvent;
|
||||
|
|
@ -690,6 +733,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
* // TODO: add to choiceGroup
|
||||
* @param {string} attribute
|
||||
* @param {number} value
|
||||
* @private
|
||||
*/
|
||||
__setAttributeForAllFormElements(attribute, value) {
|
||||
this.formElements.forEach(formElement => {
|
||||
|
|
@ -699,6 +743,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {CustomEvent & { target: LionOption; }} ev
|
||||
* @private
|
||||
*/
|
||||
__proxyChildModelValueChanged(ev) {
|
||||
// We need to redispatch the model-value-changed event on 'this', so it will
|
||||
|
|
@ -729,6 +774,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {number} currentIndex
|
||||
* @param {number} offset
|
||||
* @private
|
||||
*/
|
||||
__getEnabledOption(currentIndex, offset) {
|
||||
/**
|
||||
|
|
@ -760,6 +806,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* Moves options put in unnamed slot to slot with [role="listbox"]
|
||||
* @private
|
||||
*/
|
||||
__moveOptionsToListboxNode() {
|
||||
const slot = /** @type {HTMLSlotElement} */ (
|
||||
|
|
@ -780,6 +827,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
* @private
|
||||
*/
|
||||
__preventScrollingWithArrowKeys(ev) {
|
||||
if (this.disabled) {
|
||||
|
|
@ -798,6 +846,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* Helper method used within `._setupListboxNode`
|
||||
* @private
|
||||
*/
|
||||
__setupListboxNodeInteractions() {
|
||||
this._listboxNode.setAttribute('role', 'listbox');
|
||||
|
|
@ -812,6 +861,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
}
|
||||
|
||||
// TODO: move to ChoiceGroupMixin?
|
||||
/** @private */
|
||||
__requestOptionsToBeDisabled() {
|
||||
this.formElements.forEach(el => {
|
||||
if (el.makeRequestToBeDisabled) {
|
||||
|
|
@ -820,6 +870,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
});
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__retractRequestOptionsToBeDisabled() {
|
||||
this.formElements.forEach(el => {
|
||||
if (el.retractRequestToBeDisabled) {
|
||||
|
|
@ -828,6 +879,7 @@ const ListboxMixinImplementation = superclass =>
|
|||
});
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__initInteractionStates() {
|
||||
this.initInteractionState();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,20 +15,35 @@ import isLocalizeESModule from './isLocalizeESModule.js';
|
|||
export class LocalizeManager {
|
||||
// eslint-disable-line no-unused-vars
|
||||
constructor({ autoLoadOnLocaleChange = false, fallbackLocale = '' } = {}) {
|
||||
/** @private */
|
||||
this.__delegationTarget = document.createDocumentFragment();
|
||||
/** @protected */
|
||||
this._autoLoadOnLocaleChange = !!autoLoadOnLocaleChange;
|
||||
/** @protected */
|
||||
this._fallbackLocale = fallbackLocale;
|
||||
|
||||
/** @type {Object.<string, Object.<string, Object>>} */
|
||||
/**
|
||||
* @type {Object.<string, Object.<string, Object>>}
|
||||
* @private
|
||||
*/
|
||||
this.__storage = {};
|
||||
|
||||
/** @type {Map.<RegExp|string, function>} */
|
||||
/**
|
||||
* @type {Map.<RegExp|string, function>}
|
||||
* @private
|
||||
*/
|
||||
this.__namespacePatternsMap = new Map();
|
||||
|
||||
/** @type {Object.<string, function|null>} */
|
||||
/**
|
||||
* @type {Object.<string, function|null>}
|
||||
* @private
|
||||
*/
|
||||
this.__namespaceLoadersCache = {};
|
||||
|
||||
/** @type {Object.<string, Object.<string, Promise.<Object>>>} */
|
||||
/**
|
||||
* @type {Object.<string, Object.<string, Promise.<Object>>>}
|
||||
* @private
|
||||
*/
|
||||
this.__namespaceLoaderPromisesCache = {};
|
||||
|
||||
this.formatNumberOptions = {
|
||||
|
|
@ -50,6 +65,7 @@ export class LocalizeManager {
|
|||
*/
|
||||
const initialLocale = document.documentElement.getAttribute('data-localize-lang');
|
||||
|
||||
/** @protected */
|
||||
this._supportExternalTranslationTools = Boolean(initialLocale);
|
||||
|
||||
if (this._supportExternalTranslationTools) {
|
||||
|
|
@ -61,9 +77,11 @@ export class LocalizeManager {
|
|||
document.documentElement.lang = this.locale || 'en-GB';
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
this._setupHtmlLangAttributeObserver();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_setupTranslationToolSupport() {
|
||||
/**
|
||||
* This value allows for support for Google Translate (or other 3rd parties taking control
|
||||
|
|
@ -132,6 +150,7 @@ export class LocalizeManager {
|
|||
|
||||
/**
|
||||
* @param {string} locale
|
||||
* @protected
|
||||
*/
|
||||
_setHtmlLangAttribute(locale) {
|
||||
this._teardownHtmlLangAttributeObserver();
|
||||
|
|
@ -142,6 +161,7 @@ export class LocalizeManager {
|
|||
/**
|
||||
* @param {string} value
|
||||
* @throws {Error} Language only locales are not allowed(Use 'en-GB' instead of 'en')
|
||||
* @private
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__handleLanguageOnly(value) {
|
||||
|
|
@ -248,6 +268,7 @@ export class LocalizeManager {
|
|||
return formatter.format(vars);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_setupHtmlLangAttributeObserver() {
|
||||
if (!this._htmlLangAttributeObserver) {
|
||||
this._htmlLangAttributeObserver = new MutationObserver(mutations => {
|
||||
|
|
@ -273,6 +294,7 @@ export class LocalizeManager {
|
|||
});
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_teardownHtmlLangAttributeObserver() {
|
||||
if (this._htmlLangAttributeObserver) {
|
||||
this._htmlLangAttributeObserver.disconnect();
|
||||
|
|
@ -282,6 +304,7 @@ export class LocalizeManager {
|
|||
/**
|
||||
* @param {string} locale
|
||||
* @param {string} namespace
|
||||
* @protected
|
||||
*/
|
||||
_isNamespaceInCache(locale, namespace) {
|
||||
return !!(this.__storage[locale] && this.__storage[locale][namespace]);
|
||||
|
|
@ -290,6 +313,7 @@ export class LocalizeManager {
|
|||
/**
|
||||
* @param {string} locale
|
||||
* @param {string} namespace
|
||||
* @protected
|
||||
*/
|
||||
_getCachedNamespaceLoaderPromise(locale, namespace) {
|
||||
if (this.__namespaceLoaderPromisesCache[locale]) {
|
||||
|
|
@ -304,6 +328,7 @@ export class LocalizeManager {
|
|||
* @param {boolean} isDynamicImport
|
||||
* @param {string} namespace
|
||||
* @returns {Promise.<Object|void>}
|
||||
* @protected
|
||||
*/
|
||||
_loadNamespaceData(locale, namespaceObj, isDynamicImport, namespace) {
|
||||
const loader = this._getNamespaceLoader(namespaceObj, isDynamicImport, namespace);
|
||||
|
|
@ -326,6 +351,7 @@ export class LocalizeManager {
|
|||
* @param {boolean} isDynamicImport
|
||||
* @param {string} namespace
|
||||
* @throws {Error} Namespace shall setup properly. Check loader!
|
||||
* @protected
|
||||
*/
|
||||
_getNamespaceLoader(namespaceObj, isDynamicImport, namespace) {
|
||||
let loader = this.__namespaceLoadersCache[namespace];
|
||||
|
|
@ -356,6 +382,7 @@ export class LocalizeManager {
|
|||
* @param {string} [fallbackLocale]
|
||||
* @returns {Promise.<any>}
|
||||
* @throws {Error} Data for namespace and (locale or fallback locale) could not be loaded.
|
||||
* @protected
|
||||
*/
|
||||
_getNamespaceLoaderPromise(loader, locale, namespace, fallbackLocale = this._fallbackLocale) {
|
||||
return loader(locale, namespace).catch(() => {
|
||||
|
|
@ -384,6 +411,7 @@ export class LocalizeManager {
|
|||
* @param {string} locale
|
||||
* @param {string} namespace
|
||||
* @param {Promise.<Object>} promise
|
||||
* @protected
|
||||
*/
|
||||
_cacheNamespaceLoaderPromise(locale, namespace, promise) {
|
||||
if (!this.__namespaceLoaderPromisesCache[locale]) {
|
||||
|
|
@ -395,6 +423,7 @@ export class LocalizeManager {
|
|||
/**
|
||||
* @param {string} namespace
|
||||
* @returns {function|null}
|
||||
* @protected
|
||||
*/
|
||||
_lookupNamespaceLoader(namespace) {
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
|
@ -413,6 +442,7 @@ export class LocalizeManager {
|
|||
/**
|
||||
* @param {string} locale
|
||||
* @returns {string}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_getLangFromLocale(locale) {
|
||||
|
|
@ -448,6 +478,7 @@ export class LocalizeManager {
|
|||
* @param {string} newLocale
|
||||
* @param {string} oldLocale
|
||||
* @returns {undefined}
|
||||
* @protected
|
||||
*/
|
||||
_onLocaleChanged(newLocale, oldLocale) {
|
||||
if (newLocale === oldLocale) {
|
||||
|
|
@ -463,6 +494,7 @@ export class LocalizeManager {
|
|||
* @param {string} newLocale
|
||||
* @param {string} oldLocale
|
||||
* @returns {Promise.<Object>}
|
||||
* @protected
|
||||
*/
|
||||
_loadAllMissing(newLocale, oldLocale) {
|
||||
const oldLocaleNamespaces = this.__storage[oldLocale] || {};
|
||||
|
|
@ -486,6 +518,7 @@ export class LocalizeManager {
|
|||
* @param {string | string[]} keys
|
||||
* @param {string} locale
|
||||
* @returns {string | undefined}
|
||||
* @protected
|
||||
*/
|
||||
_getMessageForKeys(keys, locale) {
|
||||
if (typeof keys === 'string') {
|
||||
|
|
@ -509,6 +542,7 @@ export class LocalizeManager {
|
|||
* @param {string} locale
|
||||
* @returns {string}
|
||||
* @throws {Error} `key`is missing namespace. The format for `key` is "namespace:name"
|
||||
* @protected
|
||||
*
|
||||
*/
|
||||
_getMessageForKey(key, locale) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const LocalizeMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
/** @private */
|
||||
this.__boundLocalizeOnLocaleChanged =
|
||||
/** @param {...Object} args */
|
||||
(...args) => {
|
||||
|
|
@ -37,10 +38,12 @@ const LocalizeMixinImplementation = superclass =>
|
|||
};
|
||||
|
||||
// should be loaded in advance
|
||||
/** @private */
|
||||
this.__localizeStartLoadingNamespaces();
|
||||
|
||||
if (this.localizeNamespacesLoaded) {
|
||||
this.localizeNamespacesLoaded.then(() => {
|
||||
/** @private */
|
||||
this.__localizeMessageSync = true;
|
||||
});
|
||||
}
|
||||
|
|
@ -100,6 +103,7 @@ const LocalizeMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
* @private
|
||||
*/
|
||||
__getUniqueNamespaces() {
|
||||
/** @type {string[]} */
|
||||
|
|
@ -114,20 +118,24 @@ const LocalizeMixinImplementation = superclass =>
|
|||
return uniqueNamespaces;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__localizeStartLoadingNamespaces() {
|
||||
this.localizeNamespacesLoaded = localize.loadNamespaces(this.__getUniqueNamespaces());
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__localizeAddLocaleChangedListener() {
|
||||
localize.addEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__localizeRemoveLocaleChangedListener() {
|
||||
localize.removeEventListener('localeChanged', this.__boundLocalizeOnLocaleChanged);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CustomEvent} event
|
||||
* @private
|
||||
*/
|
||||
__localizeOnLocaleChanged(event) {
|
||||
this.onLocaleChanged(event.detail.newLocale, event.detail.oldLocale);
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import { localize } from '../src/localize.js';
|
|||
|
||||
export const localizeTearDown = () => {
|
||||
// makes sure that between tests the localization is reset to default state
|
||||
// @ts-ignore
|
||||
localize._teardownHtmlLangAttributeObserver();
|
||||
document.documentElement.lang = 'en-GB';
|
||||
// @ts-ignore
|
||||
localize._setupHtmlLangAttributeObserver();
|
||||
localize.reset();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,21 @@ import { setupFakeImport, resetFakeImport, fakeImport } from '../test-helpers/in
|
|||
|
||||
import { LocalizeManager } from '../src/LocalizeManager.js';
|
||||
|
||||
/**
|
||||
* @param {LocalizeManager} localizeManagerEl
|
||||
*/
|
||||
function getProtectedMembers(localizeManagerEl) {
|
||||
// @ts-ignore
|
||||
const {
|
||||
__storage: storage,
|
||||
_supportExternalTranslationTools: supportExternalTranslationTools,
|
||||
} = localizeManagerEl;
|
||||
return {
|
||||
storage,
|
||||
supportExternalTranslationTools,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* Useful for IE11 where LTR and RTL symbols are put by Intl when rendering dates
|
||||
|
|
@ -84,10 +99,11 @@ describe('LocalizeManager', () => {
|
|||
describe('addData()', () => {
|
||||
it('allows to provide inline data', () => {
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'lion-hello': { greeting: 'Hi!' },
|
||||
},
|
||||
|
|
@ -95,7 +111,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
manager.addData('en-GB', 'lion-goodbye', { farewell: 'Cheers!' });
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'lion-hello': { greeting: 'Hi!' },
|
||||
'lion-goodbye': { farewell: 'Cheers!' },
|
||||
|
|
@ -105,7 +121,7 @@ describe('LocalizeManager', () => {
|
|||
manager.addData('nl-NL', 'lion-hello', { greeting: 'Hoi!' });
|
||||
manager.addData('nl-NL', 'lion-goodbye', { farewell: 'Doei!' });
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'lion-hello': { greeting: 'Hi!' },
|
||||
'lion-goodbye': { farewell: 'Cheers!' },
|
||||
|
|
@ -119,6 +135,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
it('prevents mutating existing data for the same locale & namespace', () => {
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.addData('en-GB', 'lion-hello', { greeting: 'Hi!' });
|
||||
|
||||
|
|
@ -126,7 +143,7 @@ describe('LocalizeManager', () => {
|
|||
manager.addData('en-GB', 'lion-hello', { greeting: 'Hello!' });
|
||||
}).to.throw();
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'lion-hello': { greeting: 'Hi!' } },
|
||||
});
|
||||
});
|
||||
|
|
@ -137,13 +154,14 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -154,6 +172,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
manager.locale = 'en-US';
|
||||
|
||||
await manager.loadNamespace(
|
||||
|
|
@ -164,7 +183,7 @@ describe('LocalizeManager', () => {
|
|||
{ locale: 'nl-NL' },
|
||||
);
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'nl-NL': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -176,6 +195,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-send-button/en-GB.js', { default: { submit: 'Send' } });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
await manager.loadNamespaces([
|
||||
{
|
||||
|
|
@ -188,7 +208,7 @@ describe('LocalizeManager', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-defaults': { submit: 'Submit' },
|
||||
'my-send-button': { submit: 'Send' },
|
||||
|
|
@ -201,6 +221,7 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-send-button/nl-NL.js', { default: { submit: 'Send' } });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
manager.locale = 'en-US';
|
||||
|
||||
await manager.loadNamespaces(
|
||||
|
|
@ -217,7 +238,7 @@ describe('LocalizeManager', () => {
|
|||
{ locale: 'nl-NL' },
|
||||
);
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'nl-NL': {
|
||||
'my-defaults': { submit: 'Submit' },
|
||||
'my-send-button': { submit: 'Send' },
|
||||
|
|
@ -229,13 +250,14 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -265,6 +287,7 @@ describe('LocalizeManager', () => {
|
|||
describe('fallback locale', () => {
|
||||
it('can load a fallback locale if current one can not be loaded', async () => {
|
||||
manager = new LocalizeManager({ fallbackLocale: 'en-GB' });
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
manager.locale = 'nl-NL';
|
||||
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
|
|
@ -274,7 +297,7 @@ describe('LocalizeManager', () => {
|
|||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'nl-NL': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -283,6 +306,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
it('can load fallback generic language file if fallback locale file is not found', async () => {
|
||||
manager = new LocalizeManager({ fallbackLocale: 'en-GB' });
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
manager.locale = 'nl-NL';
|
||||
|
||||
setupFakeImport('./my-component/en.js', { default: { greeting: 'Hello!' } });
|
||||
|
|
@ -292,7 +316,7 @@ describe('LocalizeManager', () => {
|
|||
'my-component': locale => fakeImport(`./my-component/${locale}.js`),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'nl-NL': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -345,6 +369,7 @@ describe('LocalizeManager', () => {
|
|||
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.setupNamespaceLoader(
|
||||
'my-component',
|
||||
|
|
@ -357,7 +382,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
await manager.loadNamespace('my-component');
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -373,6 +398,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.setupNamespaceLoader(
|
||||
'my-defaults',
|
||||
|
|
@ -394,7 +420,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
await manager.loadNamespaces(['my-defaults', 'my-send-button']);
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-send-button': {
|
||||
submit: 'Send',
|
||||
|
|
@ -410,6 +436,7 @@ describe('LocalizeManager', () => {
|
|||
fetchMock.get('./my-component/en-GB.json', { greeting: 'Hello!' });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.setupNamespaceLoader(
|
||||
/my-.+/,
|
||||
|
|
@ -425,7 +452,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
await manager.loadNamespace('my-component');
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-component': { greeting: 'Hello!' },
|
||||
},
|
||||
|
|
@ -437,6 +464,7 @@ describe('LocalizeManager', () => {
|
|||
fetchMock.get('./my-send-button/en-GB.json', { submit: 'Send' });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.setupNamespaceLoader(
|
||||
/my-.+/,
|
||||
|
|
@ -452,7 +480,7 @@ describe('LocalizeManager', () => {
|
|||
|
||||
await manager.loadNamespaces(['my-defaults', 'my-send-button']);
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': {
|
||||
'my-defaults': { submit: 'Submit' },
|
||||
'my-send-button': { submit: 'Send' },
|
||||
|
|
@ -467,20 +495,21 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/nl-NL.js', { default: { greeting: 'Hallo!' } });
|
||||
|
||||
manager = new LocalizeManager({ autoLoadOnLocaleChange: true });
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
|
||||
manager.locale = 'nl-NL';
|
||||
await manager.loadingComplete;
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
'nl-NL': { 'my-component': { greeting: 'Hallo!' } },
|
||||
});
|
||||
|
|
@ -491,14 +520,15 @@ describe('LocalizeManager', () => {
|
|||
it('has a Promise "loadingComplete" that resolved once all pending loading is done', async () => {
|
||||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Hello!' } });
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
expect(manager.__storage).to.deep.equal({});
|
||||
expect(storage).to.deep.equal({});
|
||||
await manager.loadingComplete;
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
});
|
||||
|
|
@ -526,11 +556,12 @@ describe('LocalizeManager', () => {
|
|||
setupFakeImport('./my-component/en-GB.js', { default: { greeting: 'Loaded hello!' } });
|
||||
|
||||
manager = new LocalizeManager();
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
manager.addData('en-GB', 'my-component', { greeting: 'Hello!' });
|
||||
|
||||
await manager.loadNamespace('my-component');
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
|
||||
|
|
@ -544,7 +575,7 @@ describe('LocalizeManager', () => {
|
|||
});
|
||||
|
||||
expect(called).to.equal(0);
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
});
|
||||
|
|
@ -684,11 +715,15 @@ describe('When supporting external translation tools like Google Translate', ()
|
|||
it('triggers support for external translation tools via data-localize-lang', async () => {
|
||||
document.documentElement.removeAttribute('data-localize-lang');
|
||||
manager = getInstance();
|
||||
expect(manager._supportExternalTranslationTools).to.be.false;
|
||||
const { supportExternalTranslationTools: first } = getProtectedMembers(manager);
|
||||
|
||||
expect(first).to.be.false;
|
||||
|
||||
document.documentElement.setAttribute('data-localize-lang', 'nl-NL');
|
||||
manager = getInstance();
|
||||
expect(manager._supportExternalTranslationTools).to.be.true;
|
||||
const { supportExternalTranslationTools: second } = getProtectedMembers(manager);
|
||||
|
||||
expect(second).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -776,13 +811,14 @@ describe('[deprecated] When not supporting external translation tools like Googl
|
|||
manager = new LocalizeManager({
|
||||
autoLoadOnLocaleChange: true,
|
||||
});
|
||||
const { storage } = getProtectedMembers(manager);
|
||||
|
||||
await manager.loadNamespace({
|
||||
/** @param {string} locale */
|
||||
'my-component': locale => fakeImport(`./my-component/${locale}.js`, 25),
|
||||
});
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
});
|
||||
|
||||
|
|
@ -790,7 +826,7 @@ describe('[deprecated] When not supporting external translation tools like Googl
|
|||
await aTimeout(0); // wait for mutation observer to be called
|
||||
await manager.loadingComplete;
|
||||
|
||||
expect(manager.__storage).to.deep.equal({
|
||||
expect(storage).to.deep.equal({
|
||||
'en-GB': { 'my-component': { greeting: 'Hello!' } },
|
||||
'nl-NL': { 'my-component': { greeting: 'Hallo!' } },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,6 +4,21 @@ import { LocalizeManager } from '../src/LocalizeManager.js';
|
|||
|
||||
import { localize, setLocalize } from '../src/localize.js';
|
||||
|
||||
/**
|
||||
* @param {LocalizeManager} localizeManagerEl
|
||||
*/
|
||||
function getProtectedMembers(localizeManagerEl) {
|
||||
// @ts-ignore
|
||||
const {
|
||||
_autoLoadOnLocaleChange: autoLoadOnLocaleChange,
|
||||
_fallbackLocale: fallbackLocale,
|
||||
} = localizeManagerEl;
|
||||
return {
|
||||
autoLoadOnLocaleChange,
|
||||
fallbackLocale,
|
||||
};
|
||||
}
|
||||
|
||||
describe('localize', () => {
|
||||
// this is an important mindset:
|
||||
// we don't test the singleton
|
||||
|
|
@ -32,10 +47,12 @@ describe('localize', () => {
|
|||
});
|
||||
|
||||
it('is configured to automatically load namespaces if locale is changed', () => {
|
||||
expect(localize._autoLoadOnLocaleChange).to.equal(true);
|
||||
const { autoLoadOnLocaleChange } = getProtectedMembers(localize);
|
||||
expect(autoLoadOnLocaleChange).to.equal(true);
|
||||
});
|
||||
|
||||
it('is configured to fallback to the locale "en-GB"', () => {
|
||||
expect(localize._fallbackLocale).to.equal('en-GB');
|
||||
const { fallbackLocale } = getProtectedMembers(localize);
|
||||
expect(fallbackLocale).to.equal('en-GB');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export const ArrowMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
super();
|
||||
this.hasArrow = true;
|
||||
/** @private */
|
||||
this.__setupRepositionCompletePromise();
|
||||
}
|
||||
|
||||
|
|
@ -105,10 +106,12 @@ export const ArrowMixinImplementation = superclass =>
|
|||
`;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_arrowNodeTemplate() {
|
||||
return html` <div class="arrow" data-popper-arrow>${this._arrowTemplate()}</div> `;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_arrowTemplate() {
|
||||
return html`
|
||||
|
|
@ -123,6 +126,7 @@ export const ArrowMixinImplementation = superclass =>
|
|||
* and adds onCreate and onUpdate hooks to sync from popper state
|
||||
* @configure OverlayMixin
|
||||
* @returns {OverlayConfig}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
_defineOverlayConfig() {
|
||||
|
|
@ -143,6 +147,7 @@ export const ArrowMixinImplementation = superclass =>
|
|||
/**
|
||||
* @param {Partial<PopperOptions>} popperConfigToExtendFrom
|
||||
* @returns {Partial<PopperOptions>}
|
||||
* @protected
|
||||
*/
|
||||
_getPopperArrowConfig(popperConfigToExtendFrom) {
|
||||
/** @type {Partial<PopperOptions> & { afterWrite: (arg0: Partial<import('@popperjs/core/lib/popper').State>) => void }} */
|
||||
|
|
@ -177,6 +182,7 @@ export const ArrowMixinImplementation = superclass =>
|
|||
return popperCfg;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupRepositionCompletePromise() {
|
||||
this.repositionComplete = new Promise(resolve => {
|
||||
this.__repositionCompleteResolver = resolve;
|
||||
|
|
@ -189,6 +195,7 @@ export const ArrowMixinImplementation = superclass =>
|
|||
|
||||
/**
|
||||
* @param {Partial<import('@popperjs/core/lib/popper').State>} data
|
||||
* @private
|
||||
*/
|
||||
__syncFromPopperState(data) {
|
||||
if (!data) {
|
||||
|
|
|
|||
|
|
@ -94,9 +94,13 @@ export class OverlayController extends EventTargetShim {
|
|||
constructor(config = {}, manager = overlays) {
|
||||
super();
|
||||
this.manager = manager;
|
||||
/** @private */
|
||||
this.__sharedConfig = config;
|
||||
|
||||
/** @type {OverlayConfig} */
|
||||
/**
|
||||
* @type {OverlayConfig}
|
||||
* @protected
|
||||
*/
|
||||
this._defaultConfig = {
|
||||
placementMode: undefined,
|
||||
contentNode: config.contentNode,
|
||||
|
|
@ -155,7 +159,9 @@ export class OverlayController extends EventTargetShim {
|
|||
};
|
||||
|
||||
this.manager.add(this);
|
||||
/** @protected */
|
||||
this._contentId = `overlay-content--${Math.random().toString(36).substr(2, 10)}`;
|
||||
/** @private */
|
||||
this.__originalAttrs = new Map();
|
||||
if (this._defaultConfig.contentNode) {
|
||||
if (!this._defaultConfig.contentNode.isConnected) {
|
||||
|
|
@ -166,11 +172,17 @@ export class OverlayController extends EventTargetShim {
|
|||
this.__isContentNodeProjected = Boolean(this._defaultConfig.contentNode.assignedSlot);
|
||||
}
|
||||
this.updateConfig(config);
|
||||
/** @private */
|
||||
this.__hasActiveTrapsKeyboardFocus = false;
|
||||
/** @private */
|
||||
this.__hasActiveBackdrop = true;
|
||||
/** @type {HTMLElement | undefined} */
|
||||
/**
|
||||
* @type {HTMLElement | undefined}
|
||||
* @private
|
||||
*/
|
||||
this.__backdropNodeToBeTornDown = undefined;
|
||||
|
||||
/** @private */
|
||||
this.__escKeyHandler = this.__escKeyHandler.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -377,6 +389,7 @@ export class OverlayController extends EventTargetShim {
|
|||
* we need to know where we should reappend contentWrapperNode (or contentNode in case it's
|
||||
* projected).
|
||||
* @type {HTMLElement}
|
||||
* @protected
|
||||
*/
|
||||
get _renderTarget() {
|
||||
/** config [g1] */
|
||||
|
|
@ -395,6 +408,7 @@ export class OverlayController extends EventTargetShim {
|
|||
/**
|
||||
* @desc The element our local overlay will be positioned relative to.
|
||||
* @type {HTMLElement | undefined}
|
||||
* @protected
|
||||
*/
|
||||
get _referenceNode() {
|
||||
return this.referenceNode || this.invokerNode;
|
||||
|
|
@ -430,7 +444,10 @@ export class OverlayController extends EventTargetShim {
|
|||
// Teardown all previous configs
|
||||
this.teardown();
|
||||
|
||||
/** @type {OverlayConfig} */
|
||||
/**
|
||||
* @type {OverlayConfig}
|
||||
* @private
|
||||
*/
|
||||
this.__prevConfig = this.config || {};
|
||||
|
||||
/** @type {OverlayConfig} */
|
||||
|
|
@ -452,15 +469,19 @@ export class OverlayController extends EventTargetShim {
|
|||
},
|
||||
};
|
||||
|
||||
/** @private */
|
||||
this.__validateConfiguration(/** @type {OverlayConfig} */ (this.config));
|
||||
// TODO: remove this, so we only have the getters (no setters)
|
||||
// Object.assign(this, this.config);
|
||||
/** @protected */
|
||||
this._init({ cfgToAdd });
|
||||
/** @private */
|
||||
this.__elementToFocusAfterHide = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {OverlayConfig} newConfig
|
||||
* @private
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__validateConfiguration(newConfig) {
|
||||
|
|
@ -499,6 +520,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ cfgToAdd: OverlayConfig }} options
|
||||
* @protected
|
||||
*/
|
||||
_init({ cfgToAdd }) {
|
||||
this.__initContentWrapperNode({ cfgToAdd });
|
||||
|
|
@ -514,6 +536,7 @@ export class OverlayController extends EventTargetShim {
|
|||
this._handleFeatures({ phase: 'init' });
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__initConnectionTarget() {
|
||||
// Now, add our node to the right place in dom (renderTarget)
|
||||
if (this.contentWrapperNode !== this.__prevConfig?.contentWrapperNode) {
|
||||
|
|
@ -544,6 +567,7 @@ export class OverlayController extends EventTargetShim {
|
|||
* Cleanup ._contentWrapperNode. We do this, because creating a fresh wrapper
|
||||
* can lead to problems with event listeners...
|
||||
* @param {{ cfgToAdd: OverlayConfig }} options
|
||||
* @private
|
||||
*/
|
||||
__initContentWrapperNode({ cfgToAdd }) {
|
||||
if (this.config?.contentWrapperNode && this.placementMode === 'local') {
|
||||
|
|
@ -578,6 +602,7 @@ export class OverlayController extends EventTargetShim {
|
|||
/**
|
||||
* Display local overlays on top of elements with no z-index that appear later in the DOM
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleZIndex({ phase }) {
|
||||
if (this.placementMode !== 'local') {
|
||||
|
|
@ -594,6 +619,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @private
|
||||
*/
|
||||
__setupTeardownAccessibility({ phase }) {
|
||||
if (phase === 'init') {
|
||||
|
|
@ -634,6 +660,7 @@ export class OverlayController extends EventTargetShim {
|
|||
/**
|
||||
* @param {HTMLElement} node
|
||||
* @param {string[]} attrs
|
||||
* @private
|
||||
*/
|
||||
__storeOriginalAttrs(node, attrs) {
|
||||
const attrMap = {};
|
||||
|
|
@ -643,6 +670,7 @@ export class OverlayController extends EventTargetShim {
|
|||
this.__originalAttrs.set(node, attrMap);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__restoreOriginalAttrs() {
|
||||
for (const [node, attrMap] of this.__originalAttrs) {
|
||||
Object.entries(attrMap).forEach(([attrName, value]) => {
|
||||
|
|
@ -706,6 +734,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
async _handlePosition({ phase }) {
|
||||
if (this.placementMode === 'global') {
|
||||
|
|
@ -729,6 +758,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_keepBodySize({ phase }) {
|
||||
switch (phase) {
|
||||
|
|
@ -817,6 +847,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{backdropNode:HTMLElement, contentNode:HTMLElement}} hideConfig
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this, no-empty-function, no-unused-vars
|
||||
async _transitionHide(hideConfig) {
|
||||
|
|
@ -874,6 +905,7 @@ export class OverlayController extends EventTargetShim {
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_restoreFocus() {
|
||||
// We only are allowed to move focus if we (still) 'own' it.
|
||||
// Otherwise we assume the 'outside world' has, purposefully, taken over
|
||||
|
|
@ -894,6 +926,7 @@ export class OverlayController extends EventTargetShim {
|
|||
/**
|
||||
* All features are handled here.
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleFeatures({ phase }) {
|
||||
this._handleZIndex({ phase });
|
||||
|
|
@ -929,6 +962,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handlePreventsScroll({ phase }) {
|
||||
switch (phase) {
|
||||
|
|
@ -944,6 +978,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleBlocking({ phase }) {
|
||||
switch (phase) {
|
||||
|
|
@ -966,6 +1001,7 @@ export class OverlayController extends EventTargetShim {
|
|||
* it is removed. Otherwise this is the first time displaying a backdrop, so a animation-in
|
||||
* animation is played.
|
||||
* @param {{ animation?: boolean, phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleBackdrop({ phase }) {
|
||||
switch (phase) {
|
||||
|
|
@ -1026,6 +1062,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleTrapsKeyboardFocus({ phase }) {
|
||||
if (phase === 'show') {
|
||||
|
|
@ -1063,12 +1100,14 @@ export class OverlayController extends EventTargetShim {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__escKeyHandler(/** @type {KeyboardEvent} */ ev) {
|
||||
return ev.key === 'Escape' && this.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleHidesOnEsc({ phase }) {
|
||||
if (phase === 'show') {
|
||||
|
|
@ -1086,6 +1125,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleHidesOnOutsideEsc({ phase }) {
|
||||
if (phase === 'show') {
|
||||
|
|
@ -1097,6 +1137,7 @@ export class OverlayController extends EventTargetShim {
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_handleInheritsReferenceWidth() {
|
||||
if (!this._referenceNode || this.placementMode === 'global') {
|
||||
return;
|
||||
|
|
@ -1119,6 +1160,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleHidesOnOutsideClick({ phase }) {
|
||||
const addOrRemoveListener = phase === 'show' ? 'addEventListener' : 'removeEventListener';
|
||||
|
|
@ -1210,6 +1252,7 @@ export class OverlayController extends EventTargetShim {
|
|||
|
||||
/**
|
||||
* @param {{ phase: OverlayPhase }} config
|
||||
* @protected
|
||||
*/
|
||||
_handleAccessibility({ phase }) {
|
||||
if (phase === 'init' || phase === 'teardown') {
|
||||
|
|
@ -1231,6 +1274,7 @@ export class OverlayController extends EventTargetShim {
|
|||
this._teardownContentWrapperNode();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_teardownContentWrapperNode() {
|
||||
if (
|
||||
this.placementMode === 'global' &&
|
||||
|
|
@ -1241,6 +1285,7 @@ export class OverlayController extends EventTargetShim {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
async __createPopperInstance() {
|
||||
if (this._popper) {
|
||||
this._popper.destroy();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
constructor() {
|
||||
super();
|
||||
this.opened = false;
|
||||
/** @private */
|
||||
this.__needsSetup = true;
|
||||
/** @type {OverlayConfig} */
|
||||
this.config = {};
|
||||
|
|
@ -73,6 +74,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
* In case overriding _defineOverlayConfig is not enough
|
||||
* @param {DefineOverlayConfig} config
|
||||
* @returns {OverlayController}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
_defineOverlay({ contentNode, invokerNode, referenceNode, backdropNode, contentWrapperNode }) {
|
||||
|
|
@ -102,6 +104,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
* @desc returns an object with default configuration options for your overlay component.
|
||||
* This is generally speaking easier to override than _defineOverlay method entirely.
|
||||
* @returns {OverlayConfig}
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
_defineOverlayConfig() {
|
||||
|
|
@ -125,6 +128,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
* @overridable
|
||||
* @desc use this method to setup your open and close event listeners
|
||||
* For example, set a click event listener on _overlayInvokerNode to set opened to true
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_setupOpenCloseListeners() {
|
||||
|
|
@ -146,6 +150,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
/**
|
||||
* @overridable
|
||||
* @desc use this method to tear down your event listeners
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_teardownOpenCloseListeners() {
|
||||
|
|
@ -207,6 +212,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
return this.shadowRoot.querySelector('#overlay-content-node-wrapper');
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_setupOverlayCtrl() {
|
||||
/** @type {OverlayController} */
|
||||
this._overlayCtrl = this._defineOverlay({
|
||||
|
|
@ -221,6 +227,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
this._setupOpenCloseListeners();
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_teardownOverlayCtrl() {
|
||||
this._teardownOpenCloseListeners();
|
||||
this.__teardownSyncFromOverlayController();
|
||||
|
|
@ -234,6 +241,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
* (intercepted in before-hide for instance), so that we need to sync the controller state
|
||||
* to this webcomponent again, preventing eternal loops.
|
||||
* @param {boolean} newOpened
|
||||
* @protected
|
||||
*/
|
||||
async _setOpenedWithoutPropertyEffects(newOpened) {
|
||||
this.__blockSyncToOverlayCtrl = true;
|
||||
|
|
@ -242,6 +250,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
this.__blockSyncToOverlayCtrl = false;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupSyncFromOverlayController() {
|
||||
this.__onOverlayCtrlShow = () => {
|
||||
this.opened = true;
|
||||
|
|
@ -292,6 +301,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
(this._overlayCtrl).addEventListener('before-hide', this.__onBeforeHide);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__teardownSyncFromOverlayController() {
|
||||
/** @type {OverlayController} */
|
||||
(this._overlayCtrl).removeEventListener(
|
||||
|
|
@ -312,6 +322,7 @@ export const OverlayMixinImplementation = superclass =>
|
|||
);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__syncToOverlayController() {
|
||||
if (this.opened) {
|
||||
/** @type {OverlayController} */
|
||||
|
|
|
|||
|
|
@ -56,12 +56,22 @@ export class OverlaysManager {
|
|||
}
|
||||
|
||||
constructor() {
|
||||
/** @type {OverlayController[]} */
|
||||
/**
|
||||
* @type {OverlayController[]}
|
||||
* @private
|
||||
*/
|
||||
this.__list = [];
|
||||
/** @type {OverlayController[]} */
|
||||
/**
|
||||
* @type {OverlayController[]}
|
||||
* @private
|
||||
*/
|
||||
this.__shownList = [];
|
||||
/** @private */
|
||||
this.__siblingsInert = false;
|
||||
/** @type {WeakMap<OverlayController, OverlayController[]>} */
|
||||
/**
|
||||
* @type {WeakMap<OverlayController, OverlayController[]>}
|
||||
* @private
|
||||
*/
|
||||
this.__blockingMap = new WeakMap();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,18 @@ import { mimicClick } from '../test-helpers.js';
|
|||
* @typedef {import('../types/OverlayConfig').ViewportPlacement} ViewportPlacement
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {OverlayController} overlayControllerEl
|
||||
*/
|
||||
function getProtectedMembers(overlayControllerEl) {
|
||||
// @ts-ignore
|
||||
const { _contentId: contentId, _renderTarget: renderTarget } = overlayControllerEl;
|
||||
return {
|
||||
contentId,
|
||||
renderTarget,
|
||||
};
|
||||
}
|
||||
|
||||
const withGlobalTestConfig = () =>
|
||||
/** @type {OverlayConfig} */ ({
|
||||
placementMode: 'global',
|
||||
|
|
@ -133,7 +145,8 @@ describe('OverlayController', () => {
|
|||
const ctrl = new OverlayController({
|
||||
...withGlobalTestConfig(),
|
||||
});
|
||||
expect(ctrl._renderTarget).to.equal(overlays.globalRootNode);
|
||||
const { renderTarget } = getProtectedMembers(ctrl);
|
||||
expect(renderTarget).to.equal(overlays.globalRootNode);
|
||||
});
|
||||
|
||||
it.skip('creates local target next to sibling for placement mode "local"', async () => {
|
||||
|
|
@ -141,7 +154,8 @@ describe('OverlayController', () => {
|
|||
...withLocalTestConfig(),
|
||||
invokerNode: /** @type {HTMLElement} */ (await fixture(html`<button>Invoker</button>`)),
|
||||
});
|
||||
expect(ctrl._renderTarget).to.be.undefined;
|
||||
const { renderTarget } = getProtectedMembers(ctrl);
|
||||
expect(renderTarget).to.be.undefined;
|
||||
expect(ctrl.content).to.equal(ctrl.invokerNode?.nextElementSibling);
|
||||
});
|
||||
|
||||
|
|
@ -156,7 +170,8 @@ describe('OverlayController', () => {
|
|||
...withLocalTestConfig(),
|
||||
contentNode,
|
||||
});
|
||||
expect(ctrl._renderTarget).to.equal(parentNode);
|
||||
const { renderTarget } = getProtectedMembers(ctrl);
|
||||
expect(renderTarget).to.equal(parentNode);
|
||||
});
|
||||
|
||||
it('throws when passing a content node that was created "offline"', async () => {
|
||||
|
|
@ -179,8 +194,10 @@ describe('OverlayController', () => {
|
|||
...withLocalTestConfig(),
|
||||
contentNode,
|
||||
});
|
||||
const { renderTarget } = getProtectedMembers(overlay);
|
||||
|
||||
expect(overlay.contentNode.isConnected).to.be.true;
|
||||
expect(overlay._renderTarget).to.not.be.undefined;
|
||||
expect(renderTarget).to.not.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1265,7 +1282,8 @@ describe('OverlayController', () => {
|
|||
...withLocalTestConfig(),
|
||||
handlesAccessibility: true,
|
||||
});
|
||||
expect(ctrl.contentNode.id).to.contain(ctrl._contentId);
|
||||
const { contentId } = getProtectedMembers(ctrl);
|
||||
expect(ctrl.contentNode.id).to.contain(contentId);
|
||||
});
|
||||
|
||||
it('preserves content id when present', async () => {
|
||||
|
|
@ -1418,7 +1436,9 @@ describe('OverlayController', () => {
|
|||
isTooltip: true,
|
||||
invokerNode,
|
||||
});
|
||||
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(ctrl._contentId);
|
||||
const { contentId } = getProtectedMembers(ctrl);
|
||||
|
||||
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(contentId);
|
||||
});
|
||||
|
||||
it('adds [aria-labelledby] on invoker when invokerRelation is label', async () => {
|
||||
|
|
@ -1432,8 +1452,10 @@ describe('OverlayController', () => {
|
|||
invokerRelation: 'label',
|
||||
invokerNode,
|
||||
});
|
||||
const { contentId } = getProtectedMembers(ctrl);
|
||||
|
||||
expect(ctrl.invokerNode?.getAttribute('aria-describedby')).to.equal(null);
|
||||
expect(ctrl.invokerNode?.getAttribute('aria-labelledby')).to.equal(ctrl._contentId);
|
||||
expect(ctrl.invokerNode?.getAttribute('aria-labelledby')).to.equal(contentId);
|
||||
});
|
||||
|
||||
it('adds [role=tooltip] on content', async () => {
|
||||
|
|
|
|||
12
packages/overlays/types/ArrowMixinTypes.d.ts
vendored
12
packages/overlays/types/ArrowMixinTypes.d.ts
vendored
|
|
@ -18,13 +18,13 @@ export declare class ArrowHost {
|
|||
static get styles(): CSSResultArray;
|
||||
|
||||
render(): TemplateResult;
|
||||
_arrowTemplate(): TemplateResult;
|
||||
_arrowNodeTemplate(): TemplateResult;
|
||||
_defineOverlayConfig(): OverlayConfig;
|
||||
_getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
|
||||
__setupRepositionCompletePromise(): void;
|
||||
protected _arrowTemplate(): TemplateResult;
|
||||
protected _arrowNodeTemplate(): TemplateResult;
|
||||
protected _defineOverlayConfig(): OverlayConfig;
|
||||
protected _getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
|
||||
private __setupRepositionCompletePromise(): void;
|
||||
get _arrowNode(): Element | null;
|
||||
__syncFromPopperState(data: Partial<State>): void;
|
||||
private __syncFromPopperState(data: Partial<State>): void;
|
||||
}
|
||||
|
||||
export declare function ArrowImplementation<T extends Constructor<LitElement>>(
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export class LionPagination extends LocalizeMixin(LitElement) {
|
|||
|
||||
constructor() {
|
||||
super();
|
||||
/** @private */
|
||||
this.__visiblePages = 5;
|
||||
this.current = 1;
|
||||
this.count = 0;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export class LionProgressIndicator extends LocalizeMixin(LitElement) {
|
|||
];
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_graphicTemplate() {
|
||||
return nothing;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export class LionSelectInvoker extends LionButton {
|
|||
this.type = 'button';
|
||||
}
|
||||
|
||||
/** @private */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__handleKeydown(/** @type {KeyboardEvent} */ event) {
|
||||
switch (event.key) {
|
||||
|
|
@ -104,6 +105,7 @@ export class LionSelectInvoker extends LionButton {
|
|||
this.removeEventListener('keydown', this.__handleKeydown);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_contentTemplate() {
|
||||
if (this.selectedElement) {
|
||||
const labelNodes = Array.from(this.selectedElement.childNodes);
|
||||
|
|
@ -117,16 +119,19 @@ export class LionSelectInvoker extends LionButton {
|
|||
|
||||
/**
|
||||
* To be overriden for a placeholder, used when `hasNoDefaultSelected` is true on the select rich
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_noSelectionTemplate() {
|
||||
return html``;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_beforeTemplate() {
|
||||
return html` <div id="content-wrapper">${this._contentTemplate()}</div> `;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_afterTemplate() {
|
||||
return html`${!this.singleOption ? html`<slot name="after"></slot>` : ''}`;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @enhance FormControlMixin
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_inputGroupInputTemplate() {
|
||||
|
|
@ -118,14 +119,22 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
this.interactionMode = 'auto';
|
||||
|
||||
this.singleOption = false;
|
||||
/** @protected */
|
||||
this._arrowWidth = 28;
|
||||
|
||||
/** @private */
|
||||
this.__onKeyUp = this.__onKeyUp.bind(this);
|
||||
/** @private */
|
||||
this.__invokerOnBlur = this.__invokerOnBlur.bind(this);
|
||||
/** @private */
|
||||
this.__overlayOnHide = this.__overlayOnHide.bind(this);
|
||||
/** @private */
|
||||
this.__overlayOnShow = this.__overlayOnShow.bind(this);
|
||||
/** @private */
|
||||
this.__invokerOnClick = this.__invokerOnClick.bind(this);
|
||||
/** @private */
|
||||
this.__overlayBeforeShow = this.__overlayBeforeShow.bind(this);
|
||||
/** @protected */
|
||||
this._listboxOnClick = this._listboxOnClick.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -248,20 +257,24 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
* In the select disabled options are still going to a possible value for example
|
||||
* when prefilling or programmatically setting it.
|
||||
* @override ChoiceGroupMixin
|
||||
* @protected
|
||||
*/
|
||||
_getCheckedElements() {
|
||||
return this.formElements.filter(el => el.checked);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_onFormElementsChanged() {
|
||||
this.singleOption = this.formElements.length === 1;
|
||||
this._invokerNode.singleOption = this.singleOption;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__initInteractionStates() {
|
||||
this.initInteractionState();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__toggleInvokerDisabled() {
|
||||
if (this._invokerNode) {
|
||||
this._invokerNode.disabled = this.disabled;
|
||||
|
|
@ -269,6 +282,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__syncInvokerElement() {
|
||||
// sync to invoker
|
||||
if (this._invokerNode) {
|
||||
|
|
@ -285,6 +299,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupInvokerNode() {
|
||||
this._invokerNode.id = `invoker-${this._inputId}`;
|
||||
this._invokerNode.setAttribute('aria-haspopup', 'listbox');
|
||||
|
|
@ -292,22 +307,26 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
this.__setupInvokerNodeEventListener();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__invokerOnClick() {
|
||||
if (!this.disabled && !this.readOnly && !this.singleOption && !this.__blockListShow) {
|
||||
this._overlayCtrl.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__invokerOnBlur() {
|
||||
this.dispatchEvent(new Event('blur'));
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupInvokerNodeEventListener() {
|
||||
this._invokerNode.addEventListener('click', this.__invokerOnClick);
|
||||
|
||||
this._invokerNode.addEventListener('blur', this.__invokerOnBlur);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__teardownInvokerNode() {
|
||||
this._invokerNode.removeEventListener('click', this.__invokerOnClick);
|
||||
this._invokerNode.removeEventListener('blur', this.__invokerOnBlur);
|
||||
|
|
@ -315,6 +334,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_defineOverlayConfig() {
|
||||
|
|
@ -328,6 +348,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
* By default, we will set it to 'min', and then set it back to what it was initially when
|
||||
* something is selected.
|
||||
* As a subclasser you can override this behavior.
|
||||
* @protected
|
||||
*/
|
||||
_noDefaultSelectedInheritsWidth() {
|
||||
if (this.checkedIndex === -1) {
|
||||
|
|
@ -339,12 +360,14 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__overlayBeforeShow() {
|
||||
if (this.hasNoDefaultSelected) {
|
||||
this._noDefaultSelectedInheritsWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__overlayOnShow() {
|
||||
if (this.checkedIndex != null) {
|
||||
this.activeIndex = /** @type {number} */ (this.checkedIndex);
|
||||
|
|
@ -352,12 +375,14 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
this._listboxNode.focus();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__overlayOnHide() {
|
||||
this._invokerNode.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @enhance OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
_setupOverlayCtrl() {
|
||||
super._setupOverlayCtrl();
|
||||
|
|
@ -372,6 +397,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @enhance OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
_teardownOverlayCtrl() {
|
||||
super._teardownOverlayCtrl();
|
||||
|
|
@ -383,6 +409,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
/**
|
||||
* Align invoker width with content width
|
||||
* Make sure display is not set to "none" while calculating the content width
|
||||
* @protected
|
||||
*/
|
||||
async _alignInvokerWidth() {
|
||||
if (this._overlayCtrl && this._overlayCtrl.content) {
|
||||
|
|
@ -410,6 +437,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @configure FormControlMixin
|
||||
* @protected
|
||||
*/
|
||||
_onLabelClick() {
|
||||
this._invokerNode.focus();
|
||||
|
|
@ -417,6 +445,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
get _overlayInvokerNode() {
|
||||
return this._invokerNode;
|
||||
|
|
@ -424,6 +453,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @configure OverlayMixin
|
||||
* @protected
|
||||
*/
|
||||
get _overlayContentNode() {
|
||||
return this._listboxNode;
|
||||
|
|
@ -431,6 +461,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} ev
|
||||
* @private
|
||||
*/
|
||||
__onKeyUp(ev) {
|
||||
if (this.disabled) {
|
||||
|
|
@ -474,6 +505,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
* an item.
|
||||
*
|
||||
* @param {KeyboardEvent} ev - the keydown event object
|
||||
* @protected
|
||||
*/
|
||||
_listboxOnKeyDown(ev) {
|
||||
super._listboxOnKeyDown(ev);
|
||||
|
|
@ -502,15 +534,18 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_listboxOnClick() {
|
||||
this.opened = false;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_setupListboxNode() {
|
||||
super._setupListboxNode();
|
||||
this._listboxNode.addEventListener('click', this._listboxOnClick);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_teardownListboxNode() {
|
||||
super._teardownListboxNode();
|
||||
if (this._listboxNode) {
|
||||
|
|
@ -521,6 +556,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
|
|||
/**
|
||||
* Normally, when textbox gets focus or a char is typed, it opens listbox.
|
||||
* In transition phases (like clicking option) we prevent this.
|
||||
* @private
|
||||
*/
|
||||
__blockListShowDuringTransition() {
|
||||
this.__blockListShow = true;
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export class LionSelect extends LionFieldWithSelect {
|
|||
this._inputNode.removeEventListener('change', this._proxyChangeEvent);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_proxyChangeEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('user-input-changed', {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const sym = Symbol.for('lion::SingletonManagerClassStorage');
|
|||
|
||||
export class SingletonManagerClass {
|
||||
constructor() {
|
||||
/** protected */
|
||||
this._map = window[sym] ? window[sym] : (window[sym] = new Map());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,12 +30,14 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
* Therefore we do a full override and typecast to an intersection type that includes LionSwitchButton
|
||||
* @returns {LionSwitchButton}
|
||||
*/
|
||||
// @ts-ignore
|
||||
get _inputNode() {
|
||||
return /** @type {LionSwitchButton} */ (Array.from(this.children).find(
|
||||
el => el.slot === 'input',
|
||||
));
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
|
|
@ -62,10 +64,12 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
`;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_groupOneTemplate() {
|
||||
return html`${this._labelTemplate()} ${this._helpTextTemplate()} ${this._feedbackTemplate()}`;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_groupTwoTemplate() {
|
||||
return html`${this._inputGroupTemplate()}`;
|
||||
}
|
||||
|
|
@ -74,6 +78,7 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
super();
|
||||
this.role = 'switch';
|
||||
this.checked = false;
|
||||
/** @private */
|
||||
this.__handleButtonSwitchCheckedChanged = this.__handleButtonSwitchCheckedChanged.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -109,16 +114,19 @@ export class LionSwitch extends ScopedElementsMixin(ChoiceInputMixin(LionField))
|
|||
|
||||
/**
|
||||
* Override this function from ChoiceInputMixin.
|
||||
* @protected
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__handleButtonSwitchCheckedChanged() {
|
||||
this.checked = this._inputNode.checked;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_syncButtonSwitch() {
|
||||
this._inputNode.disabled = this.disabled;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,8 +77,11 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
|
||||
this.role = 'switch';
|
||||
this.checked = false;
|
||||
/** @protected */
|
||||
this._toggleChecked = this._toggleChecked.bind(this);
|
||||
/** @private */
|
||||
this.__handleKeydown = this.__handleKeydown.bind(this);
|
||||
/** @private */
|
||||
this.__handleKeyup = this.__handleKeyup.bind(this);
|
||||
}
|
||||
|
||||
|
|
@ -97,6 +100,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
this.removeEventListener('keyup', this.__handleKeyup);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_toggleChecked() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
|
|
@ -106,6 +110,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__checkedStateChange() {
|
||||
this.dispatchEvent(
|
||||
new Event('checked-changed', {
|
||||
|
|
@ -118,6 +123,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
* @private
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
__handleKeydown(e) {
|
||||
|
|
@ -129,6 +135,7 @@ export class LionSwitchButton extends DisabledWithTabIndexMixin(LitElement) {
|
|||
|
||||
/**
|
||||
* @param {KeyboardEvent} e
|
||||
* @private
|
||||
*/
|
||||
__handleKeyup(e) {
|
||||
if ([32 /* space */, 13 /* enter */].indexOf(e.keyCode) !== -1) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,16 @@ import '@lion/switch/define';
|
|||
* @typedef {import('@lion/core').TemplateResult} TemplateResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {LionSwitch} lionSwitchEl
|
||||
*/
|
||||
function getProtectedMembers(lionSwitchEl) {
|
||||
return {
|
||||
// @ts-ignore
|
||||
inputNode: lionSwitchEl._inputNode,
|
||||
};
|
||||
}
|
||||
|
||||
const fixture = /** @type {(arg: TemplateResult) => Promise<LionSwitch>} */ (_fixture);
|
||||
|
||||
describe('lion-switch', () => {
|
||||
|
|
@ -31,13 +41,14 @@ describe('lion-switch', () => {
|
|||
|
||||
it('should sync its "disabled" state to child button', async () => {
|
||||
const el = await fixture(html`<lion-switch disabled></lion-switch>`);
|
||||
expect(el._inputNode.disabled).to.be.true;
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.be.true;
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
expect(inputNode.disabled).to.be.true;
|
||||
expect(inputNode.hasAttribute('disabled')).to.be.true;
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
await el.updateComplete; // safari takes longer
|
||||
expect(el._inputNode.disabled).to.be.false;
|
||||
expect(el._inputNode.hasAttribute('disabled')).to.be.false;
|
||||
expect(inputNode.disabled).to.be.false;
|
||||
expect(inputNode.hasAttribute('disabled')).to.be.false;
|
||||
});
|
||||
|
||||
it('is hidden when attribute hidden is true', async () => {
|
||||
|
|
@ -47,20 +58,23 @@ describe('lion-switch', () => {
|
|||
|
||||
it('should sync its "checked" state to child button', async () => {
|
||||
const uncheckedEl = await fixture(html`<lion-switch></lion-switch>`);
|
||||
const { inputNode: uncheckeInputNode } = getProtectedMembers(uncheckedEl);
|
||||
const checkedEl = await fixture(html`<lion-switch checked></lion-switch>`);
|
||||
expect(uncheckedEl._inputNode.checked).to.be.false;
|
||||
expect(checkedEl._inputNode.checked).to.be.true;
|
||||
const { inputNode: checkeInputNode } = getProtectedMembers(checkedEl);
|
||||
expect(uncheckeInputNode.checked).to.be.false;
|
||||
expect(checkeInputNode.checked).to.be.true;
|
||||
uncheckedEl.checked = true;
|
||||
checkedEl.checked = false;
|
||||
await uncheckedEl.updateComplete;
|
||||
await checkedEl.updateComplete;
|
||||
expect(uncheckedEl._inputNode.checked).to.be.true;
|
||||
expect(checkedEl._inputNode.checked).to.be.false;
|
||||
expect(uncheckeInputNode.checked).to.be.true;
|
||||
expect(checkeInputNode.checked).to.be.false;
|
||||
});
|
||||
|
||||
it('should sync "checked" state received from child button', async () => {
|
||||
const el = await fixture(html`<lion-switch></lion-switch>`);
|
||||
const button = el._inputNode;
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
const button = inputNode;
|
||||
expect(el.checked).to.be.false;
|
||||
button.click();
|
||||
expect(el.checked).to.be.true;
|
||||
|
|
@ -86,8 +100,9 @@ describe('lion-switch', () => {
|
|||
it('should dispatch "checked-changed" event when toggled via button or label', async () => {
|
||||
const handlerSpy = sinon.spy();
|
||||
const el = await fixture(html`<lion-switch .choiceValue=${'foo'}></lion-switch>`);
|
||||
const { inputNode } = getProtectedMembers(el);
|
||||
el.addEventListener('checked-changed', handlerSpy);
|
||||
el._inputNode.click();
|
||||
inputNode.click();
|
||||
el._labelNode.click();
|
||||
await el.updateComplete;
|
||||
expect(handlerSpy.callCount).to.equal(2);
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ export class LionTabs extends LitElement {
|
|||
this.__setupSlots();
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupSlots() {
|
||||
if (this.shadowRoot) {
|
||||
const tabSlot = this.shadowRoot.querySelector('slot[name=tab]');
|
||||
|
|
@ -181,6 +182,7 @@ export class LionTabs extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__setupStore() {
|
||||
/** @type {StoreEntry[]} */
|
||||
this.__store = [];
|
||||
|
|
@ -222,6 +224,7 @@ export class LionTabs extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__cleanStore() {
|
||||
if (!this.__store) {
|
||||
return;
|
||||
|
|
@ -235,6 +238,7 @@ export class LionTabs extends LitElement {
|
|||
/**
|
||||
* @param {number} index
|
||||
* @returns {EventHandlerNonNull}
|
||||
* @private
|
||||
*/
|
||||
__createButtonClickHandler(index) {
|
||||
return () => {
|
||||
|
|
@ -244,6 +248,7 @@ export class LionTabs extends LitElement {
|
|||
|
||||
/**
|
||||
* @param {Event} ev
|
||||
* @private
|
||||
*/
|
||||
__handleButtonKeyup(ev) {
|
||||
const _ev = /** @type {KeyboardEvent} */ (ev);
|
||||
|
|
@ -289,6 +294,7 @@ export class LionTabs extends LitElement {
|
|||
|
||||
/**
|
||||
* @param {number} value The new index for focus
|
||||
* @protected
|
||||
*/
|
||||
_setSelectedIndexWithFocus(value) {
|
||||
const stale = this.__selectedIndex;
|
||||
|
|
@ -305,10 +311,12 @@ export class LionTabs extends LitElement {
|
|||
return this.__selectedIndex || 0;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
get _pairCount() {
|
||||
return (this.__store && this.__store.length) || 0;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__updateSelected(withFocus = false) {
|
||||
if (
|
||||
!(this.__store && typeof this.selectedIndex === 'number' && this.__store[this.selectedIndex])
|
||||
|
|
|
|||
|
|
@ -302,6 +302,8 @@ describe('<lion-tabs>', () => {
|
|||
<div slot="panel">panel 2</div>
|
||||
</lion-tabs>
|
||||
`));
|
||||
|
||||
// @ts-ignore : this el is LionTabs
|
||||
el._setSelectedIndexWithFocus(1);
|
||||
expect(el.querySelector('[slot="tab"]:nth-of-type(2)') === document.activeElement).to.be.true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
|||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
get slots() {
|
||||
return {
|
||||
...super.slots,
|
||||
|
|
@ -95,7 +96,8 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
|||
if (changedProperties.has('rows')) {
|
||||
const native = this._inputNode;
|
||||
if (native) {
|
||||
native.rows = this.rows;
|
||||
// eslint-disable-next-line dot-notation
|
||||
native['rows'] = this.rows;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +171,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
|||
autosize.update(this._inputNode);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__initializeAutoresize() {
|
||||
// @ts-ignore this property is added by webcomponentsjs polyfill for old browsers
|
||||
if (this.__shady_native_contains) {
|
||||
|
|
@ -181,6 +184,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
async __waitForTextareaRenderedInRealDOM() {
|
||||
let count = 3; // max tasks to wait for
|
||||
// @ts-ignore this property is added by webcomponentsjs polyfill for old browsers
|
||||
|
|
@ -191,6 +195,7 @@ export class LionTextarea extends NativeTextFieldMixin(LionFieldWithTextArea) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__startAutoresize() {
|
||||
autosize(this._inputNode);
|
||||
this.setTextareaMaxHeight();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@ import '@lion/textarea/define';
|
|||
|
||||
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionTextarea>} */ (_fixture);
|
||||
|
||||
/**
|
||||
* @param {LionTextarea} lionTextareaEl
|
||||
*/
|
||||
function getProtectedMembers(lionTextareaEl) {
|
||||
const { _inputNode: input } = lionTextareaEl;
|
||||
return {
|
||||
input,
|
||||
};
|
||||
}
|
||||
|
||||
function hasBrowserResizeSupport() {
|
||||
const textarea = document.createElement('textarea');
|
||||
return textarea.style.resize !== undefined;
|
||||
|
|
@ -31,20 +41,25 @@ describe('<lion-textarea>', () => {
|
|||
|
||||
it('has .readOnly=false .rows=2 and rows="2" by default', async () => {
|
||||
const el = await fixture(`<lion-textarea>foo</lion-textarea>`);
|
||||
const { input } = getProtectedMembers(el);
|
||||
|
||||
expect(el.rows).to.equal(2);
|
||||
expect(el.getAttribute('rows')).to.be.equal('2');
|
||||
expect(el._inputNode.rows).to.equal(2);
|
||||
expect(el._inputNode.getAttribute('rows')).to.be.equal('2');
|
||||
// @ts-ignore
|
||||
expect(input.rows).to.equal(2);
|
||||
expect(input.getAttribute('rows')).to.be.equal('2');
|
||||
expect(el.readOnly).to.be.false;
|
||||
expect(el._inputNode.hasAttribute('readonly')).to.be.false;
|
||||
expect(input.hasAttribute('readonly')).to.be.false;
|
||||
});
|
||||
|
||||
it('sync rows down to the native textarea', async () => {
|
||||
const el = await fixture(`<lion-textarea rows="8">foo</lion-textarea>`);
|
||||
const { input } = getProtectedMembers(el);
|
||||
expect(el.rows).to.equal(8);
|
||||
expect(el.getAttribute('rows')).to.be.equal('8');
|
||||
expect(el._inputNode.rows).to.equal(8);
|
||||
expect(el._inputNode.getAttribute('rows')).to.be.equal('8');
|
||||
// @ts-ignore
|
||||
expect(input.rows).to.equal(8);
|
||||
expect(input.getAttribute('rows')).to.be.equal('8');
|
||||
});
|
||||
|
||||
it('sync readOnly to the native textarea', async () => {
|
||||
|
|
@ -59,7 +74,8 @@ describe('<lion-textarea>', () => {
|
|||
}
|
||||
|
||||
const el = await fixture(`<lion-textarea></lion-textarea>`);
|
||||
const computedStyle = window.getComputedStyle(el._inputNode);
|
||||
const { input } = getProtectedMembers(el);
|
||||
const computedStyle = window.getComputedStyle(input);
|
||||
expect(computedStyle.resize).to.equal('none');
|
||||
});
|
||||
|
||||
|
|
@ -139,13 +155,14 @@ describe('<lion-textarea>', () => {
|
|||
|
||||
it('has an attribute that can be used to set the placeholder text of the textarea', async () => {
|
||||
const el = await fixture(`<lion-textarea placeholder="text"></lion-textarea>`);
|
||||
const { input } = getProtectedMembers(el);
|
||||
expect(el.getAttribute('placeholder')).to.equal('text');
|
||||
expect(el._inputNode.getAttribute('placeholder')).to.equal('text');
|
||||
expect(input.getAttribute('placeholder')).to.equal('text');
|
||||
|
||||
el.placeholder = 'foo';
|
||||
await el.updateComplete;
|
||||
expect(el.getAttribute('placeholder')).to.equal('foo');
|
||||
expect(el._inputNode.getAttribute('placeholder')).to.equal('foo');
|
||||
expect(input.getAttribute('placeholder')).to.equal('foo');
|
||||
});
|
||||
|
||||
it('fires resize textarea when a visibility change has been detected', async () => {
|
||||
|
|
|
|||
|
|
@ -49,10 +49,13 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
* @type {'label'|'description'}
|
||||
*/
|
||||
this.invokerRelation = 'description';
|
||||
/** @protected */
|
||||
this._mouseActive = false;
|
||||
/** @protected */
|
||||
this._keyActive = false;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
_defineOverlayConfig() {
|
||||
return /** @type {OverlayConfig} */ ({
|
||||
|
|
@ -67,6 +70,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
});
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_hasDisabledInvoker() {
|
||||
if (this._overlayCtrl && this._overlayCtrl.invoker) {
|
||||
return (
|
||||
|
|
@ -77,6 +81,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_setupOpenCloseListeners() {
|
||||
super._setupOpenCloseListeners();
|
||||
this.__resetActive = this.__resetActive.bind(this);
|
||||
|
|
@ -92,6 +97,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
this._overlayInvokerNode.addEventListener('focusout', this._hideKey);
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_teardownOpenCloseListeners() {
|
||||
super._teardownOpenCloseListeners();
|
||||
this._overlayCtrl.removeEventListener('hide', this.__resetActive);
|
||||
|
|
@ -101,11 +107,13 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
|
||||
}
|
||||
|
||||
/** @private */
|
||||
__resetActive() {
|
||||
this._mouseActive = false;
|
||||
this._keyActive = false;
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_showMouse() {
|
||||
if (!this._keyActive) {
|
||||
this._mouseActive = true;
|
||||
|
|
@ -115,12 +123,14 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_hideMouse() {
|
||||
if (!this._keyActive) {
|
||||
this.opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_showKey() {
|
||||
if (!this._mouseActive) {
|
||||
this._keyActive = true;
|
||||
|
|
@ -130,6 +140,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
|
|||
}
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
_hideKey() {
|
||||
if (!this._mouseActive) {
|
||||
this.opened = false;
|
||||
|
|
|
|||
|
|
@ -4,30 +4,45 @@ import { localize } from '@lion/localize';
|
|||
import { Required } from '@lion/form-core';
|
||||
import { loadDefaultFeedbackMessages } from '../src/loadDefaultFeedbackMessages.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('@lion/form-core').Validator} Validator
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Validator} validatorEl
|
||||
*/
|
||||
function getProtectedMembers(validatorEl) {
|
||||
// @ts-ignore protected members allowed in test
|
||||
return {
|
||||
// @ts-ignore
|
||||
getMessage: (...args) => validatorEl._getMessage(...args),
|
||||
};
|
||||
}
|
||||
|
||||
describe('loadDefaultFeedbackMessages', () => {
|
||||
it('will set default feedback message for Required', async () => {
|
||||
const el = new Required();
|
||||
expect(await el._getMessage()).to.equals(
|
||||
const { getMessage } = getProtectedMembers(el);
|
||||
expect(await getMessage()).to.equals(
|
||||
'Please configure an error message for "Required" by overriding "static async getMessage()"',
|
||||
);
|
||||
|
||||
loadDefaultFeedbackMessages();
|
||||
expect(await el._getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
|
||||
expect(await getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
|
||||
});
|
||||
|
||||
it('will await loading of translations when switching locale', async () => {
|
||||
const el = new Required();
|
||||
const { getMessage } = getProtectedMembers(el);
|
||||
loadDefaultFeedbackMessages();
|
||||
expect(await el._getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
|
||||
expect(await el._getMessage({ fieldName: 'user name' })).to.equal(
|
||||
'Please enter a(n) user name.',
|
||||
);
|
||||
expect(await getMessage({ fieldName: 'password' })).to.equal('Please enter a(n) password.');
|
||||
expect(await getMessage({ fieldName: 'user name' })).to.equal('Please enter a(n) user name.');
|
||||
|
||||
localize.locale = 'de-DE';
|
||||
expect(await el._getMessage({ fieldName: 'Password' })).to.equal(
|
||||
expect(await getMessage({ fieldName: 'Password' })).to.equal(
|
||||
'Password muss ausgefüllt werden.',
|
||||
);
|
||||
expect(await el._getMessage({ fieldName: 'Benutzername' })).to.equal(
|
||||
expect(await getMessage({ fieldName: 'Benutzername' })).to.equal(
|
||||
'Benutzername muss ausgefüllt werden.',
|
||||
);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue