@limetech/lime-elements 40.11.2 → 40.12.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/CHANGELOG.md +14 -0
- package/dist/cjs/{focus-trigger-element-BfnJmxKO.js → focus-trigger-element-Sr7Yjtmk.js} +6 -17
- package/dist/cjs/get-focus-target-DgqZ25gx.js +105 -0
- package/dist/cjs/lime-elements.cjs.js +1 -1
- package/dist/cjs/limel-breadcrumbs_6.cjs.entry.js +2 -1
- package/dist/cjs/limel-file-viewer.cjs.entry.js +81 -47
- package/dist/cjs/limel-form.cjs.entry.js +7 -7
- package/dist/cjs/limel-hotkey_4.cjs.entry.js +2 -71
- package/dist/cjs/limel-popover_2.cjs.entry.js +2 -1
- package/dist/cjs/limel-side-panel.cjs.entry.js +72 -5
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/side-panel/side-panel.css +118 -6
- package/dist/collection/components/side-panel/side-panel.js +120 -77
- package/dist/collection/util/focus-trigger-element.js +6 -17
- package/dist/collection/util/get-focus-target.js +47 -16
- package/dist/esm/{focus-trigger-element-BOFcjLq3.js → focus-trigger-element-CIEHI_yk.js} +6 -17
- package/dist/esm/get-focus-target-BhoYaGlg.js +102 -0
- package/dist/esm/lime-elements.js +1 -1
- package/dist/esm/limel-breadcrumbs_6.entry.js +2 -1
- package/dist/esm/limel-file-viewer.entry.js +81 -47
- package/dist/esm/limel-form.entry.js +7 -7
- package/dist/esm/limel-hotkey_4.entry.js +1 -70
- package/dist/esm/limel-popover_2.entry.js +2 -1
- package/dist/esm/limel-side-panel.entry.js +72 -5
- package/dist/esm/loader.js +1 -1
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/p-09390c6a.entry.js +1 -0
- package/dist/lime-elements/p-25d0d06b.entry.js +159 -0
- package/dist/lime-elements/{p-ae2e5e87.entry.js → p-49eb2f64.entry.js} +1 -1
- package/dist/lime-elements/p-4f58c6b3.entry.js +1 -0
- package/dist/lime-elements/p-5a15606a.entry.js +1 -0
- package/dist/lime-elements/p-9ff6a455.entry.js +1 -0
- package/dist/lime-elements/p-B8Rm8W5O.js +1 -0
- package/dist/lime-elements/p-BhoYaGlg.js +1 -0
- package/dist/types/components/side-panel/side-panel.d.ts +35 -73
- package/dist/types/components.d.ts +70 -238
- package/dist/types/util/focus-trigger-element.d.ts +4 -4
- package/dist/types/util/get-focus-target.d.ts +18 -6
- package/package.json +6 -6
- package/dist/lime-elements/p-1a5478e3.entry.js +0 -1
- package/dist/lime-elements/p-2cf9f2e1.entry.js +0 -159
- package/dist/lime-elements/p-33b58741.entry.js +0 -1
- package/dist/lime-elements/p-82c65f70.entry.js +0 -1
- package/dist/lime-elements/p-9461c6bb.entry.js +0 -1
- package/dist/lime-elements/p-D0F7gOYG.js +0 -1
|
@@ -22,87 +22,28 @@ function stopEvent(event) {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* A resizable panel that is displayed on top of the right side of the page.
|
|
25
|
-
*
|
|
25
|
+
* It does not block the rest of the page. Place it in an element with
|
|
26
|
+
* `position: relative`.
|
|
26
27
|
*
|
|
27
|
-
* The panel
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
28
|
+
* The panel has:
|
|
29
|
+
* - a toolbar with a close button, and buttons from `actions`
|
|
30
|
+
* - a header that shows the last entry of `stack`, with a slot for controls
|
|
31
|
+
* - backs behind its left edge, for the other entries of `stack`
|
|
32
|
+
* - a handle on its left edge, to resize the panel
|
|
33
|
+
* - a full screen mode, for small screens
|
|
31
34
|
*
|
|
32
|
-
* The
|
|
35
|
+
* The panel emits `close`, but does not remove itself. The consumer must
|
|
36
|
+
* remove it from the DOM.
|
|
33
37
|
*
|
|
34
|
-
*
|
|
35
|
-
* to give it an accessible name, which screen readers announce. When the panel
|
|
36
|
-
* opens, focus moves into it. When the panel is removed, focus returns to the
|
|
37
|
-
* element that had it before, unless focus is somewhere else by then.
|
|
38
|
-
*
|
|
39
|
-
* The panel emits `close` when the user clicks the close button, or presses
|
|
40
|
-
* Escape. It does not remove itself. The consumer must remove it from the DOM.
|
|
41
|
-
* The user can resize the panel by dragging its left edge, or with the arrow
|
|
42
|
-
* keys. The panel emits `changeWidth` when a resize is finished.
|
|
43
|
-
*
|
|
44
|
-
* See the examples for how closing and resizing work in detail.
|
|
45
|
-
*
|
|
46
|
-
* ## Header
|
|
47
|
-
*
|
|
48
|
-
* The header shows the heading, subheading and icon of the last entry in
|
|
49
|
-
* `stack`. When the entry has a `color`, the header gets that background color,
|
|
50
|
-
* and the headings are white with a text shadow. When `stack` is empty, the
|
|
51
|
-
* panel has no header.
|
|
52
|
-
*
|
|
53
|
-
* Put controls for the header, for example a follow button or a menu, in the
|
|
54
|
-
* `header-actions` slot. They are shown at the right end of the header. When
|
|
55
|
-
* `stack` is empty, there is no header, and the slot content is not shown.
|
|
56
|
-
*
|
|
57
|
-
* The slot is for small controls. It is at most half as wide as the header,
|
|
58
|
-
* and at most as tall as the heading and subheading together. Larger content
|
|
59
|
-
* is cut off, so that the header keeps its size.
|
|
60
|
-
*
|
|
61
|
-
* The slot content is not tied to an entry in `stack`. When the last entry
|
|
62
|
-
* changes, the consumer must also change the slot content.
|
|
63
|
-
*
|
|
64
|
-
* The slot content keeps its own colors. Use `--side-panel-header-text-color`
|
|
65
|
-
* to match the text color of the header. It is white when the header has a
|
|
66
|
-
* `color`. It can also be used as the `color` of an `Icon`.
|
|
67
|
-
*
|
|
68
|
-
* ## Actions
|
|
69
|
-
*
|
|
70
|
-
* The toolbar has a close button on the left. Other buttons are set with
|
|
71
|
-
* `actions`, and are shown on the right. The `type` of an action decides its
|
|
72
|
-
* icon. When the user clicks an action, the panel emits `selectAction` with the
|
|
73
|
-
* `id` of the action and the click event. The consumer decides what the action
|
|
74
|
-
* does.
|
|
75
|
-
*
|
|
76
|
-
* An action with an `href` is a link, so the user can open it in a new tab.
|
|
77
|
-
* To handle the navigation in another way, for example with the router of the
|
|
78
|
-
* application, call `preventDefault` on the click event in `selectAction`. Do
|
|
79
|
-
* this only for a plain click with the primary button. Let the browser handle
|
|
80
|
-
* all other clicks, so that Ctrl-click and Cmd-click still open the link in a
|
|
81
|
-
* new tab:
|
|
82
|
-
*
|
|
83
|
-
* ```ts
|
|
84
|
-
* function handleSelectAction(event: CustomEvent<SidePanelActionSelect>) {
|
|
85
|
-
* const click = event.detail.event;
|
|
86
|
-
* const isPlainClick =
|
|
87
|
-
* click.button === 0 &&
|
|
88
|
-
* !click.metaKey &&
|
|
89
|
-
* !click.ctrlKey &&
|
|
90
|
-
* !click.shiftKey &&
|
|
91
|
-
* !click.altKey;
|
|
92
|
-
* if (!isPlainClick) {
|
|
93
|
-
* return;
|
|
94
|
-
* }
|
|
95
|
-
*
|
|
96
|
-
* click.preventDefault();
|
|
97
|
-
* router.navigate(event.detail.id);
|
|
98
|
-
* }
|
|
99
|
-
* ```
|
|
38
|
+
* Each example shows one feature in detail, and when to use it.
|
|
100
39
|
*
|
|
101
40
|
* @exampleComponent limel-example-side-panel-basic
|
|
102
41
|
* @exampleComponent limel-example-side-panel-insets
|
|
103
42
|
* @exampleComponent limel-example-side-panel-escape
|
|
104
43
|
* @exampleComponent limel-example-side-panel-actions
|
|
105
44
|
* @exampleComponent limel-example-side-panel-header-actions
|
|
45
|
+
* @exampleComponent limel-example-side-panel-stacking
|
|
46
|
+
* @exampleComponent limel-example-side-panel-full-screen
|
|
106
47
|
* @slot - Content of the panel
|
|
107
48
|
* @slot header-actions - Small controls at the right end of the header. Content that is larger than half the header width or the height of the headings is cut off.
|
|
108
49
|
* @private
|
|
@@ -117,7 +58,7 @@ export class SidePanel {
|
|
|
117
58
|
/**
|
|
118
59
|
* The panels in the stack, outermost first. The last entry is the panel
|
|
119
60
|
* that is displayed. Its heading, subheading and icon are shown in the
|
|
120
|
-
* header.
|
|
61
|
+
* header. The other entries are shown as backs behind the panel.
|
|
121
62
|
*/
|
|
122
63
|
this.stack = [];
|
|
123
64
|
/**
|
|
@@ -134,13 +75,27 @@ export class SidePanel {
|
|
|
134
75
|
* event either.
|
|
135
76
|
*/
|
|
136
77
|
this.closeOnEscape = true;
|
|
78
|
+
/**
|
|
79
|
+
* Set to `true` when the screen is too narrow for the panel to be displayed
|
|
80
|
+
* on top of the page. The panel then covers the full width of the
|
|
81
|
+
* positioned ancestor. It cannot be resized, and the backs of the stack are
|
|
82
|
+
* not shown.
|
|
83
|
+
*/
|
|
84
|
+
this.fullScreen = false;
|
|
137
85
|
this.closeButtonId = createRandomString();
|
|
138
86
|
this.actionIdPrefix = createRandomString();
|
|
87
|
+
this.backIdPrefix = createRandomString();
|
|
88
|
+
this.focusCloseButtonAfterRender = false;
|
|
139
89
|
this.renderAction = (action, index) => {
|
|
140
90
|
const id = `${this.actionIdPrefix}-${index}`;
|
|
141
91
|
const Tag = action.href ? 'a' : 'button';
|
|
142
92
|
return (h(Tag, { class: action.type, id: id, href: action.href, target: getLinkTarget(action), "aria-label": action.label, onClick: this.handleClickAction(action) }, h("limel-icon", { name: ACTION_ICONS[action.type], "aria-hidden": "true" }), h("limel-tooltip", { elementId: id, label: action.label, helperLabel: action.helperLabel })));
|
|
143
93
|
};
|
|
94
|
+
this.renderBack = (entry, index) => {
|
|
95
|
+
var _a;
|
|
96
|
+
const id = `${this.backIdPrefix}-${index}`;
|
|
97
|
+
return (h("button", { class: "back", id: id, style: { width: (_a = this.panel) === null || _a === void 0 ? void 0 : _a.style.width }, "aria-label": entry.heading, onClick: this.handleClickBack(index) }, h("span", { class: "back-toolbar" }), h("span", { class: "back-band", style: { 'background-color': entry.color } }), h("limel-tooltip", { elementId: id, label: entry.heading, helperLabel: entry.subheading, openDirection: "left" })));
|
|
98
|
+
};
|
|
144
99
|
/**
|
|
145
100
|
* Components in the content can emit events with the same names as the
|
|
146
101
|
* panel's events, for example `close` from `limel-dialog` or
|
|
@@ -235,6 +190,9 @@ export class SidePanel {
|
|
|
235
190
|
event: event,
|
|
236
191
|
});
|
|
237
192
|
};
|
|
193
|
+
this.handleClickBack = (index) => () => {
|
|
194
|
+
this.selectStackEntry.emit(index);
|
|
195
|
+
};
|
|
238
196
|
this.handleClickClose = () => {
|
|
239
197
|
this.close.emit();
|
|
240
198
|
};
|
|
@@ -256,8 +214,24 @@ export class SidePanel {
|
|
|
256
214
|
this.removeFromOpenPanels();
|
|
257
215
|
queueMicrotask(this.restoreFocus);
|
|
258
216
|
}
|
|
217
|
+
// A removed button takes the focus with it, so a keyboard user that goes
|
|
218
|
+
// back would lose their place in the page. Both props can change in the
|
|
219
|
+
// same update, so the result comes from both, and not from the prop that
|
|
220
|
+
// changed last.
|
|
221
|
+
handleBacksChange() {
|
|
222
|
+
this.focusCloseButtonAfterRender = this.isFocusedBackRemoved();
|
|
223
|
+
}
|
|
224
|
+
componentDidRender() {
|
|
225
|
+
var _a;
|
|
226
|
+
if (!this.focusCloseButtonAfterRender) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.focusCloseButtonAfterRender = false;
|
|
230
|
+
(_a = this.host.shadowRoot
|
|
231
|
+
.querySelector('button.close')) === null || _a === void 0 ? void 0 : _a.focus();
|
|
232
|
+
}
|
|
259
233
|
render() {
|
|
260
|
-
return (h(Host, { key: '
|
|
234
|
+
return (h(Host, { key: 'ca07c3557df3be7e026a95da296689e1e3c0cf41' }, h("section", { key: '610a8b47c7b34a6d4cb4d253d40cff889ea4c73d', ref: this.setPanel, role: "dialog", "aria-label": this.label, tabindex: "-1" }, this.renderHandle(), h("div", { key: 'c9ddb80456ac82cb39801943b55fcd4de3928852', class: "content" }, h("div", { key: 'f1430da0610a4fe71363ead5c7730d627a8a935c', class: "toolbar" }, this.renderCloseButton(), this.renderActions()), this.renderHeader(), this.renderBody())), this.renderStack(), h("span", { key: 'c7d204c8b323f88962508a4c54493717fe347c8e', "data-limit": "min", ref: this.setMinLimit }), h("span", { key: '2a7fa787fd0d4c777f90b2ef7dff1d9316108416', "data-limit": "max", ref: this.setMaxLimit })));
|
|
261
235
|
}
|
|
262
236
|
// A native button and not `limel-icon-button`, because the hover animation
|
|
263
237
|
// styles the icon, which is inside the shadow root of `limel-icon-button`.
|
|
@@ -270,6 +244,21 @@ export class SidePanel {
|
|
|
270
244
|
}
|
|
271
245
|
return (h("div", { class: "actions" }, this.actions.map((action, index) => this.renderAction(action, index))));
|
|
272
246
|
}
|
|
247
|
+
renderStack() {
|
|
248
|
+
const backs = this.stack.slice(0, -1);
|
|
249
|
+
if (this.fullScreen || backs.length === 0) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
return (h("div", { class: "stack" }, backs.map((entry, index) => this.renderBack(entry, index))));
|
|
253
|
+
}
|
|
254
|
+
isFocusedBackRemoved() {
|
|
255
|
+
const backs = [...this.host.shadowRoot.querySelectorAll('.back')];
|
|
256
|
+
const index = backs.indexOf(this.host.shadowRoot.activeElement);
|
|
257
|
+
if (index === -1) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return this.fullScreen || index >= this.stack.length - 1;
|
|
261
|
+
}
|
|
273
262
|
renderHeader() {
|
|
274
263
|
const entry = this.stack.at(-1);
|
|
275
264
|
if (!entry) {
|
|
@@ -279,6 +268,9 @@ export class SidePanel {
|
|
|
279
268
|
}
|
|
280
269
|
renderHandle() {
|
|
281
270
|
var _a, _b, _c;
|
|
271
|
+
if (this.fullScreen) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
282
274
|
return (h("limel-resize-handle", { class: "handle", label: this.resizeLabel, value: (_a = this.widthRange) === null || _a === void 0 ? void 0 : _a.now, min: (_b = this.widthRange) === null || _b === void 0 ? void 0 : _b.min, max: (_c = this.widthRange) === null || _c === void 0 ? void 0 : _c.max, onChangeSize: this.handleChangeSize, onEndResize: this.handleEndResize }));
|
|
283
275
|
}
|
|
284
276
|
renderBody() {
|
|
@@ -324,9 +316,16 @@ export class SidePanel {
|
|
|
324
316
|
// An inline width wins over `--side-panel-width`, and is still limited by
|
|
325
317
|
// `min-width` and `max-width`. The width is set on the element and not with
|
|
326
318
|
// a state, because a render and a measurement on each pointer move make
|
|
327
|
-
// the drag slow when the content is large.
|
|
319
|
+
// the drag slow when the content is large. The backs of the stack get the
|
|
320
|
+
// width too, so that they have the same width as the panel.
|
|
328
321
|
setWidth(width) {
|
|
329
|
-
this.
|
|
322
|
+
for (const element of this.getPanelBoxes()) {
|
|
323
|
+
element.style.width = `${width}px`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
getPanelBoxes() {
|
|
327
|
+
const backs = this.host.shadowRoot.querySelectorAll('.back');
|
|
328
|
+
return [this.panel, ...backs];
|
|
330
329
|
}
|
|
331
330
|
rememberWidthBeforeResize() {
|
|
332
331
|
if (this.isResizing()) {
|
|
@@ -456,7 +455,7 @@ export class SidePanel {
|
|
|
456
455
|
"optional": false,
|
|
457
456
|
"docs": {
|
|
458
457
|
"tags": [],
|
|
459
|
-
"text": "The panels in the stack, outermost first. The last entry is the panel\nthat is displayed. Its heading, subheading and icon are shown in the\nheader."
|
|
458
|
+
"text": "The panels in the stack, outermost first. The last entry is the panel\nthat is displayed. Its heading, subheading and icon are shown in the\nheader. The other entries are shown as backs behind the panel."
|
|
460
459
|
},
|
|
461
460
|
"getter": false,
|
|
462
461
|
"setter": false,
|
|
@@ -526,6 +525,26 @@ export class SidePanel {
|
|
|
526
525
|
"reflect": false,
|
|
527
526
|
"attribute": "close-on-escape",
|
|
528
527
|
"defaultValue": "true"
|
|
528
|
+
},
|
|
529
|
+
"fullScreen": {
|
|
530
|
+
"type": "boolean",
|
|
531
|
+
"mutable": false,
|
|
532
|
+
"complexType": {
|
|
533
|
+
"original": "boolean",
|
|
534
|
+
"resolved": "boolean",
|
|
535
|
+
"references": {}
|
|
536
|
+
},
|
|
537
|
+
"required": false,
|
|
538
|
+
"optional": false,
|
|
539
|
+
"docs": {
|
|
540
|
+
"tags": [],
|
|
541
|
+
"text": "Set to `true` when the screen is too narrow for the panel to be displayed\non top of the page. The panel then covers the full width of the\npositioned ancestor. It cannot be resized, and the backs of the stack are\nnot shown."
|
|
542
|
+
},
|
|
543
|
+
"getter": false,
|
|
544
|
+
"setter": false,
|
|
545
|
+
"reflect": true,
|
|
546
|
+
"attribute": "full-screen",
|
|
547
|
+
"defaultValue": "false"
|
|
529
548
|
}
|
|
530
549
|
};
|
|
531
550
|
}
|
|
@@ -587,9 +606,33 @@ export class SidePanel {
|
|
|
587
606
|
"resolved": "number",
|
|
588
607
|
"references": {}
|
|
589
608
|
}
|
|
609
|
+
}, {
|
|
610
|
+
"method": "selectStackEntry",
|
|
611
|
+
"name": "selectStackEntry",
|
|
612
|
+
"bubbles": true,
|
|
613
|
+
"cancelable": true,
|
|
614
|
+
"composed": true,
|
|
615
|
+
"docs": {
|
|
616
|
+
"tags": [],
|
|
617
|
+
"text": "Emitted when a back of the stack is clicked. The detail is the index of\nits entry in `stack`."
|
|
618
|
+
},
|
|
619
|
+
"complexType": {
|
|
620
|
+
"original": "number",
|
|
621
|
+
"resolved": "number",
|
|
622
|
+
"references": {}
|
|
623
|
+
}
|
|
590
624
|
}];
|
|
591
625
|
}
|
|
592
626
|
static get elementRef() { return "host"; }
|
|
627
|
+
static get watchers() {
|
|
628
|
+
return [{
|
|
629
|
+
"propName": "stack",
|
|
630
|
+
"methodName": "handleBacksChange"
|
|
631
|
+
}, {
|
|
632
|
+
"propName": "fullScreen",
|
|
633
|
+
"methodName": "handleBacksChange"
|
|
634
|
+
}];
|
|
635
|
+
}
|
|
593
636
|
static get listeners() {
|
|
594
637
|
return [{
|
|
595
638
|
"name": "close",
|
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { findFocusTarget } from "./get-focus-target";
|
|
2
2
|
const isDisabled = (element) => {
|
|
3
3
|
return (element.disabled === true ||
|
|
4
4
|
element.hasAttribute('disabled') ||
|
|
5
5
|
element.getAttribute('aria-disabled') === 'true');
|
|
6
6
|
};
|
|
7
7
|
/**
|
|
8
|
-
* Focuses the first focusable element inside
|
|
9
|
-
* Supports custom elements by searching
|
|
10
|
-
* and its light DOM.
|
|
8
|
+
* Focuses a trigger element, or the first focusable element inside it if the
|
|
9
|
+
* trigger can not take focus itself. Supports custom elements by searching
|
|
10
|
+
* both the element's shadow root and its light DOM.
|
|
11
11
|
*
|
|
12
12
|
* @param trigger - The trigger element to focus.
|
|
13
|
-
* @returns `
|
|
13
|
+
* @returns `false` if there is no trigger, or it is disabled. Otherwise `true`.
|
|
14
14
|
*/
|
|
15
15
|
export const focusTriggerElement = (trigger) => {
|
|
16
|
-
var _a;
|
|
17
16
|
if (!trigger || isDisabled(trigger)) {
|
|
18
17
|
return false;
|
|
19
18
|
}
|
|
20
|
-
|
|
21
|
-
if (shadowFocusable) {
|
|
22
|
-
shadowFocusable.focus();
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
const lightDomFocusable = trigger.querySelector(FOCUSABLE_SELECTOR);
|
|
26
|
-
if (lightDomFocusable) {
|
|
27
|
-
lightDomFocusable.focus();
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
trigger.focus();
|
|
19
|
+
findFocusTarget(trigger).focus();
|
|
31
20
|
return true;
|
|
32
21
|
};
|
|
@@ -10,44 +10,75 @@ const FOCUSABLE = [
|
|
|
10
10
|
.join(', ');
|
|
11
11
|
const DISABLED = CONTROLS.map((selector) => `${selector}[disabled]:not([tabindex="-1"])`).join(', ');
|
|
12
12
|
/**
|
|
13
|
-
* Get the element that
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Get the element that gets the focus when a user moves to `owner`. That is
|
|
14
|
+
* the owner itself if it can be focused, otherwise the first element that can
|
|
15
|
+
* be focused inside it, also inside shadow roots. A disabled control is only
|
|
16
|
+
* used when there is nothing that can be focused, and if there is no control
|
|
17
|
+
* at all, the owner itself is used.
|
|
18
18
|
*
|
|
19
19
|
* An element that passes the focus on to an element in its shadow root is
|
|
20
|
-
* never used itself, since the
|
|
20
|
+
* never used itself, since the focus lands on the element inside.
|
|
21
21
|
*
|
|
22
|
+
* Waits for the components in the way to render, so use this for an owner
|
|
23
|
+
* that may not have rendered yet.
|
|
24
|
+
*
|
|
25
|
+
* @see findFocusTarget for an owner that has already rendered
|
|
22
26
|
* @param owner
|
|
23
27
|
*/
|
|
24
28
|
export async function getFocusTarget(owner) {
|
|
25
|
-
|
|
29
|
+
return runWalk(findTarget(owner));
|
|
30
|
+
}
|
|
31
|
+
async function runWalk(walk, step = walk.next()) {
|
|
32
|
+
if (step.done) {
|
|
33
|
+
return step.value;
|
|
34
|
+
}
|
|
35
|
+
await step.value.componentOnReady();
|
|
36
|
+
const found = await runWalk(walk, walk.next());
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Same as `getFocusTarget`, for an owner that has already rendered, without
|
|
41
|
+
* waiting for any component.
|
|
42
|
+
*
|
|
43
|
+
* @see getFocusTarget for an owner that may not have rendered yet
|
|
44
|
+
* @param owner
|
|
45
|
+
*/
|
|
46
|
+
export function findFocusTarget(owner) {
|
|
47
|
+
const walk = findTarget(owner);
|
|
48
|
+
for (let step = walk.next();; step = walk.next()) {
|
|
49
|
+
if (step.done) {
|
|
50
|
+
return step.value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function* findTarget(owner) {
|
|
55
|
+
var _a;
|
|
26
56
|
const search = {};
|
|
27
|
-
|
|
57
|
+
const found = yield* find(owner, search);
|
|
58
|
+
return (_a = found !== null && found !== void 0 ? found : search.disabled) !== null && _a !== void 0 ? _a : owner;
|
|
28
59
|
}
|
|
29
|
-
|
|
60
|
+
function* find(element, search) {
|
|
30
61
|
if (isFocusable(element)) {
|
|
31
62
|
return element;
|
|
32
63
|
}
|
|
33
64
|
rememberDisabled(element, search);
|
|
34
|
-
return findInside(element, search);
|
|
65
|
+
return yield* findInside(element, search);
|
|
35
66
|
}
|
|
36
|
-
|
|
67
|
+
function* findInside(element, search) {
|
|
37
68
|
if (isStencilComponent(element)) {
|
|
38
|
-
|
|
69
|
+
yield element;
|
|
39
70
|
}
|
|
40
71
|
const inShadowRoot = element.shadowRoot
|
|
41
|
-
?
|
|
72
|
+
? yield* findAmong(element.shadowRoot.children, search)
|
|
42
73
|
: undefined;
|
|
43
|
-
return inShadowRoot !== null && inShadowRoot !== void 0 ? inShadowRoot : findAmong(element.children, search);
|
|
74
|
+
return inShadowRoot !== null && inShadowRoot !== void 0 ? inShadowRoot : (yield* findAmong(element.children, search));
|
|
44
75
|
}
|
|
45
|
-
|
|
76
|
+
function* findAmong(elements, search) {
|
|
46
77
|
for (const element of elements) {
|
|
47
78
|
if (element.localName === 'limel-tooltip') {
|
|
48
79
|
continue;
|
|
49
80
|
}
|
|
50
|
-
const found =
|
|
81
|
+
const found = yield* find(element, search);
|
|
51
82
|
if (found) {
|
|
52
83
|
return found;
|
|
53
84
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { a as assignValue } from './_assignValue-CEFvyZxO.js';
|
|
2
|
+
import { f as findFocusTarget } from './get-focus-target-BhoYaGlg.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* This base implementation of `_.zipObject` which assigns values using `assignFunc`.
|
|
@@ -42,36 +43,24 @@ function zipObject(props, values) {
|
|
|
42
43
|
return baseZipObject(props || [], values || [], assignValue);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
46
46
|
const isDisabled = (element) => {
|
|
47
47
|
return (element.disabled === true ||
|
|
48
48
|
element.hasAttribute('disabled') ||
|
|
49
49
|
element.getAttribute('aria-disabled') === 'true');
|
|
50
50
|
};
|
|
51
51
|
/**
|
|
52
|
-
* Focuses the first focusable element inside
|
|
53
|
-
* Supports custom elements by searching
|
|
54
|
-
* and its light DOM.
|
|
52
|
+
* Focuses a trigger element, or the first focusable element inside it if the
|
|
53
|
+
* trigger can not take focus itself. Supports custom elements by searching
|
|
54
|
+
* both the element's shadow root and its light DOM.
|
|
55
55
|
*
|
|
56
56
|
* @param trigger - The trigger element to focus.
|
|
57
|
-
* @returns `
|
|
57
|
+
* @returns `false` if there is no trigger, or it is disabled. Otherwise `true`.
|
|
58
58
|
*/
|
|
59
59
|
const focusTriggerElement = (trigger) => {
|
|
60
|
-
var _a;
|
|
61
60
|
if (!trigger || isDisabled(trigger)) {
|
|
62
61
|
return false;
|
|
63
62
|
}
|
|
64
|
-
|
|
65
|
-
if (shadowFocusable) {
|
|
66
|
-
shadowFocusable.focus();
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
const lightDomFocusable = trigger.querySelector(FOCUSABLE_SELECTOR);
|
|
70
|
-
if (lightDomFocusable) {
|
|
71
|
-
lightDomFocusable.focus();
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
trigger.focus();
|
|
63
|
+
findFocusTarget(trigger).focus();
|
|
75
64
|
return true;
|
|
76
65
|
};
|
|
77
66
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const CONTROLS = ['button', 'input:not([type="hidden"])', 'select', 'textarea'];
|
|
2
|
+
const FOCUSABLE = [
|
|
3
|
+
'a[href]',
|
|
4
|
+
...CONTROLS.map((selector) => `${selector}:not([disabled])`),
|
|
5
|
+
'summary',
|
|
6
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
7
|
+
'[tabindex]',
|
|
8
|
+
]
|
|
9
|
+
.map((selector) => `${selector}:not([tabindex="-1"])`)
|
|
10
|
+
.join(', ');
|
|
11
|
+
const DISABLED = CONTROLS.map((selector) => `${selector}[disabled]:not([tabindex="-1"])`).join(', ');
|
|
12
|
+
/**
|
|
13
|
+
* Get the element that gets the focus when a user moves to `owner`. That is
|
|
14
|
+
* the owner itself if it can be focused, otherwise the first element that can
|
|
15
|
+
* be focused inside it, also inside shadow roots. A disabled control is only
|
|
16
|
+
* used when there is nothing that can be focused, and if there is no control
|
|
17
|
+
* at all, the owner itself is used.
|
|
18
|
+
*
|
|
19
|
+
* An element that passes the focus on to an element in its shadow root is
|
|
20
|
+
* never used itself, since the focus lands on the element inside.
|
|
21
|
+
*
|
|
22
|
+
* Waits for the components in the way to render, so use this for an owner
|
|
23
|
+
* that may not have rendered yet.
|
|
24
|
+
*
|
|
25
|
+
* @see findFocusTarget for an owner that has already rendered
|
|
26
|
+
* @param owner
|
|
27
|
+
*/
|
|
28
|
+
async function getFocusTarget(owner) {
|
|
29
|
+
return runWalk(findTarget(owner));
|
|
30
|
+
}
|
|
31
|
+
async function runWalk(walk, step = walk.next()) {
|
|
32
|
+
if (step.done) {
|
|
33
|
+
return step.value;
|
|
34
|
+
}
|
|
35
|
+
await step.value.componentOnReady();
|
|
36
|
+
const found = await runWalk(walk, walk.next());
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Same as `getFocusTarget`, for an owner that has already rendered, without
|
|
41
|
+
* waiting for any component.
|
|
42
|
+
*
|
|
43
|
+
* @see getFocusTarget for an owner that may not have rendered yet
|
|
44
|
+
* @param owner
|
|
45
|
+
*/
|
|
46
|
+
function findFocusTarget(owner) {
|
|
47
|
+
const walk = findTarget(owner);
|
|
48
|
+
for (let step = walk.next();; step = walk.next()) {
|
|
49
|
+
if (step.done) {
|
|
50
|
+
return step.value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function* findTarget(owner) {
|
|
55
|
+
var _a;
|
|
56
|
+
const search = {};
|
|
57
|
+
const found = yield* find(owner, search);
|
|
58
|
+
return (_a = found !== null && found !== void 0 ? found : search.disabled) !== null && _a !== void 0 ? _a : owner;
|
|
59
|
+
}
|
|
60
|
+
function* find(element, search) {
|
|
61
|
+
if (isFocusable(element)) {
|
|
62
|
+
return element;
|
|
63
|
+
}
|
|
64
|
+
rememberDisabled(element, search);
|
|
65
|
+
return yield* findInside(element, search);
|
|
66
|
+
}
|
|
67
|
+
function* findInside(element, search) {
|
|
68
|
+
if (isStencilComponent(element)) {
|
|
69
|
+
yield element;
|
|
70
|
+
}
|
|
71
|
+
const inShadowRoot = element.shadowRoot
|
|
72
|
+
? yield* findAmong(element.shadowRoot.children, search)
|
|
73
|
+
: undefined;
|
|
74
|
+
return inShadowRoot !== null && inShadowRoot !== void 0 ? inShadowRoot : (yield* findAmong(element.children, search));
|
|
75
|
+
}
|
|
76
|
+
function* findAmong(elements, search) {
|
|
77
|
+
for (const element of elements) {
|
|
78
|
+
if (element.localName === 'limel-tooltip') {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const found = yield* find(element, search);
|
|
82
|
+
if (found) {
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function isFocusable(element) {
|
|
89
|
+
var _a;
|
|
90
|
+
return !((_a = element.shadowRoot) === null || _a === void 0 ? void 0 : _a.delegatesFocus) && element.matches(FOCUSABLE);
|
|
91
|
+
}
|
|
92
|
+
function rememberDisabled(element, search) {
|
|
93
|
+
if (!search.disabled && element.matches(DISABLED)) {
|
|
94
|
+
search.disabled = element;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function isStencilComponent(element) {
|
|
98
|
+
return ('componentOnReady' in element &&
|
|
99
|
+
typeof element.componentOnReady === 'function');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { findFocusTarget as f, getFocusTarget as g };
|