@lion/ui 0.18.1 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/calendar/src/LionCalendar.js +1 -1
- package/components/calendar/test/lion-calendar.test.js +67 -47
- package/components/calendar/test-suites/LionCalendar.suite.js +79 -0
- package/components/input-amount-dropdown/src/LionInputAmountDropdown.js +6 -4
- package/components/input-amount-dropdown/test/LionInputAmountDropdown.test.js +20 -1
- package/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.js +24 -0
- package/components/input-datepicker/test/lion-input-datepicker.test.js +13 -4
- package/components/input-datepicker/test-suites/LionInputDatepicker.suite.js +35 -0
- package/components/input-email/src/LionInputEmail.js +2 -0
- package/components/input-email/test/lion-input-email.test.js +6 -0
- package/components/input-file/src/validators.js +31 -1
- package/components/input-file/test/FileValidators.test.js +31 -0
- package/components/input-file/translations/bg.js +1 -0
- package/components/input-file/translations/cs.js +1 -0
- package/components/input-file/translations/de.js +2 -0
- package/components/input-file/translations/en.js +1 -0
- package/components/input-file/translations/es.js +1 -0
- package/components/input-file/translations/fr.js +1 -0
- package/components/input-file/translations/hu.js +1 -0
- package/components/input-file/translations/id.js +1 -0
- package/components/input-file/translations/it.js +1 -0
- package/components/input-file/translations/nl.js +1 -0
- package/components/input-file/translations/pl.js +1 -0
- package/components/input-file/translations/ro.js +1 -0
- package/components/input-file/translations/ru.js +1 -0
- package/components/input-file/translations/sk.js +1 -0
- package/components/input-file/translations/uk.js +1 -0
- package/components/input-file/translations/zh.js +1 -0
- package/components/input-tel/src/LionInputTel.js +1 -1
- package/components/input-tel/test-suites/LionInputTel.suite.js +7 -0
- package/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.js +132 -26
- package/components/overlays/test/withHoverInteraction.test.js +118 -0
- package/custom-elements.json +433 -92
- package/dist-types/components/calendar/test/lion-calendar.test.d.ts.map +1 -1
- package/dist-types/components/calendar/test-suites/LionCalendar.suite.d.ts +5 -0
- package/dist-types/components/calendar/test-suites/LionCalendar.suite.d.ts.map +1 -0
- package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts +0 -9
- package/dist-types/components/input-amount-dropdown/src/LionInputAmountDropdown.d.ts.map +1 -1
- package/dist-types/components/input-amount-dropdown/test/LionInputAmountDropdown.test.d.ts.map +1 -1
- package/dist-types/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.d.ts.map +1 -1
- package/dist-types/components/input-datepicker/test/lion-input-datepicker.test.d.ts.map +1 -1
- package/dist-types/components/input-datepicker/test-suites/LionInputDatepicker.suite.d.ts +7 -0
- package/dist-types/components/input-datepicker/test-suites/LionInputDatepicker.suite.d.ts.map +1 -0
- package/dist-types/components/input-email/src/LionInputEmail.d.ts.map +1 -1
- package/dist-types/components/input-file/src/validators.d.ts +11 -0
- package/dist-types/components/input-file/src/validators.d.ts.map +1 -1
- package/dist-types/components/input-file/translations/bg-BG.d.ts +1 -0
- package/dist-types/components/input-file/translations/bg.d.ts +1 -0
- package/dist-types/components/input-file/translations/cs-CZ.d.ts +1 -0
- package/dist-types/components/input-file/translations/cs.d.ts +1 -0
- package/dist-types/components/input-file/translations/de-DE.d.ts +1 -0
- package/dist-types/components/input-file/translations/de.d.ts +1 -0
- package/dist-types/components/input-file/translations/en-AU.d.ts +1 -0
- package/dist-types/components/input-file/translations/en-GB.d.ts +1 -0
- package/dist-types/components/input-file/translations/en-PH.d.ts +1 -0
- package/dist-types/components/input-file/translations/en-US.d.ts +1 -0
- package/dist-types/components/input-file/translations/en.d.ts +1 -0
- package/dist-types/components/input-file/translations/es-ES.d.ts +1 -0
- package/dist-types/components/input-file/translations/es.d.ts +1 -0
- package/dist-types/components/input-file/translations/fr-BE.d.ts +1 -0
- package/dist-types/components/input-file/translations/fr-FR.d.ts +1 -0
- package/dist-types/components/input-file/translations/fr.d.ts +1 -0
- package/dist-types/components/input-file/translations/hu-HU.d.ts +1 -0
- package/dist-types/components/input-file/translations/hu.d.ts +1 -0
- package/dist-types/components/input-file/translations/id-ID.d.ts +1 -0
- package/dist-types/components/input-file/translations/id.d.ts +1 -0
- package/dist-types/components/input-file/translations/it-IT.d.ts +1 -0
- package/dist-types/components/input-file/translations/it.d.ts +1 -0
- package/dist-types/components/input-file/translations/nl-BE.d.ts +1 -0
- package/dist-types/components/input-file/translations/nl-NL.d.ts +1 -0
- package/dist-types/components/input-file/translations/nl.d.ts +1 -0
- package/dist-types/components/input-file/translations/pl-PL.d.ts +1 -0
- package/dist-types/components/input-file/translations/pl.d.ts +1 -0
- package/dist-types/components/input-file/translations/ro-RO.d.ts +1 -0
- package/dist-types/components/input-file/translations/ro.d.ts +1 -0
- package/dist-types/components/input-file/translations/ru-RU.d.ts +1 -0
- package/dist-types/components/input-file/translations/ru.d.ts +1 -0
- package/dist-types/components/input-file/translations/sk-SK.d.ts +1 -0
- package/dist-types/components/input-file/translations/sk.d.ts +1 -0
- package/dist-types/components/input-file/translations/uk-UA.d.ts +1 -0
- package/dist-types/components/input-file/translations/uk.d.ts +1 -0
- package/dist-types/components/input-file/translations/zh-CN.d.ts +1 -0
- package/dist-types/components/input-file/translations/zh.d.ts +1 -0
- package/dist-types/components/input-tel/src/LionInputTel.d.ts.map +1 -1
- package/dist-types/components/input-tel/test-suites/LionInputTel.suite.d.ts.map +1 -1
- package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts +8 -2
- package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts.map +1 -1
- package/dist-types/components/overlays/test/withHoverInteraction.test.d.ts +2 -0
- package/dist-types/components/overlays/test/withHoverInteraction.test.d.ts.map +1 -0
- package/dist-types/exports/input-file.d.ts +1 -1
- package/dist-types/tsconfig.tsbuildinfo +1 -1
- package/docs/components/checkbox-group/overview.md +2 -2
- package/docs/components/collapsible/overview.md +1 -1
- package/docs/components/combobox/overview.md +1 -1
- package/docs/components/combobox/use-cases.md +1 -1
- package/docs/components/dialog/overview.md +1 -1
- package/docs/components/fieldset/overview.md +5 -5
- package/docs/components/form/overview.md +3 -3
- package/docs/components/form/use-cases.md +2 -2
- package/docs/components/icon/use-cases.md +1 -1
- package/docs/components/input/overview.md +1 -1
- package/docs/components/input-amount/overview.md +3 -3
- package/docs/components/input-amount-dropdown/api-table.md +1 -1
- package/docs/components/input-date/overview.md +3 -3
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/api-table.md +1 -1
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/overview.md +1 -1
- package/docs/components/input-file/use-cases.md +18 -0
- package/docs/components/input-iban/overview.md +2 -2
- package/docs/components/input-range/overview.md +2 -2
- package/docs/components/input-stepper/overview.md +1 -1
- package/docs/components/input-tel/api-table.md +1 -1
- package/docs/components/input-tel/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- package/docs/components/input-tel-dropdown/api-table.md +1 -1
- package/docs/components/input-tel-dropdown/overview.md +1 -1
- package/docs/components/listbox/use-cases.md +1 -1
- package/docs/components/radio-group/overview.md +3 -3
- package/docs/components/select/use-cases.md +1 -1
- package/docs/components/select-rich/use-cases.md +2 -2
- package/docs/components/tabs/examples.md +1 -1
- package/docs/components/tooltip/use-cases.md +55 -0
- package/docs/fundamentals/index.md +4 -4
- package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
- package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
- package/docs/fundamentals/rationales/accessibility.md +1 -1
- package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
- package/docs/fundamentals/systems/core/overview.md +1 -1
- package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
- package/docs/fundamentals/systems/form/model-value.md +1 -1
- package/docs/fundamentals/systems/form/overview.md +28 -28
- package/docs/fundamentals/systems/form/validate.md +2 -2
- package/docs/fundamentals/systems/localize/overview.md +4 -4
- package/docs/fundamentals/systems/overlays/configuration.md +2 -2
- package/docs/fundamentals/systems/overlays/overview.md +5 -5
- package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
- package/docs/fundamentals/tools/helpers/overview.md +1 -1
- package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
- package/docs/guides/how-to/create-a-custom-field.md +5 -5
- package/docs/guides/how-to/create-a-form.md +4 -4
- package/docs/guides/how-to/extend-a-native-input.md +2 -2
- package/docs/guides/how-to/index.md +1 -1
- package/docs/guides/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- package/exports/input-file.js +5 -1
- package/input-file.js +5 -1
- package/package.json +2 -2
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Már volt ilyen nevű fájl.',
|
|
18
18
|
selectTextMultipleFile: 'Fájl(ok) kiválasztása',
|
|
19
19
|
selectTextSingleFile: 'Fájl kiválasztása',
|
|
20
|
+
maxFileCountExceeded: 'A feltöltött fájlok száma nem haladhatja meg a {maxFileCount}-ot.',
|
|
20
21
|
};
|
|
@@ -16,4 +16,5 @@ export default {
|
|
|
16
16
|
selectTextDuplicateFileName: 'File dengan nama file yang sama sudah ada.',
|
|
17
17
|
selectTextMultipleFile: 'Pilih beberapa file',
|
|
18
18
|
selectTextSingleFile: 'Pilih berkas',
|
|
19
|
+
maxFileCountExceeded: 'Jumlah file yang diunggah tidak boleh melebihi {maxFileCount}.',
|
|
19
20
|
};
|
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Un file con lo stesso nome file era già presente.',
|
|
18
18
|
selectTextMultipleFile: 'Seleziona file',
|
|
19
19
|
selectTextSingleFile: 'Seleziona file',
|
|
20
|
+
maxFileCountExceeded: 'Il numero di file caricati non può superare {maxFileCount}.',
|
|
20
21
|
};
|
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Er bestaat al een bestand met dezelfde bestandsnaam.',
|
|
18
18
|
selectTextMultipleFile: 'Selecteer bestanden',
|
|
19
19
|
selectTextSingleFile: 'Selecteer bestand',
|
|
20
|
+
maxFileCountExceeded: 'Het aantal geüploade bestanden mag niet meer dan {maxFileCount} zijn.',
|
|
20
21
|
};
|
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Plik o tej samej nazwie już istnieje.',
|
|
18
18
|
selectTextMultipleFile: 'Wybierz pliki',
|
|
19
19
|
selectTextSingleFile: 'Wybierz plik',
|
|
20
|
+
maxFileCountExceeded: 'Liczba przesłanych plików nie może przekraczać {maxFileCount}.',
|
|
20
21
|
};
|
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Există deja un fişier cu acelaşi nume de fişier.',
|
|
18
18
|
selectTextMultipleFile: 'Selectare fișiere',
|
|
19
19
|
selectTextSingleFile: 'Selectare fișier',
|
|
20
|
+
maxFileCountExceeded: 'Numărul de fișiere încărcate nu poate depăși {maxFileCount}.',
|
|
20
21
|
};
|
|
@@ -16,4 +16,5 @@ export default {
|
|
|
16
16
|
selectTextDuplicateFileName: 'Файл с таким названием уже существует.',
|
|
17
17
|
selectTextMultipleFile: 'Выберите файлы',
|
|
18
18
|
selectTextSingleFile: 'Выберите файл',
|
|
19
|
+
maxFileCountExceeded: 'Количество загруженных файлов не может превышать {maxFileCount}.',
|
|
19
20
|
};
|
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Súbor s rovnakým názvom súboru už existoval.',
|
|
18
18
|
selectTextMultipleFile: 'Vybrať súbory',
|
|
19
19
|
selectTextSingleFile: 'Vybrať súbor',
|
|
20
|
+
maxFileCountExceeded: 'Počet nahratých súborov nesmie prekročiť {maxFileCount}.',
|
|
20
21
|
};
|
|
@@ -17,4 +17,5 @@ export default {
|
|
|
17
17
|
selectTextDuplicateFileName: 'Файл із такою назвою вже існував.',
|
|
18
18
|
selectTextMultipleFile: 'Виберіть файли',
|
|
19
19
|
selectTextSingleFile: 'Виберіть файл',
|
|
20
|
+
maxFileCountExceeded: 'Кількість завантажених файлів не може перевищувати {maxFileCount}.',
|
|
20
21
|
};
|
|
@@ -137,6 +137,7 @@ export class LionInputTel extends LocalizeMixin(LionInput) {
|
|
|
137
137
|
*/
|
|
138
138
|
constructor() {
|
|
139
139
|
super();
|
|
140
|
+
this.autocomplete = 'tel';
|
|
140
141
|
|
|
141
142
|
/**
|
|
142
143
|
* Determines what the formatter output should look like.
|
|
@@ -192,7 +193,6 @@ export class LionInputTel extends LocalizeMixin(LionInput) {
|
|
|
192
193
|
*/
|
|
193
194
|
firstUpdated(changedProperties) {
|
|
194
195
|
super.firstUpdated(changedProperties);
|
|
195
|
-
|
|
196
196
|
// This will trigger the right keyboard on mobile
|
|
197
197
|
this._inputNode.inputMode = 'tel';
|
|
198
198
|
}
|
|
@@ -222,6 +222,13 @@ export function runInputTelSuite({ klass = LionInputTel } = {}) {
|
|
|
222
222
|
expect(el._inputNode.inputMode).to.equal('tel');
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
+
it('sets autocomplete to "tel"', async () => {
|
|
226
|
+
const el = await fixture(html` <${tag}></${tag}> `);
|
|
227
|
+
await el.updateComplete;
|
|
228
|
+
// @ts-expect-error [allow-protected] inside tests
|
|
229
|
+
expect(el._inputNode.getAttribute('autocomplete')).to.equal('tel');
|
|
230
|
+
});
|
|
231
|
+
|
|
225
232
|
it('formats according to locale', async () => {
|
|
226
233
|
const el = await fixture(
|
|
227
234
|
html` <${tag} .modelValue="${'+31612345678'}" .allowedRegions="${['NL']}"></${tag}> `,
|
package/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.js
CHANGED
|
@@ -5,14 +5,27 @@
|
|
|
5
5
|
|
|
6
6
|
// N.B. Below logic is tested in LionTooltip
|
|
7
7
|
|
|
8
|
+
// TODO: this is copied from menu system. Move them to common ancestor entrypoint (maybe a core entrypoint for all interactive controls)
|
|
9
|
+
/**
|
|
10
|
+
* @param {HTMLElement|undefined} item
|
|
11
|
+
*/
|
|
12
|
+
export function isDisabled(item) {
|
|
13
|
+
return item && (item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true');
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* Use for tooltips and [flyout menus](https://www.w3.org/WAI/tutorials/menus/flyout/).
|
|
10
18
|
* Note that it handles both mouse hover and focus interaction.
|
|
11
19
|
* Provide delayIn and delayOut when the content needs a small delay before being showed
|
|
12
|
-
* @param {{ delayIn?: number, delayOut?: number }} options
|
|
20
|
+
* @param {{ isHoverSupported?: boolean, longpressDuration?: number, delayIn?: number, delayOut?: number }} options
|
|
13
21
|
* @returns {Partial<OverlayConfig>}
|
|
14
22
|
*/
|
|
15
|
-
export function withHoverInteraction({
|
|
23
|
+
export function withHoverInteraction({
|
|
24
|
+
isHoverSupported = window.matchMedia('(hover: hover)').matches,
|
|
25
|
+
longpressDuration = 500,
|
|
26
|
+
delayOut = 300,
|
|
27
|
+
delayIn = 0,
|
|
28
|
+
} = {}) {
|
|
16
29
|
return {
|
|
17
30
|
visibilityTriggerFunction: (
|
|
18
31
|
/** @type {{ controller: OverlayController }} */ { controller },
|
|
@@ -20,53 +33,146 @@ export function withHoverInteraction({ delayIn = 0, delayOut = 300 }) {
|
|
|
20
33
|
let isFocused = false;
|
|
21
34
|
let isHovered = false;
|
|
22
35
|
/** @type {NodeJS.Timeout} */
|
|
23
|
-
let
|
|
36
|
+
let pendingDelayTimeout;
|
|
37
|
+
/** @type {NodeJS.Timeout} */
|
|
38
|
+
let pendingLongpressTimeout;
|
|
39
|
+
let longpressCompleted = false;
|
|
40
|
+
// A tap fires pointerdown → pointerup → focusin because the browser moves keyboard focus to
|
|
41
|
+
// the tapped element. We only want focusin to open the tooltip when triggered by keyboard (Tab),
|
|
42
|
+
// not by tap. isLastPointerTouch marks that the preceding pointer event was a touch so the
|
|
43
|
+
// focusin handler can skip it.
|
|
44
|
+
let isLastPointerTouch = false;
|
|
45
|
+
// A tap fires the full mouse compatibility sequence: pointerdown → pointerup → mousedown → mouseup → click.
|
|
46
|
+
// Browsers generate these compatibility events automatically so touch works with mouse-only code.
|
|
47
|
+
// After a longpress the same click fires, which would trigger any click handler on the invoker
|
|
48
|
+
// e.g. opening a dialog alongside the tooltip. We register a capture-phase listener at longpress
|
|
49
|
+
// completion to intercept it, as capture runs before bubble so we stop it regardless of registration order.
|
|
50
|
+
/** @type {((e: Event) => void) | null} */
|
|
51
|
+
let pendingClickSuppressor = null;
|
|
24
52
|
|
|
25
53
|
function resetActive() {
|
|
26
54
|
isFocused = false;
|
|
27
55
|
isHovered = false;
|
|
56
|
+
longpressCompleted = false;
|
|
57
|
+
if (pendingClickSuppressor) {
|
|
58
|
+
controller.invokerNode?.removeEventListener('click', pendingClickSuppressor, {
|
|
59
|
+
capture: true,
|
|
60
|
+
});
|
|
61
|
+
pendingClickSuppressor = null;
|
|
62
|
+
}
|
|
28
63
|
}
|
|
29
64
|
|
|
30
65
|
/**
|
|
31
|
-
* @param {
|
|
66
|
+
* @param {{ shouldOpen: boolean, openTimeout?: number, closeTimeout?: number }} cfg
|
|
32
67
|
*/
|
|
33
|
-
function
|
|
34
|
-
|
|
68
|
+
function openClose({ shouldOpen, openTimeout = 0, closeTimeout = 0 }) {
|
|
69
|
+
clearTimeout(pendingDelayTimeout);
|
|
70
|
+
if (shouldOpen && !isDisabled(controller.invokerNode)) {
|
|
71
|
+
pendingDelayTimeout = setTimeout(() => controller.show(), openTimeout);
|
|
72
|
+
} else {
|
|
73
|
+
pendingDelayTimeout = setTimeout(() => controller.hide(), closeTimeout);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
35
76
|
|
|
36
|
-
|
|
77
|
+
/**
|
|
78
|
+
* @param {Event|PointerEvent} event
|
|
79
|
+
*/
|
|
80
|
+
function handleHoverAndFocus(event) {
|
|
81
|
+
const { type } = event;
|
|
37
82
|
isFocused = type === 'focusout' ? false : isFocused || type === 'focusin';
|
|
38
83
|
isHovered = type === 'mouseleave' ? false : isHovered || type === 'mouseenter';
|
|
84
|
+
if (!isHoverSupported && type === 'focusin') {
|
|
85
|
+
if (isLastPointerTouch) {
|
|
86
|
+
isLastPointerTouch = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!controller.invokerNode?.matches(':focus-visible')) return;
|
|
90
|
+
}
|
|
39
91
|
const shouldOpen = isFocused || isHovered;
|
|
92
|
+
openClose({ shouldOpen, openTimeout: delayIn, closeTimeout: delayOut });
|
|
93
|
+
}
|
|
40
94
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
95
|
+
/** @param {Event} e */
|
|
96
|
+
function preventContextMenu(e) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {PointerEvent} event */
|
|
101
|
+
function handleLongpress(event) {
|
|
102
|
+
clearTimeout(pendingLongpressTimeout);
|
|
103
|
+
if (event.pointerType !== 'touch') return;
|
|
104
|
+
const { type } = event;
|
|
105
|
+
|
|
106
|
+
if (type === 'pointerdown') {
|
|
107
|
+
longpressCompleted = false;
|
|
108
|
+
isLastPointerTouch = true;
|
|
109
|
+
pendingLongpressTimeout = setTimeout(() => {
|
|
110
|
+
longpressCompleted = true;
|
|
111
|
+
// Intercept the compatibility click the browser generates at the end of the touch sequence.
|
|
112
|
+
// Capture phase ensures we run before any bubble-phase handler (e.g. a dialog opener).
|
|
113
|
+
pendingClickSuppressor = e => e.stopImmediatePropagation();
|
|
114
|
+
controller.invokerNode?.addEventListener('click', pendingClickSuppressor, {
|
|
115
|
+
once: true, // auto-removes after first click
|
|
116
|
+
capture: true,
|
|
117
|
+
});
|
|
118
|
+
openClose({ shouldOpen: true });
|
|
119
|
+
}, longpressDuration);
|
|
45
120
|
} else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
121
|
+
openClose({
|
|
122
|
+
shouldOpen: false,
|
|
123
|
+
closeTimeout: longpressCompleted ? longpressDuration : 0,
|
|
124
|
+
});
|
|
49
125
|
}
|
|
50
126
|
}
|
|
51
127
|
|
|
52
128
|
return {
|
|
53
129
|
init: () => {
|
|
54
130
|
controller.addEventListener('hide', resetActive);
|
|
55
|
-
|
|
56
|
-
controller.
|
|
57
|
-
controller.invokerNode?.addEventListener('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
131
|
+
|
|
132
|
+
controller.invokerNode?.addEventListener('focusin', handleHoverAndFocus);
|
|
133
|
+
controller.invokerNode?.addEventListener('focusout', handleHoverAndFocus);
|
|
134
|
+
|
|
135
|
+
if (isHoverSupported) {
|
|
136
|
+
controller.contentNode?.addEventListener('mouseenter', handleHoverAndFocus);
|
|
137
|
+
controller.contentNode?.addEventListener('mouseleave', handleHoverAndFocus);
|
|
138
|
+
controller.invokerNode?.addEventListener('mouseenter', handleHoverAndFocus);
|
|
139
|
+
controller.invokerNode?.addEventListener('mouseleave', handleHoverAndFocus);
|
|
140
|
+
} else {
|
|
141
|
+
controller.invokerNode?.style.setProperty('-webkit-touch-callout', 'none');
|
|
142
|
+
controller.invokerNode?.style.setProperty('user-select', 'none');
|
|
143
|
+
controller.invokerNode?.style.setProperty('-webkit-user-select', 'none');
|
|
144
|
+
controller.invokerNode?.addEventListener('contextmenu', preventContextMenu);
|
|
145
|
+
controller.invokerNode?.addEventListener('pointerdown', handleLongpress);
|
|
146
|
+
controller.invokerNode?.addEventListener('pointerup', handleLongpress);
|
|
147
|
+
controller.invokerNode?.addEventListener('pointerleave', handleLongpress);
|
|
148
|
+
}
|
|
61
149
|
},
|
|
62
150
|
teardown: () => {
|
|
63
151
|
controller.removeEventListener('hide', resetActive);
|
|
64
|
-
|
|
65
|
-
controller.
|
|
66
|
-
controller.invokerNode?.removeEventListener('
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
152
|
+
|
|
153
|
+
controller.invokerNode?.removeEventListener('focusin', handleHoverAndFocus);
|
|
154
|
+
controller.invokerNode?.removeEventListener('focusout', handleHoverAndFocus);
|
|
155
|
+
|
|
156
|
+
if (isHoverSupported) {
|
|
157
|
+
controller.contentNode?.removeEventListener('mouseenter', handleHoverAndFocus);
|
|
158
|
+
controller.contentNode?.removeEventListener('mouseleave', handleHoverAndFocus);
|
|
159
|
+
controller.invokerNode?.removeEventListener('mouseenter', handleHoverAndFocus);
|
|
160
|
+
controller.invokerNode?.removeEventListener('mouseleave', handleHoverAndFocus);
|
|
161
|
+
} else {
|
|
162
|
+
controller.invokerNode?.style.removeProperty('-webkit-touch-callout');
|
|
163
|
+
controller.invokerNode?.style.removeProperty('user-select');
|
|
164
|
+
controller.invokerNode?.style.removeProperty('-webkit-user-select');
|
|
165
|
+
controller.invokerNode?.removeEventListener('contextmenu', preventContextMenu);
|
|
166
|
+
controller.invokerNode?.removeEventListener('pointerdown', handleLongpress);
|
|
167
|
+
controller.invokerNode?.removeEventListener('pointerup', handleLongpress);
|
|
168
|
+
controller.invokerNode?.removeEventListener('pointerleave', handleLongpress);
|
|
169
|
+
if (pendingClickSuppressor) {
|
|
170
|
+
controller.invokerNode?.removeEventListener('click', pendingClickSuppressor, {
|
|
171
|
+
capture: true,
|
|
172
|
+
});
|
|
173
|
+
pendingClickSuppressor = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
70
176
|
},
|
|
71
177
|
};
|
|
72
178
|
},
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { OverlayController } from '@lion/ui/overlays.js';
|
|
2
|
+
import { expect, fixtureSync, html } from '@open-wc/testing';
|
|
3
|
+
import sinon from 'sinon';
|
|
4
|
+
import { withHoverInteraction } from '../src/configurations/visibility-trigger-partials/withHoverInteraction.js';
|
|
5
|
+
|
|
6
|
+
function makeCtrl(opts = {}) {
|
|
7
|
+
return new OverlayController({
|
|
8
|
+
placementMode: 'local',
|
|
9
|
+
contentNode: /** @type {HTMLElement} */ (fixtureSync(html`<div>content</div>`)),
|
|
10
|
+
invokerNode: /** @type {HTMLElement} */ (fixtureSync(html`<button>invoker</button>`)),
|
|
11
|
+
...withHoverInteraction(opts),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('withHoverInteraction (isHoverSupported: false)', () => {
|
|
16
|
+
/** @type {sinon.SinonFakeTimers} */
|
|
17
|
+
let clock;
|
|
18
|
+
/** @type {OverlayController} */
|
|
19
|
+
let ctrl;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
clock = sinon.useFakeTimers();
|
|
23
|
+
ctrl = makeCtrl({ isHoverSupported: false });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
ctrl.teardown();
|
|
28
|
+
clock.restore();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('opens after holding for longpressDuration (500ms)', async () => {
|
|
32
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
33
|
+
new PointerEvent('pointerdown', { pointerType: 'touch', bubbles: true }),
|
|
34
|
+
);
|
|
35
|
+
clock.runAll();
|
|
36
|
+
await Promise.resolve();
|
|
37
|
+
expect(ctrl.isShown).to.equal(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('stays hidden on brief touch: pointerup cancels the timer', () => {
|
|
41
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
42
|
+
new PointerEvent('pointerdown', { pointerType: 'touch', bubbles: true }),
|
|
43
|
+
);
|
|
44
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
45
|
+
new PointerEvent('pointerup', { pointerType: 'touch', bubbles: true }),
|
|
46
|
+
);
|
|
47
|
+
clock.tick(500);
|
|
48
|
+
expect(ctrl.isShown).to.equal(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('ignores non-touch pointer events (mouse)', () => {
|
|
52
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
53
|
+
new PointerEvent('pointerdown', { pointerType: 'mouse', bubbles: true }),
|
|
54
|
+
);
|
|
55
|
+
clock.tick(500);
|
|
56
|
+
expect(ctrl.isShown).to.equal(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('auto-closes after longpressDuration once press ends', async () => {
|
|
60
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
61
|
+
new PointerEvent('pointerdown', { pointerType: 'touch', bubbles: true }),
|
|
62
|
+
);
|
|
63
|
+
clock.runAll();
|
|
64
|
+
await Promise.resolve();
|
|
65
|
+
expect(ctrl.isShown).to.equal(true);
|
|
66
|
+
|
|
67
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
68
|
+
new PointerEvent('pointerup', { pointerType: 'touch', bubbles: true }),
|
|
69
|
+
);
|
|
70
|
+
await clock.tickAsync(501);
|
|
71
|
+
expect(ctrl.isShown).to.equal(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('disables text selection on the invoker on init and restores it on teardown', () => {
|
|
75
|
+
const { invokerNode } = ctrl;
|
|
76
|
+
const getUserSelect = () =>
|
|
77
|
+
invokerNode?.style.getPropertyValue('user-select') ||
|
|
78
|
+
invokerNode?.style.getPropertyValue('-webkit-user-select');
|
|
79
|
+
expect(getUserSelect()).to.equal('none');
|
|
80
|
+
|
|
81
|
+
ctrl.teardown();
|
|
82
|
+
expect(getUserSelect()).to.equal('');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not open on tap-triggered focusin', () => {
|
|
86
|
+
ctrl.invokerNode?.dispatchEvent(new Event('focusin', { bubbles: true }));
|
|
87
|
+
clock.tick(300);
|
|
88
|
+
expect(ctrl.isShown).to.equal(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('suppresses focusin that immediately follows a touch pointerdown', () => {
|
|
92
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
93
|
+
new PointerEvent('pointerdown', { pointerType: 'touch', bubbles: true }),
|
|
94
|
+
);
|
|
95
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
96
|
+
new PointerEvent('pointerup', { pointerType: 'touch', bubbles: true }),
|
|
97
|
+
);
|
|
98
|
+
ctrl.invokerNode?.dispatchEvent(new Event('focusin', { bubbles: true }));
|
|
99
|
+
clock.tick(1); // fires any 0ms open timer if focusin was not suppressed
|
|
100
|
+
expect(ctrl.isShown).to.equal(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('suppresses the click event that fires after a completed longpress', async () => {
|
|
104
|
+
ctrl.invokerNode?.dispatchEvent(
|
|
105
|
+
new PointerEvent('pointerdown', { pointerType: 'touch', bubbles: true }),
|
|
106
|
+
);
|
|
107
|
+
clock.runAll();
|
|
108
|
+
await Promise.resolve();
|
|
109
|
+
expect(ctrl.isShown).to.equal(true);
|
|
110
|
+
|
|
111
|
+
let clickFired = false;
|
|
112
|
+
ctrl.invokerNode?.addEventListener('click', () => {
|
|
113
|
+
clickFired = true;
|
|
114
|
+
});
|
|
115
|
+
ctrl.invokerNode?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
116
|
+
expect(clickFired).to.equal(false);
|
|
117
|
+
});
|
|
118
|
+
});
|