@lion/ui 0.15.4 → 0.16.0-alpha.0
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/collapsible/src/LionCollapsible.js +0 -2
- package/components/collapsible/test/lion-collapsible.test.js +0 -9
- package/components/core/test-helpers/ssr-definitions/ScopedElementsHost.define.js +1 -0
- package/components/drawer/test/lion-drawer.test.js +0 -9
- package/components/form-core/src/choice-group/ChoiceGroupMixin.js +12 -2
- package/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.js +48 -4
- package/components/input-tel/translations/es.js +1 -1
- package/components/overlays/src/OverlayController.js +99 -66
- package/components/overlays/src/OverlaysManager.js +0 -33
- package/components/overlays/test/OverlayController.test.js +55 -32
- package/custom-elements.json +105 -200
- package/dist-types/components/collapsible/src/LionCollapsible.d.ts.map +1 -1
- package/dist-types/components/core/test-helpers/ssr-definitions/ScopedElementsHost.define.d.ts +2 -0
- package/dist-types/components/core/test-helpers/ssr-definitions/ScopedElementsHost.define.d.ts.map +1 -0
- package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
- package/dist-types/components/form-core/test-suites/choice-group/ChoiceGroupMixin.suite.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlayController.d.ts +1 -11
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/src/OverlaysManager.d.ts +0 -12
- package/dist-types/components/overlays/src/OverlaysManager.d.ts.map +1 -1
- package/dist-types/components/overlays/test/OverlayController.test.d.ts.map +1 -1
- package/dist-types/exports/overlays.d.ts +0 -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-date/overview.md +3 -3
- package/docs/components/input-datepicker/overview.md +5 -5
- package/docs/components/input-email/overview.md +2 -2
- package/docs/components/input-file/overview.md +1 -1
- 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/overview.md +1 -1
- package/docs/components/input-tel/use-cases.md +6 -6
- 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/switch/use-cases.md +3 -3
- package/docs/components/tabs/examples.md +1 -1
- 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/TypeScript.md +46 -0
- 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/api-table.md +45 -51
- package/docs/fundamentals/systems/overlays/configuration.md +5 -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/index.md +8 -8
- package/docs/guides/principles/index.md +5 -5
- package/docs/guides/principles/styling.md +1 -1
- package/exports/overlays.js +0 -1
- package/overlays.js +0 -1
- package/package.json +1 -1
- package/components/overlays/src/utils/contain-focus.js +0 -202
- package/components/overlays/test/utils-tests/contain-focus.test.js +0 -236
- package/dist-types/components/overlays/src/utils/contain-focus.d.ts +0 -20
- package/dist-types/components/overlays/src/utils/contain-focus.d.ts.map +0 -1
- package/dist-types/components/overlays/test/utils-tests/contain-focus.test.d.ts +0 -2
- package/dist-types/components/overlays/test/utils-tests/contain-focus.test.d.ts.map +0 -1
|
@@ -54,12 +54,10 @@ export class LionCollapsible extends LitElement {
|
|
|
54
54
|
if (this._invokerNode) {
|
|
55
55
|
this._invokerNode.addEventListener('click', this.toggle);
|
|
56
56
|
this._invokerNode.setAttribute('aria-expanded', `${this.opened}`);
|
|
57
|
-
this._invokerNode.setAttribute('id', `collapsible-invoker-${uid}`);
|
|
58
57
|
this._invokerNode.setAttribute('aria-controls', `collapsible-content-${uid}`);
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
if (this._contentNode) {
|
|
62
|
-
this._contentNode.setAttribute('aria-labelledby', `collapsible-invoker-${uid}`);
|
|
63
61
|
this._contentNode.setAttribute('id', `collapsible-content-${uid}`);
|
|
64
62
|
}
|
|
65
63
|
|
|
@@ -136,14 +136,5 @@ describe('<lion-collapsible>', () => {
|
|
|
136
136
|
expect(invoker).to.have.attribute('aria-expanded', 'true');
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
|
-
|
|
140
|
-
describe('Contents', () => {
|
|
141
|
-
it('adds aria-labelledby referring to invoker id', async () => {
|
|
142
|
-
const collapsibleElement = await fixture(defaultCollapsible);
|
|
143
|
-
const invoker = collapsibleElement.querySelector('[slot=invoker]');
|
|
144
|
-
const content = collapsibleElement.querySelector('[slot=content]');
|
|
145
|
-
expect(content).to.have.attribute('aria-labelledby', invoker?.id);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
139
|
});
|
|
149
140
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// ... empty file needed for '@lit-labs/testing/fixtures.js'
|
|
@@ -96,14 +96,5 @@ describe('<lion-drawer>', () => {
|
|
|
96
96
|
expect(invoker).to.have.attribute('aria-expanded', 'true');
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
|
-
|
|
100
|
-
describe('Contents', () => {
|
|
101
|
-
it('adds aria-labelledby referring to invoker id', async () => {
|
|
102
|
-
const drawerElement = await fixture(template);
|
|
103
|
-
const invoker = drawerElement.querySelector('[slot=invoker]');
|
|
104
|
-
const content = drawerElement.querySelector('[slot=content]');
|
|
105
|
-
expect(content).to.have.attribute('aria-labelledby', invoker?.id);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
99
|
});
|
|
109
100
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dedupeMixin } from '@open-wc/dedupe-mixin';
|
|
2
|
-
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
|
3
2
|
import { InteractionStateMixin } from '../InteractionStateMixin.js';
|
|
3
|
+
import { FormRegistrarMixin } from '../registration/FormRegistrarMixin.js';
|
|
4
4
|
import { ValidateMixin } from '../validate/ValidateMixin.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -260,7 +260,17 @@ const ChoiceGroupMixinImplementation = superclass =>
|
|
|
260
260
|
) {
|
|
261
261
|
return this[property];
|
|
262
262
|
}
|
|
263
|
-
|
|
263
|
+
|
|
264
|
+
// TODO: consider a shared approach for all value getters
|
|
265
|
+
const filteredFnElements = this.formElements.filter(el => _filterFn(el, property));
|
|
266
|
+
|
|
267
|
+
if (property === '_initialModelValue') {
|
|
268
|
+
return this.multipleChoice
|
|
269
|
+
? filteredFnElements.filter(el => el[property].checked).map(el => el[property].value)
|
|
270
|
+
: filteredFnElements.find(el => el[property].checked)?.value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return filteredFnElements.map(el => el[property]);
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
/**
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { LitElement } from 'lit';
|
|
2
|
-
import '@lion/ui/define/lion-fieldset.js';
|
|
3
1
|
import '@lion/ui/define/lion-checkbox-group.js';
|
|
4
2
|
import '@lion/ui/define/lion-checkbox.js';
|
|
3
|
+
import '@lion/ui/define/lion-fieldset.js';
|
|
5
4
|
import {
|
|
6
|
-
FormGroupMixin,
|
|
7
|
-
Required,
|
|
8
5
|
ChoiceGroupMixin,
|
|
9
6
|
ChoiceInputMixin,
|
|
7
|
+
FormGroupMixin,
|
|
8
|
+
Required,
|
|
10
9
|
} from '@lion/ui/form-core.js';
|
|
11
10
|
import { LionInput } from '@lion/ui/input.js';
|
|
12
11
|
import { expect, fixture, fixtureSync, html, unsafeStatic } from '@open-wc/testing';
|
|
12
|
+
import { LitElement } from 'lit';
|
|
13
13
|
import sinon from 'sinon';
|
|
14
14
|
|
|
15
15
|
class ChoiceInputFoo extends ChoiceInputMixin(LionInput) {}
|
|
@@ -45,6 +45,28 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
|
|
|
45
45
|
|
|
46
46
|
describe(`ChoiceGroupMixin: ${cfg.parentTagString}`, () => {
|
|
47
47
|
if (cfg.choiceType === 'single') {
|
|
48
|
+
it('has a single modelValue representing initial checked values', async () => {
|
|
49
|
+
const el = /** @type {ChoiceInputGroup} */ (
|
|
50
|
+
await fixture(html`
|
|
51
|
+
<${parentTag} name="gender[]">
|
|
52
|
+
<${childTag} .choiceValue=${'male'}></${childTag}>
|
|
53
|
+
<${childTag} .choiceValue=${'female'} checked></${childTag}>
|
|
54
|
+
<${childTag} .choiceValue=${'other'}></${childTag}>
|
|
55
|
+
</${parentTag}>
|
|
56
|
+
`)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// @ts-ignore protected members allowed in test
|
|
60
|
+
expect(el._initialModelValue).to.eql('female');
|
|
61
|
+
expect(el.modelValue).to.eql('female');
|
|
62
|
+
|
|
63
|
+
el.formElements[0].checked = true;
|
|
64
|
+
|
|
65
|
+
// @ts-ignore protected members allowed in test
|
|
66
|
+
expect(el._initialModelValue).to.eql('female');
|
|
67
|
+
expect(el.modelValue).to.eql('male');
|
|
68
|
+
});
|
|
69
|
+
|
|
48
70
|
it('has a single modelValue representing the currently checked radio value', async () => {
|
|
49
71
|
const el = /** @type {ChoiceInputGroup} */ (
|
|
50
72
|
await fixture(html`
|
|
@@ -585,6 +607,28 @@ export function runChoiceGroupMixinSuite({ parentTagString, childTagString, choi
|
|
|
585
607
|
});
|
|
586
608
|
|
|
587
609
|
describe('multipleChoice', () => {
|
|
610
|
+
it('has multiple _initialModelValue representing initial checked values', async () => {
|
|
611
|
+
const el = /** @type {ChoiceInputGroup} */ (
|
|
612
|
+
await fixture(html`
|
|
613
|
+
<${parentTag} multiple-choice name="animals[]">
|
|
614
|
+
<${childTag} .choiceValue=${'dog'} checked></${childTag}>
|
|
615
|
+
<${childTag} .choiceValue=${'cat'} checked></${childTag}>
|
|
616
|
+
<${childTag} .choiceValue=${'lion'}></${childTag}>
|
|
617
|
+
</${parentTag}>
|
|
618
|
+
`)
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
// @ts-ignore protected members allowed in test
|
|
622
|
+
expect(el._initialModelValue).to.eql(['dog', 'cat']);
|
|
623
|
+
expect(el.modelValue).to.eql(['dog', 'cat']);
|
|
624
|
+
|
|
625
|
+
el.formElements[2].checked = true;
|
|
626
|
+
|
|
627
|
+
// @ts-ignore protected members allowed in test
|
|
628
|
+
expect(el._initialModelValue).to.eql(['dog', 'cat']);
|
|
629
|
+
expect(el.modelValue).to.eql(['dog', 'cat', 'lion']);
|
|
630
|
+
});
|
|
631
|
+
|
|
588
632
|
it('has a single modelValue representing all currently checked values', async () => {
|
|
589
633
|
const el = /** @type {ChoiceInputGroup} */ (
|
|
590
634
|
await fixture(html`
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { overlays } from './singleton.js';
|
|
2
|
-
import { containFocus } from './utils/contain-focus.js';
|
|
3
2
|
import { deepContains } from './utils/deep-contains.js';
|
|
4
3
|
import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
|
|
5
4
|
import { _adoptStyleUtils } from './utils/adopt-styles.js';
|
|
5
|
+
import { getFocusableElements } from './utils/get-focusable-elements.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
|
|
@@ -113,6 +113,11 @@ const childDialogsClosedInEventLoopWeakmap = new WeakMap();
|
|
|
113
113
|
*
|
|
114
114
|
*/
|
|
115
115
|
export class OverlayController extends EventTarget {
|
|
116
|
+
/**
|
|
117
|
+
* 'True' when Shift key is pressed, 'false' otherwise
|
|
118
|
+
*/
|
|
119
|
+
#isShiftPressed = false;
|
|
120
|
+
|
|
116
121
|
/**
|
|
117
122
|
* @constructor
|
|
118
123
|
* @param {OverlayConfig} config initial config. Will be remembered as shared config
|
|
@@ -201,8 +206,6 @@ export class OverlayController extends EventTarget {
|
|
|
201
206
|
this.__escKeyHandler = this.__escKeyHandler.bind(this);
|
|
202
207
|
this.updateConfig(config);
|
|
203
208
|
/** @private */
|
|
204
|
-
this.__hasActiveTrapsKeyboardFocus = false;
|
|
205
|
-
/** @private */
|
|
206
209
|
this.__hasActiveBackdrop = true;
|
|
207
210
|
/** @private */
|
|
208
211
|
this.__cancelHandler = this.__cancelHandler.bind(this);
|
|
@@ -591,9 +594,7 @@ export class OverlayController extends EventTarget {
|
|
|
591
594
|
* @private
|
|
592
595
|
*/
|
|
593
596
|
__initContentDomStructure() {
|
|
594
|
-
const wrappingDialogElement = document.createElement(
|
|
595
|
-
this.config?._noDialogEl ? 'div' : 'dialog',
|
|
596
|
-
);
|
|
597
|
+
const wrappingDialogElement = document.createElement('dialog');
|
|
597
598
|
// We use a dialog for its visual capabilities: it renders to the top layer.
|
|
598
599
|
// A11y will depend on the type of overlay and is arranged on contentNode level.
|
|
599
600
|
// Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
|
|
@@ -625,7 +626,6 @@ export class OverlayController extends EventTarget {
|
|
|
625
626
|
contentWrapperNodeL2: this.contentWrapperNode,
|
|
626
627
|
contentNodeL3: this.contentNode,
|
|
627
628
|
});
|
|
628
|
-
// @ts-expect-error
|
|
629
629
|
wrappingDialogElement.open = true;
|
|
630
630
|
|
|
631
631
|
if (this.isTooltip) {
|
|
@@ -1145,65 +1145,97 @@ export class OverlayController extends EventTarget {
|
|
|
1145
1145
|
}
|
|
1146
1146
|
}
|
|
1147
1147
|
|
|
1148
|
-
get hasActiveTrapsKeyboardFocus() {
|
|
1149
|
-
return this.__hasActiveTrapsKeyboardFocus;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1148
|
/**
|
|
1153
|
-
* @param {
|
|
1154
|
-
* @protected
|
|
1149
|
+
* @param {KeyboardEvent} event
|
|
1155
1150
|
*/
|
|
1156
|
-
|
|
1157
|
-
if (
|
|
1158
|
-
|
|
1159
|
-
if ('showModal' in this.__wrappingDialogNode) {
|
|
1160
|
-
// @ts-ignore
|
|
1161
|
-
this.__wrappingDialogNode.close();
|
|
1162
|
-
// @ts-ignore
|
|
1163
|
-
this.__wrappingDialogNode.showModal();
|
|
1164
|
-
}
|
|
1165
|
-
// else {
|
|
1166
|
-
this.enableTrapsKeyboardFocus();
|
|
1167
|
-
// }
|
|
1168
|
-
} else if (phase === 'hide' || phase === 'teardown') {
|
|
1169
|
-
this.disableTrapsKeyboardFocus();
|
|
1151
|
+
#isShiftPressedOnKeyDownHandler = event => {
|
|
1152
|
+
if (event.key === 'Shift') {
|
|
1153
|
+
this.#isShiftPressed = true;
|
|
1170
1154
|
}
|
|
1171
|
-
}
|
|
1155
|
+
};
|
|
1172
1156
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
if (
|
|
1178
|
-
this
|
|
1157
|
+
/**
|
|
1158
|
+
* @param {KeyboardEvent} event
|
|
1159
|
+
*/
|
|
1160
|
+
#isShiftPressedOnKeyUpHandler = event => {
|
|
1161
|
+
if (event.key === 'Shift') {
|
|
1162
|
+
this.#isShiftPressed = false;
|
|
1179
1163
|
}
|
|
1164
|
+
};
|
|
1180
1165
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
'[overlays]: For best accessibility (compatibility with Safari + VoiceOver), provide a contentNode that is not a host for a shadow root',
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1166
|
+
#handleShiftKeyPress = () => {
|
|
1167
|
+
window.addEventListener('keydown', this.#isShiftPressedOnKeyDownHandler);
|
|
1168
|
+
window.addEventListener('keyup', this.#isShiftPressedOnKeyUpHandler);
|
|
1169
|
+
};
|
|
1188
1170
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1171
|
+
#stopHandlingShiftKeyPress = () => {
|
|
1172
|
+
window.removeEventListener('keydown', this.#isShiftPressedOnKeyDownHandler);
|
|
1173
|
+
window.removeEventListener('keyup', this.#isShiftPressedOnKeyUpHandler);
|
|
1174
|
+
};
|
|
1195
1175
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1176
|
+
#getInitialElementToFocus = () => {
|
|
1177
|
+
const focusableElements = getFocusableElements(this.contentNode);
|
|
1178
|
+
// Initial focus goes to first element with autofocus, or `contentNode`
|
|
1179
|
+
return focusableElements.find(e => e.hasAttribute('autofocus')) || this.contentNode;
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* When a `dialog` element gets focused, we focus programmatically something
|
|
1184
|
+
* else inside dialog for better a11y. A dialog element gets focused natively in these cases:
|
|
1185
|
+
* 1) When called by `showModal()` first time
|
|
1186
|
+
* 2) When focus is rotating. That is when a user navigates using Tab key through
|
|
1187
|
+
* all the dialog's focusable elements, then the focus goes to the browser's URL,
|
|
1188
|
+
* all its tabs and then the focus goes back to the dialog element
|
|
1189
|
+
* 3) Same as in the point #2, but when a user navigates backward by hitting `Shift + Tab`.
|
|
1190
|
+
* In this case we do not intercept and let the focus pass through. Otherwise the focus
|
|
1191
|
+
* will never leaves the dialog
|
|
1192
|
+
*
|
|
1193
|
+
* Note, Chrome does not focus `Dialog` element when Tabbing. When dialog is opened first time,
|
|
1194
|
+
* it focuses the contentNode if that has `tabindex` set. But the second time when we
|
|
1195
|
+
* move to the dialog from URL bar, nor the dialog element, nor the `contentNode` are focused.
|
|
1196
|
+
* Instead the first focusable element is focused right away
|
|
1197
|
+
*/
|
|
1198
|
+
#handleFocusInsideDialog = () => {
|
|
1199
|
+
this.__wrappingDialogNode?.addEventListener('focus', () => {
|
|
1200
|
+
if (!this.#isShiftPressed) {
|
|
1201
|
+
this.#getInitialElementToFocus().focus();
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* @param {{ phase: OverlayPhase }} config
|
|
1208
|
+
* @protected
|
|
1209
|
+
*/
|
|
1210
|
+
_handleTrapsKeyboardFocus({ phase }) {
|
|
1211
|
+
if (phase === 'init') {
|
|
1212
|
+
this.contentNode.style.outline = 'none';
|
|
1213
|
+
this.contentNode.tabIndex = -1;
|
|
1214
|
+
|
|
1215
|
+
const isContentShadowHost = Boolean(this.contentNode.shadowRoot);
|
|
1216
|
+
if (isContentShadowHost) {
|
|
1217
|
+
// eslint-disable-next-line no-console
|
|
1218
|
+
console.warn(
|
|
1219
|
+
'[overlays]: For best accessibility (compatibility with Safari + VoiceOver), provide a contentNode that is not a host for a shadow root',
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1199
1222
|
}
|
|
1200
|
-
if (
|
|
1201
|
-
this
|
|
1202
|
-
this
|
|
1223
|
+
if (phase === 'show') {
|
|
1224
|
+
this.#handleShiftKeyPress();
|
|
1225
|
+
this.#handleFocusInsideDialog();
|
|
1226
|
+
this.__wrappingDialogNode?.close();
|
|
1227
|
+
this.__wrappingDialogNode?.showModal();
|
|
1228
|
+
/**
|
|
1229
|
+
* At this moment `#handleFocusInsideDialog` should handle the focus.
|
|
1230
|
+
* But for some reason Firefox on the testing setup does not
|
|
1231
|
+
* focus the native `dialog` on showModal() and focuses the first
|
|
1232
|
+
* focusable element inside the dialog instead. Hence here we focus
|
|
1233
|
+
* contentNode explicitly
|
|
1234
|
+
*/
|
|
1235
|
+
this.#getInitialElementToFocus().focus();
|
|
1203
1236
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
this.manager.informTrapsKeyboardFocusGotDisabled({ disabledCtrl: this, findNewTrap });
|
|
1237
|
+
if (phase === 'hide') {
|
|
1238
|
+
this.#stopHandlingShiftKeyPress();
|
|
1207
1239
|
}
|
|
1208
1240
|
}
|
|
1209
1241
|
|
|
@@ -1231,12 +1263,7 @@ export class OverlayController extends EventTarget {
|
|
|
1231
1263
|
return;
|
|
1232
1264
|
}
|
|
1233
1265
|
|
|
1234
|
-
|
|
1235
|
-
event.composedPath().includes(this.contentNode) ||
|
|
1236
|
-
(this.invokerNode && event.composedPath().includes(this.invokerNode)) ||
|
|
1237
|
-
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
|
1238
|
-
|
|
1239
|
-
if (hasPressedInside) {
|
|
1266
|
+
if (this.#hasPressedInside(event)) {
|
|
1240
1267
|
this.__escKeyHandlerCalled = true;
|
|
1241
1268
|
this.hide();
|
|
1242
1269
|
// We could do event.stopPropagation() here, but we don't want to hide info for
|
|
@@ -1246,6 +1273,15 @@ export class OverlayController extends EventTarget {
|
|
|
1246
1273
|
}
|
|
1247
1274
|
}
|
|
1248
1275
|
|
|
1276
|
+
/**
|
|
1277
|
+
* @param {KeyboardEvent} event
|
|
1278
|
+
* @returns {boolean}
|
|
1279
|
+
*/
|
|
1280
|
+
#hasPressedInside = event =>
|
|
1281
|
+
event.composedPath().includes(/** @type {EventTarget} */ (this.__wrappingDialogNode)) ||
|
|
1282
|
+
(this.invokerNode && event.composedPath().includes(this.invokerNode)) ||
|
|
1283
|
+
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
|
1284
|
+
|
|
1249
1285
|
/**
|
|
1250
1286
|
* @param {KeyboardEvent} event
|
|
1251
1287
|
* @returns {void}
|
|
@@ -1253,10 +1289,7 @@ export class OverlayController extends EventTarget {
|
|
|
1253
1289
|
#outsideEscKeyHandler = event => {
|
|
1254
1290
|
if (event.key !== 'Escape') return;
|
|
1255
1291
|
|
|
1256
|
-
|
|
1257
|
-
event.composedPath().includes(this.contentNode) ||
|
|
1258
|
-
deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
|
|
1259
|
-
if (hasPressedInside) return;
|
|
1292
|
+
if (this.#hasPressedInside(event)) return;
|
|
1260
1293
|
this.hide();
|
|
1261
1294
|
};
|
|
1262
1295
|
|
|
@@ -134,39 +134,6 @@ export class OverlaysManager {
|
|
|
134
134
|
return this.__siblingsInert;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
disableTrapsKeyboardFocusForAll() {
|
|
138
|
-
this.shownList.forEach(ctrl => {
|
|
139
|
-
if (ctrl.trapsKeyboardFocus === true && ctrl.disableTrapsKeyboardFocus) {
|
|
140
|
-
ctrl.disableTrapsKeyboardFocus({ findNewTrap: false });
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* @param {'local' | 'global' | undefined} placementMode
|
|
147
|
-
*/
|
|
148
|
-
informTrapsKeyboardFocusGotEnabled(placementMode) {
|
|
149
|
-
if (this.siblingsInert === false && placementMode === 'global') {
|
|
150
|
-
this.__siblingsInert = true;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* @param {{ disabledCtrl?:OverlayController, findNewTrap?:boolean }} options
|
|
156
|
-
*/
|
|
157
|
-
informTrapsKeyboardFocusGotDisabled({ disabledCtrl, findNewTrap = true } = {}) {
|
|
158
|
-
const next = this.shownList.find(
|
|
159
|
-
ctrl => ctrl !== disabledCtrl && ctrl.trapsKeyboardFocus === true,
|
|
160
|
-
);
|
|
161
|
-
if (next) {
|
|
162
|
-
if (findNewTrap) {
|
|
163
|
-
next.enableTrapsKeyboardFocus();
|
|
164
|
-
}
|
|
165
|
-
} else if (this.siblingsInert === true) {
|
|
166
|
-
this.__siblingsInert = false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
137
|
/** PreventsScroll */
|
|
171
138
|
|
|
172
139
|
// eslint-disable-next-line class-methods-use-this
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-new */
|
|
2
|
+
/* eslint-disable lit-a11y/no-autofocus */
|
|
2
3
|
import { OverlayController, overlays } from '@lion/ui/overlays.js';
|
|
3
4
|
import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
|
|
4
5
|
import { sendKeys } from '@web/test-runner-commands';
|
|
@@ -11,13 +12,14 @@ import {
|
|
|
11
12
|
fixture,
|
|
12
13
|
expect,
|
|
13
14
|
html,
|
|
15
|
+
waitUntil,
|
|
14
16
|
} from '@open-wc/testing';
|
|
15
17
|
|
|
16
18
|
import { isActiveElement } from '../../core/test-helpers/isActiveElement.js';
|
|
17
19
|
import { createShadowHost } from '../test-helpers/createShadowHost.js';
|
|
18
20
|
import { _adoptStyleUtils } from '../src/utils/adopt-styles.js';
|
|
19
21
|
import { simulateTab } from '../src/utils/simulate-tab.js';
|
|
20
|
-
import {
|
|
22
|
+
import { browserDetection } from '../../core/src/browserDetection.js';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* @typedef {import('../types/OverlayConfig.js').ViewportPlacement} ViewportPlacement
|
|
@@ -584,30 +586,57 @@ describe('OverlayController', () => {
|
|
|
584
586
|
|
|
585
587
|
describe('Feature Configuration', () => {
|
|
586
588
|
describe('trapsKeyboardFocus', () => {
|
|
587
|
-
it('
|
|
589
|
+
it('focuses the overlay on show', async () => {
|
|
588
590
|
const ctrl = new OverlayController({
|
|
589
591
|
...withGlobalTestConfig(),
|
|
590
592
|
trapsKeyboardFocus: true,
|
|
591
593
|
});
|
|
592
|
-
expect(ctrl.hasActiveTrapsKeyboardFocus).to.be.false;
|
|
593
|
-
|
|
594
594
|
await ctrl.show();
|
|
595
|
-
expect(ctrl.
|
|
595
|
+
expect(isActiveElement(ctrl.contentNode)).to.be.true;
|
|
596
596
|
});
|
|
597
597
|
|
|
598
|
-
it('
|
|
598
|
+
it('keeps focus within the overlay e.g. you can not tab out by accident', async () => {
|
|
599
|
+
const contentNode = /** @type {HTMLElement} */ (
|
|
600
|
+
await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
|
|
601
|
+
);
|
|
599
602
|
const ctrl = new OverlayController({
|
|
600
603
|
...withGlobalTestConfig(),
|
|
601
604
|
trapsKeyboardFocus: true,
|
|
605
|
+
contentNode,
|
|
602
606
|
});
|
|
603
607
|
await ctrl.show();
|
|
604
608
|
|
|
605
|
-
|
|
609
|
+
await fixture(html`<button>click me</button>`);
|
|
610
|
+
const input1 = ctrl.contentNode.querySelectorAll('input')[0];
|
|
611
|
+
const input2 = ctrl.contentNode.querySelectorAll('input')[1];
|
|
612
|
+
input2.focus();
|
|
613
|
+
await sendKeys({ press: 'Tab' });
|
|
614
|
+
|
|
615
|
+
if (browserDetection.isChrome) {
|
|
616
|
+
expect(isActiveElement(document.body)).to.be.true;
|
|
617
|
+
await sendKeys({ press: 'Tab' });
|
|
618
|
+
expect(isActiveElement(input1)).to.be.true;
|
|
619
|
+
}
|
|
620
|
+
if (browserDetection.isFirefox) {
|
|
621
|
+
// For some reason with the headless firefox setup,
|
|
622
|
+
// once we focus on the latest focusable element, `input2` in this case,
|
|
623
|
+
// any further Tab presses keeps the focus on `input2` element
|
|
624
|
+
expect(isActiveElement(input2)).to.be.true;
|
|
625
|
+
await sendKeys({ press: 'Tab' });
|
|
626
|
+
expect(isActiveElement(input2)).to.be.true;
|
|
627
|
+
}
|
|
628
|
+
if (browserDetection.isMacSafari) {
|
|
629
|
+
expect(isActiveElement(document.body)).to.be.true;
|
|
630
|
+
await sendKeys({ press: 'Tab' });
|
|
631
|
+
expect(isActiveElement(ctrl.contentNode)).to.be.true;
|
|
632
|
+
await sendKeys({ press: 'Tab' });
|
|
633
|
+
expect(isActiveElement(input1)).to.be.true;
|
|
634
|
+
}
|
|
606
635
|
});
|
|
607
636
|
|
|
608
|
-
it('
|
|
637
|
+
it('focuses the element with [autofocus] when dialog gets opened', async () => {
|
|
609
638
|
const contentNode = /** @type {HTMLElement} */ (
|
|
610
|
-
await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
|
|
639
|
+
await fixture(html` <div><input id="input1" /><input id="input2" autofocus /></div> `)
|
|
611
640
|
);
|
|
612
641
|
const ctrl = new OverlayController({
|
|
613
642
|
...withGlobalTestConfig(),
|
|
@@ -616,21 +645,21 @@ describe('OverlayController', () => {
|
|
|
616
645
|
});
|
|
617
646
|
await ctrl.show();
|
|
618
647
|
|
|
619
|
-
const elOutside = /** @type {HTMLElement} */ (
|
|
620
|
-
await fixture(html`<button>click me</button>`)
|
|
621
|
-
);
|
|
622
|
-
const input1 = ctrl.contentNode.querySelectorAll('input')[0];
|
|
623
648
|
const input2 = ctrl.contentNode.querySelectorAll('input')[1];
|
|
649
|
+
expect(isActiveElement(input2)).to.be.true;
|
|
650
|
+
});
|
|
624
651
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
652
|
+
it('focuses the contentNode when dialog gets opened and there is no [autofocus]', async () => {
|
|
653
|
+
const contentNode = /** @type {HTMLElement} */ (
|
|
654
|
+
await fixture(html` <div><input id="input1" /><input id="input2" /></div> `)
|
|
655
|
+
);
|
|
656
|
+
const ctrl = new OverlayController({
|
|
657
|
+
...withGlobalTestConfig(),
|
|
658
|
+
trapsKeyboardFocus: true,
|
|
659
|
+
contentNode,
|
|
660
|
+
});
|
|
661
|
+
await ctrl.show();
|
|
662
|
+
expect(isActiveElement(contentNode)).to.be.true;
|
|
634
663
|
});
|
|
635
664
|
|
|
636
665
|
it('allows to move the focus outside of the overlay if trapsKeyboardFocus is disabled', async () => {
|
|
@@ -667,12 +696,10 @@ describe('OverlayController', () => {
|
|
|
667
696
|
|
|
668
697
|
await ctrl0.show();
|
|
669
698
|
await ctrl1.show();
|
|
670
|
-
expect(
|
|
671
|
-
expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.true;
|
|
699
|
+
expect(isActiveElement(ctrl1.contentNode)).to.be.true;
|
|
672
700
|
|
|
673
701
|
await ctrl1.hide();
|
|
674
|
-
expect(ctrl0.
|
|
675
|
-
expect(ctrl1.hasActiveTrapsKeyboardFocus).to.be.false;
|
|
702
|
+
expect(isActiveElement(ctrl0.contentNode)).to.be.true;
|
|
676
703
|
});
|
|
677
704
|
|
|
678
705
|
it('warns when contentNode is a host for a shadowRoot', async () => {
|
|
@@ -811,12 +838,8 @@ describe('OverlayController', () => {
|
|
|
811
838
|
);
|
|
812
839
|
const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
|
|
813
840
|
await mimicEscapePress(childOverlay.contentNode);
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
await aTimeout(100);
|
|
817
|
-
|
|
818
|
-
expect(parentOverlay.isShown).to.be.false;
|
|
819
|
-
expect(childOverlay.isShown).to.be.true;
|
|
841
|
+
await waitUntil(() => !parentOverlay.isShown);
|
|
842
|
+
await waitUntil(() => childOverlay.isShown);
|
|
820
843
|
|
|
821
844
|
await childOverlay.teardown();
|
|
822
845
|
await parentOverlay.teardown();
|