@lion/ui 0.18.0-menu-system.1 → 0.18.0-menu-system.4
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/menu/src/InteractiveListMixin.js +1 -1
- package/components/menu/src/MoreButtonMenuMixin.js +82 -92
- package/components/menu/src/MultiLevelListMixin.js +1 -1
- package/components/navigation-bar/src/LionNavigationBar.js +2 -2
- package/components/overlays/src/OverlayController.js +49 -14
- package/components/overlays/test/OverlayController.test.js +122 -11
- package/components/overlays/types/ArrowMixinTypes.ts +3 -1
- package/components/overlays/types/OverlayConfig.ts +5 -1
- package/custom-elements.json +113 -15
- package/dist-types/components/dialog/src/LionDialog.d.ts +3 -1
- package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts +3 -1
- package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +11 -2
- package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -1
- package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +4 -4
- package/dist-types/components/overlays/src/OverlayController.d.ts +16 -0
- package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
- package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
- package/dist-types/components/overlays/types/OverlayConfig.d.ts +3 -1
- package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts +3 -1
- package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +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-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/menu/extensions.md +27 -0
- 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/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/api-table.md +36 -34
- 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/package.json +1 -1
|
@@ -457,7 +457,7 @@ const InteractiveListMixinImplementation = superclass =>
|
|
|
457
457
|
* menuitemradios or -checkboxes.
|
|
458
458
|
* When no role at all, we might deal with a wrapped node (for instance triggering a
|
|
459
459
|
* submenu)
|
|
460
|
-
* Alternatively, when _activeMode is ['disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
|
|
460
|
+
* Alternatively, when _activeMode is ['tabbable-disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
|
|
461
461
|
* we search for a[href]
|
|
462
462
|
*/
|
|
463
463
|
if (node.getAttribute('role') === 'group') {
|
|
@@ -1,62 +1,31 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
:host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'] > button,
|
|
32
|
-
:host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'] > a,
|
|
33
|
-
:host([responsive-mode='desktop'])
|
|
34
|
-
[level='1']
|
|
35
|
-
> [slot='list']
|
|
36
|
-
> [data-more-button-wrapper]
|
|
37
|
-
> [role='listitem']
|
|
38
|
-
> button,
|
|
39
|
-
:host([responsive-mode='desktop'])
|
|
40
|
-
[level='1']
|
|
41
|
-
> [slot='list']
|
|
42
|
-
> [data-more-button-wrapper]
|
|
43
|
-
> [role='listitem']
|
|
44
|
-
> a {
|
|
45
|
-
white-space: nowrap;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* TODO listitem rigth margin/gap is not taken at the account ATM
|
|
49
|
-
*/
|
|
50
|
-
/**
|
|
51
|
-
* More button relies on the fact that the menu items do not break the line
|
|
52
|
-
* TODO is it optimal?
|
|
53
|
-
*/
|
|
54
|
-
:host([responsive-mode='desktop']) > [role='list'] > [role='listitem'] > button,
|
|
55
|
-
:host([responsive-mode='desktop']) > [role='list'] > [role='listitem'] > a {
|
|
56
|
-
white-space: nowrap;
|
|
57
|
-
}
|
|
58
|
-
</style>
|
|
59
|
-
`;
|
|
1
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
+
import { css } from 'lit';
|
|
3
|
+
|
|
4
|
+
const styles = css`
|
|
5
|
+
/** More button is hidden by default */
|
|
6
|
+
[data-more-button-wrapper] {
|
|
7
|
+
display: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
[data-more-button-menu][data-open]:not(:has([role='listitem'] [aria-expanded='true'])) {
|
|
11
|
+
position: absolute;
|
|
12
|
+
overflow: visible;
|
|
13
|
+
height: auto;
|
|
14
|
+
width: auto;
|
|
15
|
+
top: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** N.B. this is a simplified version of sr-only. when we move to overlayctrl, we should build this into overlayctrl */
|
|
19
|
+
[data-more-button-menu] {
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
height: 1px;
|
|
22
|
+
width: 1px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[role='list']:has(> [data-more-button-wrapper]) > [role='listitem'] > :is(button, a) {
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
60
29
|
|
|
61
30
|
// @ts-ignore - JS mixin typing
|
|
62
31
|
export const MoreButtonMenuMixin = superclass =>
|
|
@@ -85,7 +54,7 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
85
54
|
// eslint-disable-next-line class-methods-use-this
|
|
86
55
|
_getDeepActiveElement() {
|
|
87
56
|
let host = document.activeElement || document.body;
|
|
88
|
-
while (host
|
|
57
|
+
while (host?.shadowRoot?.activeElement) {
|
|
89
58
|
host = host.shadowRoot.activeElement;
|
|
90
59
|
}
|
|
91
60
|
return host;
|
|
@@ -94,7 +63,7 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
94
63
|
_createMoreButtonWrapper() {
|
|
95
64
|
const moreButtonWrapper = document.createElement('div');
|
|
96
65
|
const style = document.createElement('style');
|
|
97
|
-
style.textContent = styles;
|
|
66
|
+
style.textContent = styles.cssText;
|
|
98
67
|
moreButtonWrapper.appendChild(style);
|
|
99
68
|
moreButtonWrapper.setAttribute('data-more-button-wrapper', '');
|
|
100
69
|
[...this.getMoreButtonSlotProjection().childNodes].forEach(node =>
|
|
@@ -107,32 +76,38 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
107
76
|
const moreButtonMenu = document.createElement('div');
|
|
108
77
|
moreButtonMenu.setAttribute('data-more-button-menu', '');
|
|
109
78
|
moreButtonWrapper.appendChild(moreButtonMenu);
|
|
110
|
-
moreButtonMenu.setAttribute('role', 'none');
|
|
111
79
|
this._listNode.appendChild(moreButtonWrapper);
|
|
112
80
|
|
|
113
|
-
const isElementDirectFocusableItemUnderMoreButtonMenu = element =>
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
element?.parentElement?.parentElement === moreButtonMenu &&
|
|
117
|
-
(targetTagName === 'BUTTON' || targetTagName === 'A')
|
|
118
|
-
);
|
|
119
|
-
};
|
|
81
|
+
const isElementDirectFocusableItemUnderMoreButtonMenu = (/** @type {Element} */ element) =>
|
|
82
|
+
this.listItems.includes(element) && moreButtonMenu.contains(element);
|
|
120
83
|
|
|
121
|
-
moreButtonMenu.addEventListener(
|
|
122
|
-
|
|
123
|
-
|
|
84
|
+
moreButtonMenu.addEventListener(
|
|
85
|
+
'focusin',
|
|
86
|
+
/** @type {FocusEvent} */ event => {
|
|
87
|
+
const target = /** @type {HTMLElement} */ (event?.target);
|
|
88
|
+
if (!isElementDirectFocusableItemUnderMoreButtonMenu(target)) return;
|
|
89
|
+
|
|
90
|
+
// A user did `Shift + Tab` from L2 menu to More menu
|
|
124
91
|
if (target?.getAttribute('aria-expanded') === 'true') {
|
|
92
|
+
// close L2 menu
|
|
125
93
|
target?.click();
|
|
126
94
|
}
|
|
127
95
|
moreButton?.setAttribute('aria-expanded', 'true');
|
|
128
|
-
|
|
129
|
-
|
|
96
|
+
moreButtonMenu.setAttribute('data-open', '');
|
|
97
|
+
},
|
|
98
|
+
);
|
|
130
99
|
|
|
131
|
-
moreButtonMenu.addEventListener('focusout',
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
moreButtonMenu.addEventListener('focusout', () => {
|
|
101
|
+
// We need this timeout for Safari/WebKit.
|
|
102
|
+
// Unfortunately, an in progress click gets canceled when overflow:hidden is applied.
|
|
103
|
+
// Although mousedown would work instead, the consuming developer should have the freedom to apply all possible interaction pattterns.
|
|
104
|
+
// Especially, we should be compatible with 'withClickInteraction'
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
if (isElementDirectFocusableItemUnderMoreButtonMenu(this._getDeepActiveElement())) return;
|
|
134
107
|
moreButton?.setAttribute('aria-expanded', 'false');
|
|
135
|
-
|
|
108
|
+
|
|
109
|
+
moreButtonMenu.removeAttribute('data-open');
|
|
110
|
+
}, 100);
|
|
136
111
|
});
|
|
137
112
|
|
|
138
113
|
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
|
|
@@ -140,22 +115,26 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
140
115
|
moreButton?.addEventListener('mousedown', () => {
|
|
141
116
|
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
|
|
142
117
|
const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
|
|
143
|
-
':scope > [role="listitem"] > a,
|
|
118
|
+
':scope > [role="listitem"] > :is(a, button)',
|
|
144
119
|
);
|
|
145
120
|
|
|
146
|
-
[
|
|
121
|
+
// @ts-ignore Type 'NodeListOf<Element>' must have a '[Symbol.iterator]()' method that returns an iterator.
|
|
122
|
+
for (const focusableFirstLevelItem of focusableFirstLevelItems) {
|
|
147
123
|
if (this._getDeepActiveElement() === focusableFirstLevelItem) {
|
|
148
124
|
this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
|
|
149
125
|
}
|
|
150
|
-
}
|
|
126
|
+
}
|
|
151
127
|
});
|
|
152
128
|
|
|
153
129
|
moreButton?.addEventListener('click', () => {
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
130
|
+
if (this.hasMoreButtonMenuAnyFocusedFirstLevelItems) return;
|
|
131
|
+
/** @type {HTMLElement | null} */
|
|
132
|
+
const firstInteractiveItem = moreButtonMenu.querySelector(
|
|
133
|
+
':scope > [role="listitem"] > :is(a, button)',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
firstInteractiveItem?.focus();
|
|
137
|
+
moreButtonMenu.setAttribute('data-open', '');
|
|
159
138
|
});
|
|
160
139
|
}
|
|
161
140
|
|
|
@@ -232,13 +211,13 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
232
211
|
return fragment;
|
|
233
212
|
};
|
|
234
213
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
214
|
+
/**
|
|
215
|
+
* @param {{ moreButtonMenuElement: HTMLButtonElement, listItem: HTMLElement }} opts
|
|
216
|
+
*/
|
|
217
|
+
moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem }) {
|
|
240
218
|
const fragment = this.getListItemWithAdjacentNodes(listItem);
|
|
241
219
|
moreButtonMenuElement.prepend(fragment);
|
|
220
|
+
// eslint-disable-next-line no-param-reassign
|
|
242
221
|
listItem.style.display = '';
|
|
243
222
|
this.displayMoreButton();
|
|
244
223
|
}
|
|
@@ -268,9 +247,15 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
268
247
|
return hiddenItemsCount;
|
|
269
248
|
}
|
|
270
249
|
|
|
250
|
+
/**
|
|
251
|
+
* @param {number} hiddenItemsCount
|
|
252
|
+
*/
|
|
271
253
|
moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount) {
|
|
254
|
+
const moreButtonMenuElement = this.getMoreButtonMenu();
|
|
255
|
+
const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
|
|
272
256
|
for (let i = 0; i < hiddenItemsCount; i += 1) {
|
|
273
|
-
|
|
257
|
+
const listItem = listItems[listItems.length - 1 - i];
|
|
258
|
+
this.moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem });
|
|
274
259
|
}
|
|
275
260
|
}
|
|
276
261
|
|
|
@@ -279,6 +264,8 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
279
264
|
return;
|
|
280
265
|
}
|
|
281
266
|
|
|
267
|
+
this.style.visibility = 'hidden';
|
|
268
|
+
|
|
282
269
|
this.__resizeTimeout = setTimeout(() => {
|
|
283
270
|
this.__resizeTimeout = null;
|
|
284
271
|
|
|
@@ -286,12 +273,15 @@ export const MoreButtonMenuMixin = superclass =>
|
|
|
286
273
|
this.hideMoreButton();
|
|
287
274
|
|
|
288
275
|
if (this.doItemsFit()) {
|
|
276
|
+
this.style.visibility = '';
|
|
289
277
|
return;
|
|
290
278
|
}
|
|
291
279
|
|
|
280
|
+
this.style.visibility = 'hidden';
|
|
292
281
|
this.displayMoreButton();
|
|
293
282
|
const hiddenItemsCount = this.hideItemsOneByOneInMainMenuUntilTheyFit();
|
|
294
283
|
this.moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount);
|
|
284
|
+
this.style.visibility = '';
|
|
295
285
|
}, 50);
|
|
296
286
|
}
|
|
297
287
|
};
|
|
@@ -214,7 +214,7 @@ const MultiLevelListMixinImplementation = superclass =>
|
|
|
214
214
|
if (
|
|
215
215
|
this.orientation === 'horizontal' &&
|
|
216
216
|
subListOfActiveItem &&
|
|
217
|
-
this._activeMode !== 'disclosure'
|
|
217
|
+
this._activeMode !== 'tabbable-disclosure'
|
|
218
218
|
) {
|
|
219
219
|
this.activeItem.click();
|
|
220
220
|
}
|
|
@@ -199,7 +199,7 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
199
199
|
openableConfig: {
|
|
200
200
|
// We want to do the absolute positioning ourselves, popper is not suited for this...
|
|
201
201
|
placementMode: 'custom',
|
|
202
|
-
|
|
202
|
+
isActivated: false,
|
|
203
203
|
hidesOnOutsideClick: false,
|
|
204
204
|
inheritsReferenceWidth: 'none',
|
|
205
205
|
},
|
|
@@ -231,7 +231,7 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
|
|
|
231
231
|
// We want disclosure behavior
|
|
232
232
|
placementMode: 'custom',
|
|
233
233
|
// N.B. we don't have an invoker...
|
|
234
|
-
|
|
234
|
+
isActivated: false,
|
|
235
235
|
inheritsReferenceWidth: 'none',
|
|
236
236
|
focusContentOnOpen: true,
|
|
237
237
|
},
|
|
@@ -237,8 +237,7 @@ export class OverlayController extends EventTarget {
|
|
|
237
237
|
placement: 'center',
|
|
238
238
|
},
|
|
239
239
|
zIndex: 9999,
|
|
240
|
-
|
|
241
|
-
isOpenable: true,
|
|
240
|
+
isActivated: true,
|
|
242
241
|
focusContentOnOpen: false,
|
|
243
242
|
// This means that content will have visually-hidden / sr-only styles. This is handy for:
|
|
244
243
|
// - hidden menus that should be "indexable" by screen readers (like the Links list in VO: https://support.apple.com/en-gb/guide/voiceover/mchlp2719/mac)
|
|
@@ -247,6 +246,11 @@ export class OverlayController extends EventTarget {
|
|
|
247
246
|
// @ts-ignore - hideVisually is an extension property
|
|
248
247
|
hideVisually: false,
|
|
249
248
|
requireConnectedNodes: true,
|
|
249
|
+
|
|
250
|
+
// hides children overlays when parent is closed...
|
|
251
|
+
syncChildrenCloseState: false,
|
|
252
|
+
// // In next major, we remove this prop. Now we disable it for backwards compatibility and enable it in the places we need it internally
|
|
253
|
+
// _shouldTeardownDomStructure: false,
|
|
250
254
|
};
|
|
251
255
|
|
|
252
256
|
/** @protected */
|
|
@@ -272,7 +276,7 @@ export class OverlayController extends EventTarget {
|
|
|
272
276
|
this.__escKeyHandlerCalled = false;
|
|
273
277
|
}
|
|
274
278
|
|
|
275
|
-
|
|
279
|
+
__hasSetup = false;
|
|
276
280
|
|
|
277
281
|
/**
|
|
278
282
|
* The invokerNode
|
|
@@ -540,7 +544,7 @@ export class OverlayController extends EventTarget {
|
|
|
540
544
|
},
|
|
541
545
|
};
|
|
542
546
|
|
|
543
|
-
const shouldUpdate = !this
|
|
547
|
+
const shouldUpdate = !this.__hasSetup || !isEqualConfig(prevConfig, newConfig);
|
|
544
548
|
if (!shouldUpdate) return;
|
|
545
549
|
// Teardown all previous configs
|
|
546
550
|
|
|
@@ -592,7 +596,7 @@ export class OverlayController extends EventTarget {
|
|
|
592
596
|
* @protected
|
|
593
597
|
*/
|
|
594
598
|
_init() {
|
|
595
|
-
if (!this.config.
|
|
599
|
+
if (!this.config.isActivated) return;
|
|
596
600
|
|
|
597
601
|
// TODO: should not be behind a flag when we are fully tearing down...
|
|
598
602
|
// TODO 2: When we move away from dialog (use popover to paint to top layer)
|
|
@@ -627,7 +631,7 @@ export class OverlayController extends EventTarget {
|
|
|
627
631
|
// Use event delegation to listen for clicks on close buttons...
|
|
628
632
|
this.contentNode.addEventListener('click', this.#hideOnCloseButtonClick);
|
|
629
633
|
|
|
630
|
-
this
|
|
634
|
+
this.__hasSetup = true;
|
|
631
635
|
}
|
|
632
636
|
|
|
633
637
|
#hideOnCloseButtonClick = (/** @type {{ target: any; }} */ ev) => {
|
|
@@ -981,7 +985,7 @@ export class OverlayController extends EventTarget {
|
|
|
981
985
|
*/
|
|
982
986
|
async hide() {
|
|
983
987
|
// Function like a no-op for dynamic edge cases...
|
|
984
|
-
if (!this.config.
|
|
988
|
+
if (!this.config.isActivated) return;
|
|
985
989
|
|
|
986
990
|
this._hideComplete = new Promise(resolve => {
|
|
987
991
|
this._hideResolve = resolve;
|
|
@@ -1135,24 +1139,53 @@ export class OverlayController extends EventTarget {
|
|
|
1135
1139
|
if (this.visibilityTriggerFunction) {
|
|
1136
1140
|
this._handleVisibilityTriggers({ phase });
|
|
1137
1141
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
this.contentNode?.setAttribute('tabindex', '-1');
|
|
1142
|
+
if (this.config.syncChildrenCloseState) {
|
|
1143
|
+
this._handleSyncChildrenCloseState({ phase });
|
|
1141
1144
|
}
|
|
1145
|
+
if (this.config.focusContentOnOpen) {
|
|
1146
|
+
this._handleFocusContentOnOpen({ phase });
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1142
1149
|
|
|
1143
|
-
|
|
1150
|
+
/**
|
|
1151
|
+
* @param {{phase: OverlayPhase}} opts
|
|
1152
|
+
* @returns {void}
|
|
1153
|
+
*/
|
|
1154
|
+
_handleFocusContentOnOpen({ phase }) {
|
|
1155
|
+
if (phase === 'init') {
|
|
1156
|
+
this.contentNode?.setAttribute('tabindex', '-1');
|
|
1157
|
+
} else if (phase === 'teardown') {
|
|
1144
1158
|
// TODO: capture initial attr, use Resettable mechanism of VisibilityToggleCtrl in whole Controller
|
|
1145
1159
|
this.contentNode?.removeAttribute('tabindex');
|
|
1146
1160
|
}
|
|
1147
1161
|
}
|
|
1148
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* @param {{phase: OverlayPhase}} opts
|
|
1165
|
+
* @returns {void}
|
|
1166
|
+
*/
|
|
1167
|
+
_handleSyncChildrenCloseState({ phase }) {
|
|
1168
|
+
if (phase !== 'hide') return;
|
|
1169
|
+
|
|
1170
|
+
const visibleChildren = this.manager.shownList.filter(
|
|
1171
|
+
ctrl => ctrl !== this && deepContains(this.contentNode, ctrl.contentNode),
|
|
1172
|
+
);
|
|
1173
|
+
visibleChildren.forEach(ctrl => ctrl.hide());
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1149
1176
|
/**
|
|
1150
1177
|
* @param {{ phase: OverlayPhase }} config
|
|
1151
1178
|
*/
|
|
1152
1179
|
_handleVisibilityTriggers({ phase }) {
|
|
1153
1180
|
if (typeof this.visibilityTriggerFunction !== 'function') return;
|
|
1154
1181
|
|
|
1155
|
-
|
|
1182
|
+
// Here we initialize the __visibilityTriggerHandler of our invokerNode. It's important that we ONLY do this on init,
|
|
1183
|
+
// otherwise we risk not being able to properly clean up listeners...
|
|
1184
|
+
if (phase === 'init') {
|
|
1185
|
+
this.__visibilityTriggerHandler = this.visibilityTriggerFunction({ controller: this });
|
|
1186
|
+
}
|
|
1187
|
+
// Here we run the appropriate lifecycle, if defined in our handler
|
|
1188
|
+
this.__visibilityTriggerHandler[phase]?.();
|
|
1156
1189
|
}
|
|
1157
1190
|
|
|
1158
1191
|
/**
|
|
@@ -1562,7 +1595,7 @@ export class OverlayController extends EventTarget {
|
|
|
1562
1595
|
}
|
|
1563
1596
|
|
|
1564
1597
|
teardown() {
|
|
1565
|
-
if (!this
|
|
1598
|
+
if (!this.__hasSetup) return;
|
|
1566
1599
|
this.__handleOverlayStyles({ phase: 'teardown' });
|
|
1567
1600
|
if (this.isShown) {
|
|
1568
1601
|
this._keepBodySize({ phase: 'teardown' });
|
|
@@ -1574,10 +1607,12 @@ export class OverlayController extends EventTarget {
|
|
|
1574
1607
|
|
|
1575
1608
|
this.contentNode?.removeEventListener('click', this.#hideOnCloseButtonClick);
|
|
1576
1609
|
|
|
1610
|
+
// if (this.config._shouldTeardownDomStructure) {
|
|
1577
1611
|
this.__rearrangeNodesCleanup?.();
|
|
1612
|
+
// }
|
|
1578
1613
|
this.__teardownVisibility();
|
|
1579
1614
|
|
|
1580
|
-
this
|
|
1615
|
+
this.__hasSetup = false;
|
|
1581
1616
|
}
|
|
1582
1617
|
|
|
1583
1618
|
/** @private */
|
|
@@ -115,26 +115,35 @@ const withLocalTestConfig = () =>
|
|
|
115
115
|
*/
|
|
116
116
|
async function createNestedEscControllers(parentContent) {
|
|
117
117
|
const childContent = /** @type {HTMLDivElement} */ (parentContent.querySelector('div[id]'));
|
|
118
|
-
// Assert valid fixure
|
|
119
|
-
const isValidFixture =
|
|
120
|
-
(parentContent.id.startsWith('parent-overlay--hidesOnEsc') ||
|
|
121
|
-
parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc')) &&
|
|
122
|
-
(childContent.id.startsWith('child-overlay--hidesOnEsc') ||
|
|
123
|
-
childContent.id.startsWith('child-overlay--hidesOnOutsideEsc'));
|
|
124
|
-
if (!isValidFixture) {
|
|
125
|
-
throw new Error('Provide a valid fixture');
|
|
126
|
-
}
|
|
127
118
|
|
|
128
119
|
if (parentContent.hasAttribute('data-convert-to-shadow-root')) {
|
|
129
120
|
const shadowRootParent = parentContent.attachShadow({ mode: 'open' });
|
|
130
121
|
shadowRootParent.appendChild(childContent);
|
|
131
122
|
}
|
|
132
123
|
|
|
124
|
+
const parentConfig = {};
|
|
125
|
+
const childConfig = {};
|
|
126
|
+
|
|
133
127
|
const parentHasOutsideOnEsc = parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc');
|
|
134
128
|
const childHasOutsideOnEsc = childContent.id.startsWith('child-overlay--hidesOnOutsideEsc');
|
|
135
129
|
|
|
136
|
-
|
|
137
|
-
|
|
130
|
+
if (parentHasOutsideOnEsc) {
|
|
131
|
+
parentConfig.hidesOnOutsideEsc = true;
|
|
132
|
+
} else {
|
|
133
|
+
parentConfig.hidesOnEsc = true;
|
|
134
|
+
}
|
|
135
|
+
if (childHasOutsideOnEsc) {
|
|
136
|
+
childConfig.hidesOnOutsideEsc = true;
|
|
137
|
+
} else {
|
|
138
|
+
childConfig.hidesOnEsc = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const parentHasSyncChildrenCloseState =
|
|
142
|
+
parentContent.dataset.syncChildrenCloseState !== undefined;
|
|
143
|
+
|
|
144
|
+
if (parentHasSyncChildrenCloseState) {
|
|
145
|
+
parentConfig.syncChildrenCloseState = true;
|
|
146
|
+
}
|
|
138
147
|
|
|
139
148
|
const parentOverlay = new OverlayController({
|
|
140
149
|
...withGlobalTestConfig(),
|
|
@@ -347,6 +356,21 @@ describe('OverlayController', () => {
|
|
|
347
356
|
await ctrl.hide();
|
|
348
357
|
expect(ctrl.isShown).to.be.false;
|
|
349
358
|
});
|
|
359
|
+
|
|
360
|
+
// it('cleans up the dom structure it created', async () => {
|
|
361
|
+
// const contentNode = /** @type {HTMLElement} */ (fixtureSync(html`<div>my content</div>`));
|
|
362
|
+
// const ctrl = new OverlayController({
|
|
363
|
+
// placementMode: 'global',
|
|
364
|
+
// contentNode,
|
|
365
|
+
// });
|
|
366
|
+
// await ctrl.show();
|
|
367
|
+
// expect(ctrl.contentWrapperNode).to.exist;
|
|
368
|
+
// expect(ctrl.__wrappingDialogNode).to.exist;
|
|
369
|
+
|
|
370
|
+
// await ctrl.teardown();
|
|
371
|
+
// expect(ctrl.contentWrapperNode).to.not.exist;
|
|
372
|
+
// expect(ctrl.__wrappingDialogNode).to.not.exist;
|
|
373
|
+
// });
|
|
350
374
|
});
|
|
351
375
|
|
|
352
376
|
describe('Node Configuration', () => {
|
|
@@ -621,6 +645,7 @@ describe('OverlayController', () => {
|
|
|
621
645
|
});
|
|
622
646
|
});
|
|
623
647
|
|
|
648
|
+
// TODO: make features modular (so they can be treeshaken) and test their lifecycle within this desrcibe
|
|
624
649
|
describe('Feature Configuration', () => {
|
|
625
650
|
describe('trapsKeyboardFocus', () => {
|
|
626
651
|
it('focuses the overlay on show', async () => {
|
|
@@ -983,6 +1008,7 @@ describe('OverlayController', () => {
|
|
|
983
1008
|
});
|
|
984
1009
|
});
|
|
985
1010
|
});
|
|
1011
|
+
|
|
986
1012
|
describe('hidesOnOutsideClick', () => {
|
|
987
1013
|
it('hides on outside click', async () => {
|
|
988
1014
|
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
|
|
@@ -1703,6 +1729,91 @@ describe('OverlayController', () => {
|
|
|
1703
1729
|
expect(ctrl1.hasActiveBackdrop).to.be.true;
|
|
1704
1730
|
});
|
|
1705
1731
|
});
|
|
1732
|
+
|
|
1733
|
+
describe('focusContentOnOpen', () => {
|
|
1734
|
+
it('adds tabindex="-1" to the content node when focusContentOnOpen is true', async () => {
|
|
1735
|
+
const ctrl = new OverlayController({
|
|
1736
|
+
...withGlobalTestConfig(),
|
|
1737
|
+
isBlocking: false,
|
|
1738
|
+
focusContentOnOpen: true,
|
|
1739
|
+
});
|
|
1740
|
+
const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
|
|
1741
|
+
ctrl.updateConfig({ contentNode });
|
|
1742
|
+
await ctrl.show();
|
|
1743
|
+
expect(contentNode.getAttribute('tabindex')).to.equal('-1');
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
it('makes contentNode the root of "next tab flow"', async () => {
|
|
1747
|
+
const ctrl = new OverlayController({
|
|
1748
|
+
...withGlobalTestConfig(),
|
|
1749
|
+
isBlocking: false,
|
|
1750
|
+
focusContentOnOpen: true,
|
|
1751
|
+
});
|
|
1752
|
+
const contentNode = /** @type {HTMLElement} */ (
|
|
1753
|
+
await fixture('<div><button>Button</button></div>')
|
|
1754
|
+
);
|
|
1755
|
+
ctrl.updateConfig({ contentNode });
|
|
1756
|
+
await ctrl.show();
|
|
1757
|
+
const button = /** @type {HTMLButtonElement} */ (contentNode.querySelector('button'));
|
|
1758
|
+
button.focus();
|
|
1759
|
+
expect(isActiveElement(button)).to.be.true;
|
|
1760
|
+
});
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
describe('syncChildrenCloseState', () => {
|
|
1764
|
+
it('does not close all children of nested controllers when syncChildrenCloseState is false', async () => {
|
|
1765
|
+
const parentContent = /** @type {HTMLDivElement} */ (
|
|
1766
|
+
await fixture(
|
|
1767
|
+
html` <!-- -->
|
|
1768
|
+
<div data-sync-children-close-state id="parent-overlay">
|
|
1769
|
+
<div id="child-overlay">we open our child and it will close when parent does</div>
|
|
1770
|
+
</div>`,
|
|
1771
|
+
)
|
|
1772
|
+
);
|
|
1773
|
+
const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
|
|
1774
|
+
expect(parentOverlay.isShown).to.be.true;
|
|
1775
|
+
expect(childOverlay.isShown).to.be.true;
|
|
1776
|
+
|
|
1777
|
+
await parentOverlay.hide();
|
|
1778
|
+
await childOverlay._hideComplete;
|
|
1779
|
+
expect(parentOverlay.isShown).to.be.false;
|
|
1780
|
+
expect(childOverlay.isShown).to.be.false;
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
it('does not close all children of nested controllers when syncChildrenCloseState is false', async () => {
|
|
1784
|
+
const parentContent = /** @type {HTMLDivElement} */ (
|
|
1785
|
+
await fixture(
|
|
1786
|
+
html` <!-- -->
|
|
1787
|
+
<div id="parent-overlay">
|
|
1788
|
+
<div id="child-overlay">
|
|
1789
|
+
we open our child and it will not close when parent does
|
|
1790
|
+
</div>
|
|
1791
|
+
</div>`,
|
|
1792
|
+
)
|
|
1793
|
+
);
|
|
1794
|
+
const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
|
|
1795
|
+
expect(parentOverlay.isShown).to.be.true;
|
|
1796
|
+
expect(childOverlay.isShown).to.be.true;
|
|
1797
|
+
|
|
1798
|
+
await parentOverlay.hide();
|
|
1799
|
+
expect(parentOverlay.isShown).to.be.false;
|
|
1800
|
+
expect(childOverlay.isShown).to.be.true;
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
describe('isActivated', () => {
|
|
1805
|
+
it('allows to conditionally disable overlay/openable functionality (for responsive context like nav menus, accordions/collapsibles vs plain headings/content etc.)', async () => {
|
|
1806
|
+
const ctrl = new OverlayController({
|
|
1807
|
+
...withGlobalTestConfig(),
|
|
1808
|
+
isActivated: false,
|
|
1809
|
+
});
|
|
1810
|
+
expect(ctrl.__hasSetup).to.be.false;
|
|
1811
|
+
ctrl.updateConfig({
|
|
1812
|
+
isActivated: true,
|
|
1813
|
+
});
|
|
1814
|
+
expect(ctrl.__hasSetup).to.be.true;
|
|
1815
|
+
});
|
|
1816
|
+
});
|
|
1706
1817
|
});
|
|
1707
1818
|
|
|
1708
1819
|
describe('Show / Hide / Toggle', () => {
|
|
@@ -26,7 +26,9 @@ export declare class ArrowHost {
|
|
|
26
26
|
|
|
27
27
|
protected _defineOverlayConfig(): OverlayConfig;
|
|
28
28
|
|
|
29
|
-
protected _getPopperArrowConfig(
|
|
29
|
+
protected _getPopperArrowConfig(
|
|
30
|
+
popperConfigToExtendFrom: Partial<PopperOptions>,
|
|
31
|
+
): Partial<PopperOptions>;
|
|
30
32
|
|
|
31
33
|
private __setupRepositionCompletePromise(): void;
|
|
32
34
|
|
|
@@ -111,11 +111,15 @@ export interface OverlayConfig {
|
|
|
111
111
|
* By default, we function either as overlay or disclosure. In some cases, this should be dynamically set (think of responsive changes).
|
|
112
112
|
* or it depends on the context (a top level menu is not openable (unless it's a context menu), its children menus are).
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
isActivated?: boolean;
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* By default, an error is thrown when contentNode is not connected to the DOM.
|
|
118
118
|
* This allows to prepare nodes and later connect them to dom.
|
|
119
119
|
*/
|
|
120
120
|
requireConnectedNodes?: boolean;
|
|
121
|
+
|
|
122
|
+
syncChildrenCloseState?: boolean;
|
|
123
|
+
|
|
124
|
+
_shouldTeardownDomStructure?: boolean;
|
|
121
125
|
}
|