@stackoverflow/stacks 1.6.2 → 1.6.3
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/README.md +22 -0
- package/dist/controllers/index.d.ts +7 -7
- package/dist/controllers/s-expandable-control.d.ts +1 -1
- package/dist/css/stacks.css +4 -15
- package/dist/css/stacks.min.css +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/js/stacks.js +174 -112
- package/dist/js/stacks.min.js +1 -1
- package/dist/stacks.d.ts +1 -1
- package/lib/css/atomic/gap.less +1 -1
- package/lib/css/components/buttons.less +3 -1
- package/lib/css/components/cards.less +1 -1
- package/lib/css/components/expandable.less +1 -1
- package/lib/css/components/inputs.less +4 -18
- package/lib/css/components/pagination.less +1 -1
- package/lib/css/components/popovers.less +6 -7
- package/lib/ts/controllers/index.ts +14 -7
- package/lib/ts/controllers/s-expandable-control.ts +79 -34
- package/lib/ts/controllers/s-modal.ts +116 -58
- package/lib/ts/controllers/s-navigation-tablist.ts +30 -20
- package/lib/ts/controllers/s-popover.ts +149 -73
- package/lib/ts/controllers/s-table.ts +69 -28
- package/lib/ts/controllers/s-tooltip.ts +87 -29
- package/lib/ts/controllers/s-uploader.ts +58 -39
- package/lib/ts/index.ts +11 -3
- package/lib/ts/stacks.ts +40 -19
- package/lib/tsconfig.json +1 -1
- package/package.json +8 -6
|
@@ -13,7 +13,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
13
13
|
|
|
14
14
|
private _boundTabTrap!: (event: KeyboardEvent) => void;
|
|
15
15
|
|
|
16
|
-
connect
|
|
16
|
+
connect() {
|
|
17
17
|
this.validate();
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -22,26 +22,26 @@ export class ModalController extends Stacks.StacksController {
|
|
|
22
22
|
*/
|
|
23
23
|
disconnect() {
|
|
24
24
|
this.unbindDocumentEvents();
|
|
25
|
-
}
|
|
25
|
+
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Toggles the visibility of the modal
|
|
29
29
|
*/
|
|
30
|
-
toggle
|
|
30
|
+
toggle(dispatcher: Event | Element | null = null) {
|
|
31
31
|
this._toggle(undefined, dispatcher);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Shows the modal
|
|
36
36
|
*/
|
|
37
|
-
show
|
|
37
|
+
show(dispatcher: Event | Element | null = null) {
|
|
38
38
|
this._toggle(true, dispatcher);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Hides the modal
|
|
43
43
|
*/
|
|
44
|
-
hide
|
|
44
|
+
hide(dispatcher: Event | Element | null = null) {
|
|
45
45
|
this._toggle(false, dispatcher);
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -52,10 +52,15 @@ export class ModalController extends Stacks.StacksController {
|
|
|
52
52
|
// check for returnElement support
|
|
53
53
|
const returnElementSelector = this.data.get("return-element");
|
|
54
54
|
if (returnElementSelector) {
|
|
55
|
-
this.returnElement = <HTMLElement>
|
|
55
|
+
this.returnElement = <HTMLElement>(
|
|
56
|
+
document.querySelector(returnElementSelector)
|
|
57
|
+
);
|
|
56
58
|
|
|
57
59
|
if (!this.returnElement) {
|
|
58
|
-
throw
|
|
60
|
+
throw (
|
|
61
|
+
"Unable to find element by return-element selector: " +
|
|
62
|
+
returnElementSelector
|
|
63
|
+
);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
}
|
|
@@ -64,9 +69,13 @@ export class ModalController extends Stacks.StacksController {
|
|
|
64
69
|
* Toggles the visibility of the modal element
|
|
65
70
|
* @param show Optional parameter that force shows/hides the element or toggles it if left undefined
|
|
66
71
|
*/
|
|
67
|
-
private _toggle
|
|
72
|
+
private _toggle(
|
|
73
|
+
show?: boolean | undefined,
|
|
74
|
+
dispatcher: Event | Element | null = null
|
|
75
|
+
) {
|
|
68
76
|
let toShow = show;
|
|
69
|
-
const isVisible =
|
|
77
|
+
const isVisible =
|
|
78
|
+
this.modalTarget.getAttribute("aria-hidden") === "false";
|
|
70
79
|
|
|
71
80
|
// if we're letting the class toggle, we need to figure out if the popover is visible manually
|
|
72
81
|
if (typeof toShow === "undefined") {
|
|
@@ -81,10 +90,14 @@ export class ModalController extends Stacks.StacksController {
|
|
|
81
90
|
const dispatchingElement = this.getDispatcher(dispatcher);
|
|
82
91
|
|
|
83
92
|
// show/hide events trigger before toggling the class
|
|
84
|
-
const triggeredEvent = this.triggerEvent(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const triggeredEvent = this.triggerEvent(
|
|
94
|
+
toShow ? "show" : "hide",
|
|
95
|
+
{
|
|
96
|
+
returnElement: this.returnElement,
|
|
97
|
+
dispatcher: this.getDispatcher(dispatchingElement),
|
|
98
|
+
},
|
|
99
|
+
this.modalTarget
|
|
100
|
+
);
|
|
88
101
|
|
|
89
102
|
// if this pre-show/hide event was prevented, don't attempt to continue changing the modal state
|
|
90
103
|
if (triggeredEvent.defaultPrevented) {
|
|
@@ -97,29 +110,41 @@ export class ModalController extends Stacks.StacksController {
|
|
|
97
110
|
if (toShow) {
|
|
98
111
|
this.bindDocumentEvents();
|
|
99
112
|
this.focusInsideModal();
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
113
|
+
} else {
|
|
102
114
|
this.unbindDocumentEvents();
|
|
103
115
|
this.focusReturnElement();
|
|
104
116
|
this.removeModalOnHide();
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
// check for transitionend support
|
|
108
|
-
const supportsTransitionEnd =
|
|
120
|
+
const supportsTransitionEnd =
|
|
121
|
+
this.modalTarget.ontransitionend !== undefined;
|
|
109
122
|
|
|
110
123
|
// shown/hidden events trigger after toggling the class
|
|
111
124
|
if (supportsTransitionEnd) {
|
|
112
125
|
// wait until after the modal finishes transitioning to fire the event
|
|
113
|
-
this.modalTarget.addEventListener(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
this.modalTarget.addEventListener(
|
|
127
|
+
"transitionend",
|
|
128
|
+
() => {
|
|
129
|
+
//TODO this is firing waaay to soon?
|
|
130
|
+
this.triggerEvent(
|
|
131
|
+
toShow ? "shown" : "hidden",
|
|
132
|
+
{
|
|
133
|
+
dispatcher: dispatchingElement,
|
|
134
|
+
},
|
|
135
|
+
this.modalTarget
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
{ once: true }
|
|
139
|
+
);
|
|
119
140
|
} else {
|
|
120
|
-
this.triggerEvent(
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
this.triggerEvent(
|
|
142
|
+
toShow ? "shown" : "hidden",
|
|
143
|
+
{
|
|
144
|
+
dispatcher: dispatchingElement,
|
|
145
|
+
},
|
|
146
|
+
this.modalTarget
|
|
147
|
+
);
|
|
123
148
|
}
|
|
124
149
|
}
|
|
125
150
|
|
|
@@ -131,12 +156,19 @@ export class ModalController extends Stacks.StacksController {
|
|
|
131
156
|
return;
|
|
132
157
|
}
|
|
133
158
|
|
|
134
|
-
this.modalTarget.addEventListener(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
this.modalTarget.addEventListener(
|
|
160
|
+
"s-modal:hidden",
|
|
161
|
+
() => {
|
|
162
|
+
// double check the element still exists when the event is called
|
|
163
|
+
if (
|
|
164
|
+
this.returnElement &&
|
|
165
|
+
document.body.contains(this.returnElement)
|
|
166
|
+
) {
|
|
167
|
+
this.returnElement.focus();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
{ once: true }
|
|
171
|
+
);
|
|
140
172
|
}
|
|
141
173
|
|
|
142
174
|
/**
|
|
@@ -147,17 +179,26 @@ export class ModalController extends Stacks.StacksController {
|
|
|
147
179
|
return;
|
|
148
180
|
}
|
|
149
181
|
|
|
150
|
-
this.modalTarget.addEventListener(
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
this.modalTarget.addEventListener(
|
|
183
|
+
"s-modal:hidden",
|
|
184
|
+
() => {
|
|
185
|
+
this.element.remove();
|
|
186
|
+
},
|
|
187
|
+
{ once: true }
|
|
188
|
+
);
|
|
153
189
|
}
|
|
154
190
|
|
|
155
191
|
/**
|
|
156
192
|
* Gets all elements within the modal that could receive keyboard focus.
|
|
157
193
|
*/
|
|
158
194
|
private getAllTabbables() {
|
|
159
|
-
return Array.from(
|
|
160
|
-
.
|
|
195
|
+
return Array.from(
|
|
196
|
+
this.modalTarget.querySelectorAll<HTMLElement>(
|
|
197
|
+
"[href], input, select, textarea, button, [tabindex]"
|
|
198
|
+
)
|
|
199
|
+
).filter((el: Element) =>
|
|
200
|
+
el.matches(":not([disabled]):not([tabindex='-1'])")
|
|
201
|
+
);
|
|
161
202
|
}
|
|
162
203
|
|
|
163
204
|
/**
|
|
@@ -165,7 +206,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
165
206
|
*/
|
|
166
207
|
private firstVisible(elements: HTMLElement[]) {
|
|
167
208
|
// https://stackoverflow.com/a/21696585
|
|
168
|
-
return elements.find(el => el.offsetParent !== null);
|
|
209
|
+
return elements.find((el) => el.offsetParent !== null);
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
/**
|
|
@@ -181,17 +222,22 @@ export class ModalController extends Stacks.StacksController {
|
|
|
181
222
|
* Otherwise, the first visible focusable element will receive focus.
|
|
182
223
|
*/
|
|
183
224
|
private focusInsideModal() {
|
|
184
|
-
this.modalTarget.addEventListener(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
225
|
+
this.modalTarget.addEventListener(
|
|
226
|
+
"s-modal:shown",
|
|
227
|
+
() => {
|
|
228
|
+
const initialFocus =
|
|
229
|
+
this.firstVisible(this.initialFocusTargets) ??
|
|
230
|
+
this.firstVisible(this.getAllTabbables());
|
|
231
|
+
initialFocus?.focus();
|
|
232
|
+
},
|
|
233
|
+
{ once: true }
|
|
234
|
+
);
|
|
188
235
|
}
|
|
189
236
|
|
|
190
237
|
/**
|
|
191
238
|
* Returns keyboard focus to the modal if it has left or is about to leave.
|
|
192
239
|
*/
|
|
193
240
|
private keepFocusWithinModal(e: KeyboardEvent) {
|
|
194
|
-
|
|
195
241
|
// If somehow the user has tabbed out of the modal or if focus started outside the modal, push them to the first item.
|
|
196
242
|
if (!this.modalTarget.contains(<Element>e.target)) {
|
|
197
243
|
const focusTarget = this.firstVisible(this.getAllTabbables());
|
|
@@ -220,7 +266,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
220
266
|
} else if (!e.shiftKey && e.target === lastTabbable) {
|
|
221
267
|
e.preventDefault();
|
|
222
268
|
firstTabbable.focus();
|
|
223
|
-
}
|
|
269
|
+
}
|
|
224
270
|
}
|
|
225
271
|
}
|
|
226
272
|
}
|
|
@@ -228,11 +274,14 @@ export class ModalController extends Stacks.StacksController {
|
|
|
228
274
|
/**
|
|
229
275
|
* Binds global events to the document for hiding popovers on user interaction
|
|
230
276
|
*/
|
|
231
|
-
private bindDocumentEvents
|
|
277
|
+
private bindDocumentEvents() {
|
|
232
278
|
// in order for removeEventListener to remove the right event, this bound function needs a constant reference
|
|
233
|
-
this._boundClickFn =
|
|
234
|
-
|
|
235
|
-
this.
|
|
279
|
+
this._boundClickFn =
|
|
280
|
+
this._boundClickFn || this.hideOnOutsideClick.bind(this);
|
|
281
|
+
this._boundKeypressFn =
|
|
282
|
+
this._boundKeypressFn || this.hideOnEscapePress.bind(this);
|
|
283
|
+
this._boundTabTrap =
|
|
284
|
+
this._boundTabTrap || this.keepFocusWithinModal.bind(this);
|
|
236
285
|
|
|
237
286
|
document.addEventListener("mousedown", this._boundClickFn);
|
|
238
287
|
document.addEventListener("keyup", this._boundKeypressFn);
|
|
@@ -242,7 +291,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
242
291
|
/**
|
|
243
292
|
* Unbinds global events to the document for hiding popovers on user interaction
|
|
244
293
|
*/
|
|
245
|
-
private unbindDocumentEvents
|
|
294
|
+
private unbindDocumentEvents() {
|
|
246
295
|
document.removeEventListener("mousedown", this._boundClickFn);
|
|
247
296
|
document.removeEventListener("keyup", this._boundKeypressFn);
|
|
248
297
|
document.removeEventListener("keydown", this._boundTabTrap);
|
|
@@ -251,11 +300,16 @@ export class ModalController extends Stacks.StacksController {
|
|
|
251
300
|
/**
|
|
252
301
|
* Forces the popover to hide if a user clicks outside of it or its reference element
|
|
253
302
|
*/
|
|
254
|
-
private hideOnOutsideClick
|
|
303
|
+
private hideOnOutsideClick(e: Event) {
|
|
255
304
|
const target = <Node>e.target;
|
|
256
305
|
// check if the document was clicked inside either the toggle element or the modal itself
|
|
257
306
|
// note: .contains also returns true if the node itself matches the target element
|
|
258
|
-
if (
|
|
307
|
+
if (
|
|
308
|
+
!this.modalTarget
|
|
309
|
+
.querySelector(".s-modal--dialog")
|
|
310
|
+
?.contains(target) &&
|
|
311
|
+
document.body.contains(target)
|
|
312
|
+
) {
|
|
259
313
|
this._toggle(false, e);
|
|
260
314
|
}
|
|
261
315
|
}
|
|
@@ -263,9 +317,12 @@ export class ModalController extends Stacks.StacksController {
|
|
|
263
317
|
/**
|
|
264
318
|
* Forces the popover to hide if the user presses escape while it, one of its childen, or the reference element are focused
|
|
265
319
|
*/
|
|
266
|
-
private hideOnEscapePress
|
|
320
|
+
private hideOnEscapePress(e: KeyboardEvent) {
|
|
267
321
|
// if the ESC key (27) wasn't pressed or if no popovers are showing, return
|
|
268
|
-
if (
|
|
322
|
+
if (
|
|
323
|
+
e.which !== 27 ||
|
|
324
|
+
this.modalTarget.getAttribute("aria-hidden") === "true"
|
|
325
|
+
) {
|
|
269
326
|
return;
|
|
270
327
|
}
|
|
271
328
|
|
|
@@ -276,14 +333,12 @@ export class ModalController extends Stacks.StacksController {
|
|
|
276
333
|
* Determines the correct dispatching element from a potential input
|
|
277
334
|
* @param dispatcher The event or element to get the dispatcher from
|
|
278
335
|
*/
|
|
279
|
-
private getDispatcher(dispatcher: Event|Element|null = null)
|
|
336
|
+
private getDispatcher(dispatcher: Event | Element | null = null): Element {
|
|
280
337
|
if (dispatcher instanceof Event) {
|
|
281
338
|
return <Element>dispatcher.target;
|
|
282
|
-
}
|
|
283
|
-
else if (dispatcher instanceof Element) {
|
|
339
|
+
} else if (dispatcher instanceof Element) {
|
|
284
340
|
return dispatcher;
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
341
|
+
} else {
|
|
287
342
|
return this.element;
|
|
288
343
|
}
|
|
289
344
|
}
|
|
@@ -311,7 +366,10 @@ export function hideModal(element: HTMLElement) {
|
|
|
311
366
|
* @param show whether to force show/hide the modal; toggles the modal if left undefined
|
|
312
367
|
*/
|
|
313
368
|
function toggleModal(element: HTMLElement, show?: boolean | undefined) {
|
|
314
|
-
const controller = Stacks.application.getControllerForElementAndIdentifier(
|
|
369
|
+
const controller = Stacks.application.getControllerForElementAndIdentifier(
|
|
370
|
+
element,
|
|
371
|
+
"s-modal"
|
|
372
|
+
) as ModalController;
|
|
315
373
|
|
|
316
374
|
if (!controller) {
|
|
317
375
|
throw "Unable to get s-modal controller from element";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as Stacks from "../stacks";
|
|
2
2
|
|
|
3
3
|
export class TabListController extends Stacks.StacksController {
|
|
4
|
-
|
|
5
4
|
private boundSelectTab!: (event: MouseEvent) => void;
|
|
6
5
|
private boundHandleKeydown!: (event: KeyboardEvent) => void;
|
|
7
6
|
|
|
@@ -59,8 +58,12 @@ export class TabListController extends Stacks.StacksController {
|
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
// Use circular navigation when users go past the first or last tab.
|
|
62
|
-
if (tabIndex < 0) {
|
|
63
|
-
|
|
61
|
+
if (tabIndex < 0) {
|
|
62
|
+
tabIndex = tabs.length - 1;
|
|
63
|
+
}
|
|
64
|
+
if (tabIndex >= tabs.length) {
|
|
65
|
+
tabIndex = 0;
|
|
66
|
+
}
|
|
64
67
|
|
|
65
68
|
tabElement = tabs[tabIndex];
|
|
66
69
|
this.switchToTab(tabElement);
|
|
@@ -74,23 +77,30 @@ export class TabListController extends Stacks.StacksController {
|
|
|
74
77
|
* the s-navigation-tablist:select event is prevented.
|
|
75
78
|
*/
|
|
76
79
|
private switchToTab(newTab: HTMLElement) {
|
|
77
|
-
|
|
78
80
|
const oldTab = this.selectedTab;
|
|
79
|
-
if (oldTab === newTab) {
|
|
81
|
+
if (oldTab === newTab) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
80
84
|
|
|
81
|
-
if (this.triggerEvent("select", { oldTab, newTab }).defaultPrevented) {
|
|
85
|
+
if (this.triggerEvent("select", { oldTab, newTab }).defaultPrevented) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
82
88
|
|
|
83
89
|
this.selectedTab = newTab;
|
|
84
90
|
this.triggerEvent("selected", { oldTab, newTab });
|
|
85
91
|
}
|
|
86
|
-
|
|
92
|
+
|
|
87
93
|
/**
|
|
88
94
|
* Returns the currently selected tab or null if no tabs are selected.
|
|
89
95
|
*/
|
|
90
|
-
public get selectedTab()
|
|
91
|
-
return
|
|
96
|
+
public get selectedTab(): HTMLElement | null {
|
|
97
|
+
return (
|
|
98
|
+
this.tabTargets.find(
|
|
99
|
+
(e) => e.getAttribute("aria-selected") === "true"
|
|
100
|
+
) || null
|
|
101
|
+
);
|
|
92
102
|
}
|
|
93
|
-
|
|
103
|
+
|
|
94
104
|
/**
|
|
95
105
|
* Switches the tablist to the provided tab, updating the tabs and panels
|
|
96
106
|
* to reflect the change.
|
|
@@ -99,20 +109,20 @@ export class TabListController extends Stacks.StacksController {
|
|
|
99
109
|
*/
|
|
100
110
|
public set selectedTab(selectedTab: HTMLElement | null) {
|
|
101
111
|
for (const tab of this.tabTargets) {
|
|
102
|
-
const panelId = tab.getAttribute(
|
|
112
|
+
const panelId = tab.getAttribute("aria-controls");
|
|
103
113
|
const panel = panelId ? document.getElementById(panelId) : null;
|
|
104
114
|
|
|
105
115
|
if (tab === selectedTab) {
|
|
106
|
-
tab.classList.add(
|
|
107
|
-
tab.setAttribute(
|
|
108
|
-
tab.removeAttribute(
|
|
109
|
-
panel?.classList.remove(
|
|
116
|
+
tab.classList.add("is-selected");
|
|
117
|
+
tab.setAttribute("aria-selected", "true");
|
|
118
|
+
tab.removeAttribute("tabindex");
|
|
119
|
+
panel?.classList.remove("d-none");
|
|
110
120
|
} else {
|
|
111
|
-
tab.classList.remove(
|
|
112
|
-
tab.setAttribute(
|
|
113
|
-
tab.setAttribute(
|
|
114
|
-
panel?.classList.add(
|
|
121
|
+
tab.classList.remove("is-selected");
|
|
122
|
+
tab.setAttribute("aria-selected", "false");
|
|
123
|
+
tab.setAttribute("tabindex", "-1");
|
|
124
|
+
panel?.classList.add("d-none");
|
|
115
125
|
}
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
|
-
}
|
|
128
|
+
}
|