@stackoverflow/stacks 1.2.0 → 1.3.2
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/dist/controllers/s-expandable-control.d.ts +1 -1
- package/dist/controllers/s-tooltip.d.ts +16 -1
- package/dist/css/stacks.css +974 -731
- package/dist/css/stacks.min.css +1 -1
- package/dist/js/stacks.js +174 -91
- package/dist/js/stacks.min.js +1 -1
- package/lib/css/atomic/borders.less +1 -0
- package/lib/css/atomic/colors.less +1 -0
- package/lib/css/atomic/misc.less +1 -1
- package/lib/css/atomic/typography.less +0 -6
- package/lib/css/atomic/width-height.less +1 -1
- package/lib/css/components/activity-indicator.less +18 -17
- package/lib/css/components/avatars.less +51 -131
- package/lib/css/components/badges.less +2 -0
- package/lib/css/components/breadcrumbs.less +4 -4
- package/lib/css/components/buttons.less +38 -54
- package/lib/css/components/empty-states.less +15 -0
- package/lib/css/components/{collapsible.less → expandable.less} +0 -0
- package/lib/css/components/inputs.less +44 -110
- package/lib/css/components/labels.less +98 -0
- package/lib/css/components/notices.less +190 -163
- package/lib/css/components/post-summary.less +117 -114
- package/lib/css/components/progress-bars.less +1 -1
- package/lib/css/components/prose.less +4 -4
- package/lib/css/components/spinner.less +39 -1
- package/lib/css/components/tables.less +1 -5
- package/lib/css/components/topbar.less +4 -1
- package/lib/css/components/uploader.less +70 -84
- package/lib/css/exports/constants-colors.less +68 -49
- package/lib/css/stacks-dynamic.less +0 -1
- package/lib/css/stacks-static.less +3 -2
- package/lib/ts/controllers/s-expandable-control.ts +23 -19
- package/lib/ts/controllers/s-modal.ts +16 -16
- package/lib/ts/controllers/s-navigation-tablist.ts +13 -13
- package/lib/ts/controllers/s-popover.ts +26 -18
- package/lib/ts/controllers/s-table.ts +31 -29
- package/lib/ts/controllers/s-tooltip.ts +62 -23
- package/lib/ts/controllers/s-uploader.ts +26 -12
- package/lib/ts/stacks.ts +8 -4
- package/package.json +23 -23
- package/lib/css/components/banners.less +0 -80
- package/lib/css/components/blank-states.less +0 -26
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Stacks from '../stacks';
|
|
2
2
|
|
|
3
3
|
// Radio buttons only trigger a change event when they're *checked*, but not when
|
|
4
|
-
// they're *unchecked*. Therefore, if we have an active `s-
|
|
4
|
+
// they're *unchecked*. Therefore, if we have an active `s-expandable-control` in
|
|
5
5
|
// the document, we listen for change events on *all* radio buttons and find any
|
|
6
6
|
// other radio buttons in the same `name` group, triggering a custom event on all
|
|
7
7
|
// of them so the controller can re-evaluate.
|
|
@@ -20,7 +20,7 @@ function globalChangeListener(e: UIEvent) {
|
|
|
20
20
|
if (other === e.target) {
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
let customEvent;
|
|
24
24
|
try {
|
|
25
25
|
customEvent = new Event(RADIO_OFF_EVENT);
|
|
26
26
|
} catch (ex) {
|
|
@@ -32,7 +32,7 @@ function globalChangeListener(e: UIEvent) {
|
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
let refCount = 0;
|
|
36
36
|
function globalChangeListenerRequired(required: boolean) {
|
|
37
37
|
if (required) {
|
|
38
38
|
refCount++;
|
|
@@ -52,16 +52,16 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
52
52
|
private events!: string[];
|
|
53
53
|
private isCheckable!: boolean;
|
|
54
54
|
private isRadio!: boolean;
|
|
55
|
-
private lastKeydownClickTimestamp
|
|
55
|
+
private lastKeydownClickTimestamp = 0;
|
|
56
56
|
|
|
57
57
|
initialize() {
|
|
58
58
|
if (this.element.nodeName === "INPUT" && ["radio", "checkbox"].indexOf((<HTMLInputElement>this.element).type) >= 0) {
|
|
59
|
-
this.isCollapsed = this._isCollapsedForCheckable;
|
|
59
|
+
this.isCollapsed = this._isCollapsedForCheckable.bind(this);
|
|
60
60
|
this.events = ["change", RADIO_OFF_EVENT];
|
|
61
61
|
this.isCheckable = true;
|
|
62
62
|
this.isRadio = (<HTMLInputElement>this.element).type === "radio";
|
|
63
63
|
} else {
|
|
64
|
-
this.isCollapsed = this._isCollapsedForClickable;
|
|
64
|
+
this.isCollapsed = this._isCollapsedForClickable.bind(this);
|
|
65
65
|
this.events = ["click", "keydown"];
|
|
66
66
|
}
|
|
67
67
|
this.listener = this.listener.bind(this);
|
|
@@ -71,7 +71,7 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
71
71
|
// for non-checkable elements, the initial source of truth is the collapsed/expanded
|
|
72
72
|
// state of the controlled element (unless the element doesn't exist)
|
|
73
73
|
_isCollapsedForClickable() {
|
|
74
|
-
|
|
74
|
+
const cc = this.controlledExpandables;
|
|
75
75
|
// the element is considered collapsed if *any* target element is collapsed
|
|
76
76
|
return cc.length > 0 ? !cc.every(element => element.classList.contains("is-expanded")) : this.element.getAttribute("aria-expanded") === "false";
|
|
77
77
|
};
|
|
@@ -82,7 +82,7 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
82
82
|
};
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
get
|
|
85
|
+
get controlledExpandables() {
|
|
86
86
|
const attr = this.element.getAttribute("aria-controls");
|
|
87
87
|
if (!attr) {
|
|
88
88
|
throw `[aria-controls="targetId1 ... targetIdN"] attribute required`;
|
|
@@ -104,8 +104,8 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
104
104
|
if (!this.data.has("toggle-class")) {
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
const cl = this.element.classList;
|
|
108
|
+
const toggleClass = this.data.get("toggle-class");
|
|
109
109
|
if (!toggleClass) {
|
|
110
110
|
throw "couldn't find toggle class"
|
|
111
111
|
}
|
|
@@ -115,7 +115,7 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
listener(e: Event) {
|
|
118
|
-
|
|
118
|
+
let newCollapsed;
|
|
119
119
|
if (this.isCheckable) {
|
|
120
120
|
newCollapsed = !(<HTMLInputElement>this.element).checked;
|
|
121
121
|
} else {
|
|
@@ -142,7 +142,7 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
this.element.setAttribute("aria-expanded", newCollapsed ? "false" : "true");
|
|
145
|
-
for (
|
|
145
|
+
for (const controlledElement of this.controlledExpandables) {
|
|
146
146
|
controlledElement.classList.toggle("is-expanded", !newCollapsed);
|
|
147
147
|
}
|
|
148
148
|
this._dispatchShowHideEvent(!newCollapsed);
|
|
@@ -151,7 +151,7 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
151
151
|
|
|
152
152
|
connect() {
|
|
153
153
|
this.events.forEach(e => {
|
|
154
|
-
this.element.addEventListener(e, this.listener);
|
|
154
|
+
this.element.addEventListener(e, this.listener.bind(this));
|
|
155
155
|
}, this);
|
|
156
156
|
|
|
157
157
|
if (this.isRadio) {
|
|
@@ -159,15 +159,19 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// synchronize state -- in all cases, this means setting the correct `aria-expanded`
|
|
162
|
-
// attribute; for checkable controls this also means setting the `is-collapsed` class
|
|
163
|
-
|
|
162
|
+
// attribute; for checkable controls this also means setting the `is-collapsed` class.
|
|
163
|
+
// Note: aria-expanded is currently an invalid attribute on radio elements
|
|
164
|
+
// Support for aria-expanded is being debated by the W3C https://github.com/w3c/aria/issues/1404 as recently as June 2022
|
|
165
|
+
if (!this.isRadio) {
|
|
166
|
+
this.element.setAttribute("aria-expanded", this.isCollapsed() ? "false" : "true");
|
|
167
|
+
}
|
|
164
168
|
if (this.isCheckable) {
|
|
165
|
-
|
|
169
|
+
const cc = this.controlledExpandables;
|
|
166
170
|
if (cc.length) {
|
|
167
|
-
|
|
171
|
+
const expected = !this.isCollapsed();
|
|
168
172
|
// if any element does not match the expected state, set them all to the expected state
|
|
169
173
|
if (cc.some(element => element.classList.contains("is-expanded") !== expected)) {
|
|
170
|
-
for (
|
|
174
|
+
for (const controlledElement of this.controlledExpandables) {
|
|
171
175
|
controlledElement.classList.toggle("is-expanded", expected);
|
|
172
176
|
}
|
|
173
177
|
this._dispatchShowHideEvent(expected);
|
|
@@ -179,7 +183,7 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
179
183
|
|
|
180
184
|
disconnect() {
|
|
181
185
|
this.events.forEach(e => {
|
|
182
|
-
this.element.removeEventListener(e, this.listener);
|
|
186
|
+
this.element.removeEventListener(e, this.listener.bind(this));
|
|
183
187
|
}, this);
|
|
184
188
|
|
|
185
189
|
if (this.isRadio) {
|
|
@@ -6,12 +6,12 @@ export class ModalController extends Stacks.StacksController {
|
|
|
6
6
|
private modalTarget!: HTMLElement;
|
|
7
7
|
private initialFocusTargets!: HTMLElement[];
|
|
8
8
|
|
|
9
|
-
private _boundClickFn!:
|
|
10
|
-
private _boundKeypressFn!:
|
|
9
|
+
private _boundClickFn!: (event: MouseEvent) => void;
|
|
10
|
+
private _boundKeypressFn!: (event: KeyboardEvent) => void;
|
|
11
11
|
|
|
12
12
|
private returnElement!: HTMLElement;
|
|
13
13
|
|
|
14
|
-
private _boundTabTrap!:
|
|
14
|
+
private _boundTabTrap!: (event: KeyboardEvent) => void;
|
|
15
15
|
|
|
16
16
|
connect () {
|
|
17
17
|
this.validate();
|
|
@@ -50,7 +50,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
50
50
|
*/
|
|
51
51
|
private validate() {
|
|
52
52
|
// check for returnElement support
|
|
53
|
-
|
|
53
|
+
const returnElementSelector = this.data.get("return-element");
|
|
54
54
|
if (returnElementSelector) {
|
|
55
55
|
this.returnElement = <HTMLElement>document.querySelector(returnElementSelector);
|
|
56
56
|
|
|
@@ -65,8 +65,8 @@ export class ModalController extends Stacks.StacksController {
|
|
|
65
65
|
* @param show Optional parameter that force shows/hides the element or toggles it if left undefined
|
|
66
66
|
*/
|
|
67
67
|
private _toggle (show?: boolean | undefined, dispatcher: Event|Element|null = null) {
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
let toShow = show;
|
|
69
|
+
const isVisible = this.modalTarget.getAttribute("aria-hidden") === "false";
|
|
70
70
|
|
|
71
71
|
// if we're letting the class toggle, we need to figure out if the popover is visible manually
|
|
72
72
|
if (typeof toShow === "undefined") {
|
|
@@ -78,10 +78,10 @@ export class ModalController extends Stacks.StacksController {
|
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
const dispatchingElement = this.getDispatcher(dispatcher);
|
|
82
82
|
|
|
83
83
|
// show/hide events trigger before toggling the class
|
|
84
|
-
|
|
84
|
+
const triggeredEvent = this.triggerEvent(toShow ? "show" : "hide", {
|
|
85
85
|
returnElement: this.returnElement,
|
|
86
86
|
dispatcher: this.getDispatcher(dispatchingElement)
|
|
87
87
|
}, this.modalTarget);
|
|
@@ -105,7 +105,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// check for transitionend support
|
|
108
|
-
|
|
108
|
+
const supportsTransitionEnd = (this.modalTarget).ontransitionend !== undefined;
|
|
109
109
|
|
|
110
110
|
// shown/hidden events trigger after toggling the class
|
|
111
111
|
if (supportsTransitionEnd) {
|
|
@@ -182,7 +182,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
182
182
|
*/
|
|
183
183
|
private focusInsideModal() {
|
|
184
184
|
this.modalTarget.addEventListener("s-modal:shown", () => {
|
|
185
|
-
|
|
185
|
+
const initialFocus = this.firstVisible(this.initialFocusTargets) ?? this.firstVisible(this.getAllTabbables());
|
|
186
186
|
initialFocus?.focus();
|
|
187
187
|
}, {once: true });
|
|
188
188
|
}
|
|
@@ -194,7 +194,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
194
194
|
|
|
195
195
|
// If somehow the user has tabbed out of the modal or if focus started outside the modal, push them to the first item.
|
|
196
196
|
if (!this.modalTarget.contains(<Element>e.target)) {
|
|
197
|
-
|
|
197
|
+
const focusTarget = this.firstVisible(this.getAllTabbables());
|
|
198
198
|
if (focusTarget) {
|
|
199
199
|
e.preventDefault();
|
|
200
200
|
focusTarget.focus();
|
|
@@ -205,10 +205,10 @@ export class ModalController extends Stacks.StacksController {
|
|
|
205
205
|
|
|
206
206
|
// If we observe a tab keydown and we're on an edge, cycle the focus to the other side.
|
|
207
207
|
if (e.key === "Tab") {
|
|
208
|
-
|
|
208
|
+
const tabbables = this.getAllTabbables();
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
const firstTabbable = this.firstVisible(tabbables);
|
|
211
|
+
const lastTabbable = this.lastVisible(tabbables);
|
|
212
212
|
|
|
213
213
|
if (firstTabbable && lastTabbable) {
|
|
214
214
|
if (firstTabbable === lastTabbable) {
|
|
@@ -252,7 +252,7 @@ export class ModalController extends Stacks.StacksController {
|
|
|
252
252
|
* Forces the popover to hide if a user clicks outside of it or its reference element
|
|
253
253
|
*/
|
|
254
254
|
private hideOnOutsideClick (e: Event) {
|
|
255
|
-
|
|
255
|
+
const target = <Node>e.target;
|
|
256
256
|
// check if the document was clicked inside either the toggle element or the modal itself
|
|
257
257
|
// note: .contains also returns true if the node itself matches the target element
|
|
258
258
|
if (!this.modalTarget.querySelector(".s-modal--dialog")!.contains(target) && document.body.contains(target)) {
|
|
@@ -311,7 +311,7 @@ export function hideModal(element: HTMLElement) {
|
|
|
311
311
|
* @param show whether to force show/hide the modal; toggles the modal if left undefined
|
|
312
312
|
*/
|
|
313
313
|
function toggleModal(element: HTMLElement, show?: boolean | undefined) {
|
|
314
|
-
|
|
314
|
+
const controller = Stacks.application.getControllerForElementAndIdentifier(element, "s-modal") as ModalController;
|
|
315
315
|
|
|
316
316
|
if (!controller) {
|
|
317
317
|
throw "Unable to get s-modal controller from element";
|
|
@@ -2,8 +2,8 @@ import * as Stacks from "../stacks";
|
|
|
2
2
|
|
|
3
3
|
export class TabListController extends Stacks.StacksController {
|
|
4
4
|
|
|
5
|
-
private boundSelectTab
|
|
6
|
-
private boundHandleKeydown
|
|
5
|
+
private boundSelectTab!: (event: MouseEvent) => void;
|
|
6
|
+
private boundHandleKeydown!: (event: KeyboardEvent) => void;
|
|
7
7
|
|
|
8
8
|
connect() {
|
|
9
9
|
super.connect();
|
|
@@ -11,7 +11,7 @@ export class TabListController extends Stacks.StacksController {
|
|
|
11
11
|
this.boundSelectTab = this.selectTab.bind(this);
|
|
12
12
|
this.boundHandleKeydown = this.handleKeydown.bind(this);
|
|
13
13
|
|
|
14
|
-
for (
|
|
14
|
+
for (const tab of this.tabTargets) {
|
|
15
15
|
tab.addEventListener("click", this.boundSelectTab);
|
|
16
16
|
tab.addEventListener("keydown", this.boundHandleKeydown);
|
|
17
17
|
}
|
|
@@ -20,7 +20,7 @@ export class TabListController extends Stacks.StacksController {
|
|
|
20
20
|
disconnect() {
|
|
21
21
|
super.disconnect();
|
|
22
22
|
|
|
23
|
-
for (
|
|
23
|
+
for (const tab of this.tabTargets) {
|
|
24
24
|
tab.removeEventListener("click", this.boundSelectTab);
|
|
25
25
|
tab.removeEventListener("keydown", this.boundHandleKeydown);
|
|
26
26
|
}
|
|
@@ -29,8 +29,8 @@ export class TabListController extends Stacks.StacksController {
|
|
|
29
29
|
/**
|
|
30
30
|
* Gets all tabs within the controller.
|
|
31
31
|
*/
|
|
32
|
-
get tabTargets() {
|
|
33
|
-
return
|
|
32
|
+
get tabTargets(): HTMLElement[] {
|
|
33
|
+
return Array.from(this.element.querySelectorAll("[role=tab]"));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
@@ -47,8 +47,8 @@ export class TabListController extends Stacks.StacksController {
|
|
|
47
47
|
handleKeydown(event: KeyboardEvent) {
|
|
48
48
|
let tabElement = <HTMLElement>event.currentTarget;
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const tabs = this.tabTargets;
|
|
51
|
+
let tabIndex = tabs.indexOf(tabElement);
|
|
52
52
|
|
|
53
53
|
if (event.key === "ArrowRight") {
|
|
54
54
|
tabIndex++;
|
|
@@ -62,7 +62,7 @@ export class TabListController extends Stacks.StacksController {
|
|
|
62
62
|
if (tabIndex < 0) { tabIndex = tabs.length - 1; }
|
|
63
63
|
if (tabIndex >= tabs.length) { tabIndex = 0; }
|
|
64
64
|
|
|
65
|
-
tabElement =
|
|
65
|
+
tabElement = tabs[tabIndex];
|
|
66
66
|
this.switchToTab(tabElement);
|
|
67
67
|
|
|
68
68
|
// Focus the newly selected tab so it can receive keyboard events.
|
|
@@ -75,7 +75,7 @@ export class TabListController extends Stacks.StacksController {
|
|
|
75
75
|
*/
|
|
76
76
|
private switchToTab(newTab: HTMLElement) {
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
const oldTab = this.selectedTab;
|
|
79
79
|
if (oldTab === newTab) { return; }
|
|
80
80
|
|
|
81
81
|
if (this.triggerEvent("select", { oldTab, newTab }).defaultPrevented) { return; }
|
|
@@ -98,9 +98,9 @@ export class TabListController extends Stacks.StacksController {
|
|
|
98
98
|
* is not a valid tab, all tabs will be unselected.
|
|
99
99
|
*/
|
|
100
100
|
public set selectedTab(selectedTab: HTMLElement | null) {
|
|
101
|
-
for (
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
for (const tab of this.tabTargets) {
|
|
102
|
+
const panelId = tab.getAttribute('aria-controls');
|
|
103
|
+
const panel = panelId ? document.getElementById(panelId) : null;
|
|
104
104
|
|
|
105
105
|
if (tab === selectedTab) {
|
|
106
106
|
tab.classList.add('is-selected');
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createPopper, Placement } from '@popperjs/core';
|
|
2
|
+
import type * as Popper from '@popperjs/core';
|
|
2
3
|
import * as Stacks from "../stacks";
|
|
3
4
|
|
|
4
5
|
type OutsideClickBehavior = "always" | "never" | "if-in-viewport" | "after-dismissal";
|
|
5
6
|
|
|
6
7
|
export abstract class BasePopoverController extends Stacks.StacksController {
|
|
7
|
-
|
|
8
|
-
private popper!: Popper;
|
|
8
|
+
private popper!: Popper.Instance;
|
|
9
9
|
|
|
10
10
|
protected popoverElement!: HTMLElement;
|
|
11
11
|
|
|
@@ -87,6 +87,8 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
87
87
|
this.hide();
|
|
88
88
|
if (this.popper) {
|
|
89
89
|
this.popper.destroy();
|
|
90
|
+
// eslint-disable-next-line
|
|
91
|
+
// @ts-ignore The operand of a 'delete' operator must be optional .ts(2790)
|
|
90
92
|
delete this.popper;
|
|
91
93
|
}
|
|
92
94
|
super.disconnect();
|
|
@@ -105,7 +107,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
105
107
|
show(dispatcher: Event|Element|null = null) {
|
|
106
108
|
if (this.isVisible) { return; }
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
const dispatcherElement = this.getDispatcher(dispatcher);
|
|
109
111
|
|
|
110
112
|
if (this.triggerEvent("show", {
|
|
111
113
|
dispatcher: dispatcherElement
|
|
@@ -115,7 +117,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
115
117
|
this.initializePopper();
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
this.popoverElement
|
|
120
|
+
this.popoverElement.classList.add("is-visible");
|
|
119
121
|
|
|
120
122
|
// ensure the popper has been positioned correctly
|
|
121
123
|
this.scheduleUpdate();
|
|
@@ -129,7 +131,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
129
131
|
hide(dispatcher: Event|Element|null = null) {
|
|
130
132
|
if (!this.isVisible) { return; }
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
const dispatcherElement = this.getDispatcher(dispatcher);
|
|
133
135
|
|
|
134
136
|
if (this.triggerEvent("hide", {
|
|
135
137
|
dispatcher: dispatcherElement
|
|
@@ -140,6 +142,8 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
140
142
|
if (this.popper) {
|
|
141
143
|
// completely destroy the popper on hide; this is in line with Popper.js's performance recommendations
|
|
142
144
|
this.popper.destroy();
|
|
145
|
+
// eslint-disable-next-line
|
|
146
|
+
// @ts-ignore The operand of a 'delete' operator must be optional .ts(2790)
|
|
143
147
|
delete this.popper;
|
|
144
148
|
}
|
|
145
149
|
|
|
@@ -182,7 +186,6 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
182
186
|
* Initializes the Popper for this instance
|
|
183
187
|
*/
|
|
184
188
|
private initializePopper() {
|
|
185
|
-
// @ts-ignore
|
|
186
189
|
this.popper = createPopper(this.referenceElement, this.popoverElement, {
|
|
187
190
|
placement: this.data.get("placement") as Placement || "bottom",
|
|
188
191
|
modifiers: [
|
|
@@ -206,7 +209,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
206
209
|
* Validates the popover settings and attempts to set necessary internal variables
|
|
207
210
|
*/
|
|
208
211
|
private validate() {
|
|
209
|
-
|
|
212
|
+
const referenceSelector = this.data.get("reference-selector");
|
|
210
213
|
|
|
211
214
|
this.referenceElement = <HTMLElement>this.element;
|
|
212
215
|
|
|
@@ -221,7 +224,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
221
224
|
|
|
222
225
|
const popoverId = this.referenceElement.getAttribute(this.popoverSelectorAttribute);
|
|
223
226
|
|
|
224
|
-
|
|
227
|
+
let popoverElement: HTMLElement | null = null;
|
|
225
228
|
|
|
226
229
|
// if the popover is named, attempt to fetch it (and throw an error if it doesn't exist)
|
|
227
230
|
if (popoverId) {
|
|
@@ -264,7 +267,7 @@ export abstract class BasePopoverController extends Stacks.StacksController {
|
|
|
264
267
|
*/
|
|
265
268
|
protected scheduleUpdate() {
|
|
266
269
|
if (this.popper && this.isVisible) {
|
|
267
|
-
this.popper.update();
|
|
270
|
+
void this.popper.update();
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
}
|
|
@@ -274,8 +277,8 @@ export class PopoverController extends BasePopoverController {
|
|
|
274
277
|
|
|
275
278
|
protected popoverSelectorAttribute = "aria-controls";
|
|
276
279
|
|
|
277
|
-
private boundHideOnOutsideClick!:
|
|
278
|
-
private boundHideOnEscapePress!:
|
|
280
|
+
private boundHideOnOutsideClick!: (event: MouseEvent) => void;
|
|
281
|
+
private boundHideOnEscapePress!: (event: KeyboardEvent) => void;
|
|
279
282
|
|
|
280
283
|
/**
|
|
281
284
|
* Toggles optional classes and accessibility attributes in addition to BasePopoverController.shown
|
|
@@ -332,7 +335,7 @@ export class PopoverController extends BasePopoverController {
|
|
|
332
335
|
const target = <Node>e.target;
|
|
333
336
|
// check if the document was clicked inside either the reference element or the popover itself
|
|
334
337
|
// note: .contains also returns true if the node itself matches the target element
|
|
335
|
-
if (this.shouldHideOnOutsideClick && !this.referenceElement.contains(target) && !this.popoverElement
|
|
338
|
+
if (this.shouldHideOnOutsideClick && !this.referenceElement.contains(target) && !this.popoverElement.contains(target) && document.body.contains(target)) {
|
|
336
339
|
this.hide(e);
|
|
337
340
|
}
|
|
338
341
|
};
|
|
@@ -349,7 +352,7 @@ export class PopoverController extends BasePopoverController {
|
|
|
349
352
|
|
|
350
353
|
// check if the target was inside the popover element and refocus the triggering element
|
|
351
354
|
// note: .contains also returns true if the node itself matches the target element
|
|
352
|
-
if (this.popoverElement
|
|
355
|
+
if (this.popoverElement.contains(<Node>e.target)) {
|
|
353
356
|
this.referenceElement.focus();
|
|
354
357
|
}
|
|
355
358
|
|
|
@@ -364,8 +367,10 @@ export class PopoverController extends BasePopoverController {
|
|
|
364
367
|
if (!this.data.has("toggle-class")) {
|
|
365
368
|
return;
|
|
366
369
|
}
|
|
367
|
-
|
|
368
|
-
this.data.get("toggle-class")
|
|
370
|
+
|
|
371
|
+
const toggleClass = this.data.get("toggle-class") || "";
|
|
372
|
+
const cl = this.referenceElement.classList;
|
|
373
|
+
toggleClass.split(/\s+/).forEach(function (cls: string) {
|
|
369
374
|
cl.toggle(cls, show);
|
|
370
375
|
});
|
|
371
376
|
}
|
|
@@ -375,7 +380,9 @@ export class PopoverController extends BasePopoverController {
|
|
|
375
380
|
* @param {boolean=} show - A boolean indicating whether this is being triggered by a show or hide.
|
|
376
381
|
*/
|
|
377
382
|
private toggleAccessibilityAttributes(show?: boolean) {
|
|
378
|
-
|
|
383
|
+
const expandedValue = show?.toString() || this.referenceElement.ariaExpanded || "false";
|
|
384
|
+
this.referenceElement.ariaExpanded = expandedValue;
|
|
385
|
+
this.referenceElement.setAttribute("aria-expanded", expandedValue);
|
|
379
386
|
}
|
|
380
387
|
}
|
|
381
388
|
|
|
@@ -453,6 +460,7 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
453
460
|
}
|
|
454
461
|
|
|
455
462
|
if (typeof popover === 'string') {
|
|
463
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
456
464
|
const elements = document.createRange().createContextualFragment(popover).children;
|
|
457
465
|
if (elements.length !== 1) {
|
|
458
466
|
throw "popover should contain a single element";
|
|
@@ -461,7 +469,7 @@ export function attachPopover(element: Element, popover: Element | string, optio
|
|
|
461
469
|
}
|
|
462
470
|
|
|
463
471
|
const existingId = referenceElement.getAttribute("aria-controls");
|
|
464
|
-
|
|
472
|
+
let popoverId = popover.id;
|
|
465
473
|
|
|
466
474
|
if (!popover.classList.contains('s-popover')) {
|
|
467
475
|
throw `popover should have the "s-popover" class but had class="${popover.className}"`;
|
|
@@ -557,7 +565,7 @@ function getPopover(element: Element): GetPopoverResult {
|
|
|
557
565
|
* @param include Whether to add the controllerName value
|
|
558
566
|
*/
|
|
559
567
|
function toggleController(el: Element, controllerName: string, include: boolean) {
|
|
560
|
-
|
|
568
|
+
const controllers = new Set(el.getAttribute('data-controller')?.split(/\s+/));
|
|
561
569
|
if (include) {
|
|
562
570
|
controllers.add(controllerName);
|
|
563
571
|
} else {
|
|
@@ -9,14 +9,15 @@ export class TableController extends Stacks.StacksController {
|
|
|
9
9
|
if (["asc", "desc", "none"].indexOf(direction) < 0) {
|
|
10
10
|
throw "direction must be one of asc, desc, or none"
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
13
|
+
const controller = this;
|
|
13
14
|
this.columnTargets.forEach(function (target) {
|
|
14
|
-
|
|
15
|
+
const isCurrrent = target === headElem;
|
|
15
16
|
|
|
16
17
|
target.classList.toggle("is-sorted", isCurrrent && direction !== "none");
|
|
17
18
|
|
|
18
19
|
target.querySelectorAll(".js-sorting-indicator").forEach(function (icon) {
|
|
19
|
-
|
|
20
|
+
const visible = isCurrrent ? direction : "none";
|
|
20
21
|
icon.classList.toggle("d-none", !icon.classList.contains("js-sorting-indicator-" + visible));
|
|
21
22
|
});
|
|
22
23
|
|
|
@@ -29,16 +30,17 @@ export class TableController extends Stacks.StacksController {
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
sort(evt: Event) {
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
34
|
+
const controller = this;
|
|
35
|
+
const colHead = evt.currentTarget;
|
|
34
36
|
if (!(colHead instanceof HTMLTableCellElement)) {
|
|
35
37
|
throw "invalid event target";
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const table = this.element as HTMLTableElement;
|
|
40
|
+
const tbody = table.tBodies[0];
|
|
39
41
|
|
|
40
42
|
// the column slot number of the clicked header
|
|
41
|
-
|
|
43
|
+
const colno = getCellSlot(colHead);
|
|
42
44
|
|
|
43
45
|
if (colno < 0) { // this shouldn't happen if the clicked element is actually a column head
|
|
44
46
|
return;
|
|
@@ -46,23 +48,23 @@ export class TableController extends Stacks.StacksController {
|
|
|
46
48
|
|
|
47
49
|
// an index of the <tbody>, so we can find out for each row which <td> element is
|
|
48
50
|
// in the same column slot as the header
|
|
49
|
-
|
|
51
|
+
const slotIndex = buildIndex(tbody);
|
|
50
52
|
|
|
51
53
|
// the default behavior when clicking a header is to sort by this column in ascending
|
|
52
54
|
// direction, *unless* it is already sorted that way
|
|
53
|
-
|
|
55
|
+
const direction = this.getElementData(colHead, "sort-direction") === "asc" ? -1 : 1;
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
const rows = Array.from(table.tBodies[0].rows);
|
|
56
58
|
|
|
57
59
|
// if this is still false after traversing the data, that means all values are integers (or empty)
|
|
58
60
|
// and thus we'll sort numerically.
|
|
59
|
-
|
|
61
|
+
let anyNonInt = false;
|
|
60
62
|
|
|
61
63
|
// data will be a list of tuples [value, rowNum], where value is what we're sorting by
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const data: [string | number, number][] = [];
|
|
65
|
+
let firstBottomRow: HTMLTableRowElement;
|
|
64
66
|
rows.forEach(function (row, index) {
|
|
65
|
-
|
|
67
|
+
const force = controller.getElementData(row, "sort-to");
|
|
66
68
|
if (force === "top") {
|
|
67
69
|
return; // rows not added to the list will automatically end up at the top
|
|
68
70
|
} else if (force === "bottom") {
|
|
@@ -71,7 +73,7 @@ export class TableController extends Stacks.StacksController {
|
|
|
71
73
|
}
|
|
72
74
|
return;
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
const cell = slotIndex[index][colno];
|
|
75
77
|
if (!cell) {
|
|
76
78
|
data.push(["", index]);
|
|
77
79
|
return;
|
|
@@ -79,10 +81,10 @@ export class TableController extends Stacks.StacksController {
|
|
|
79
81
|
|
|
80
82
|
// unless the to-be-sorted-by value is explicitly provided on the element via this attribute,
|
|
81
83
|
// the value we're using is the cell's text, trimmed of any whitespace
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
const explicit = controller.getElementData(cell, "sort-val");
|
|
85
|
+
const d = typeof explicit === "string" ? explicit : cell.textContent!.trim();
|
|
84
86
|
|
|
85
|
-
if ((d !== "") && (parseInt(d, 10)
|
|
87
|
+
if ((d !== "") && (`${parseInt(d, 10)}` !== d)) {
|
|
86
88
|
anyNonInt = true;
|
|
87
89
|
}
|
|
88
90
|
data.push([d, index]);
|
|
@@ -114,7 +116,7 @@ export class TableController extends Stacks.StacksController {
|
|
|
114
116
|
|
|
115
117
|
// this is the actual reordering of the table rows
|
|
116
118
|
data.forEach(function (tup) {
|
|
117
|
-
|
|
119
|
+
const row = rows[tup[1]];
|
|
118
120
|
row.parentElement!.removeChild(row);
|
|
119
121
|
if (firstBottomRow) {
|
|
120
122
|
tbody.insertBefore(row, firstBottomRow);
|
|
@@ -165,29 +167,29 @@ function getCellSlot(cell: HTMLTableCellElement): number {
|
|
|
165
167
|
// If the second argument is given, it's a <td> or <th> that we're trying to find, and the algorithm
|
|
166
168
|
// stops as soon as it has found it and the function returns its slot number.
|
|
167
169
|
function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HTMLTableCellElement) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const index = [];
|
|
171
|
+
let curRow = section.children[0];
|
|
170
172
|
|
|
171
173
|
// the elements of these two arrays are synchronized; the first array contains table cell elements,
|
|
172
174
|
// the second one contains a number that indicates for how many more rows this elements will
|
|
173
175
|
// exist (i.e. the value is initially one less than the cell's rowspan, and will be decreased for each row)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const growing: HTMLTableCellElement[] = [];
|
|
177
|
+
const growingRowsLeft: number[] = [];
|
|
176
178
|
|
|
177
179
|
// continue while we have actual <tr>'s left *or* we still have rowspan'ed elements that aren't done
|
|
178
180
|
while (curRow || growingRowsLeft.some(function (e) { return e !== 0; })) {
|
|
179
|
-
|
|
181
|
+
const curIndexRow: HTMLTableCellElement[] = [];
|
|
180
182
|
index.push(curIndexRow);
|
|
181
183
|
|
|
182
|
-
|
|
184
|
+
let curSlot = 0;
|
|
183
185
|
if (curRow) {
|
|
184
|
-
for (
|
|
186
|
+
for (let curCellInd = 0; curCellInd < curRow.children.length; curCellInd++) {
|
|
185
187
|
while (growingRowsLeft[curSlot]) {
|
|
186
188
|
growingRowsLeft[curSlot]--;
|
|
187
189
|
curIndexRow[curSlot] = growing[curSlot];
|
|
188
190
|
curSlot++;
|
|
189
191
|
}
|
|
190
|
-
|
|
192
|
+
const cell = curRow.children[curCellInd];
|
|
191
193
|
if (!(cell instanceof HTMLTableCellElement)) {
|
|
192
194
|
throw "invalid table"
|
|
193
195
|
}
|
|
@@ -197,7 +199,7 @@ function buildIndexOrGetCellSlot(section: HTMLTableSectionElement, findCell?: HT
|
|
|
197
199
|
if (cell === findCell) {
|
|
198
200
|
return curSlot;
|
|
199
201
|
}
|
|
200
|
-
|
|
202
|
+
const nextFreeSlot = curSlot + cell.colSpan;
|
|
201
203
|
for (; curSlot < nextFreeSlot; curSlot++) {
|
|
202
204
|
growingRowsLeft[curSlot] = cell.rowSpan - 1; // if any of these is already growing, the table is broken -- no guarantees of anything
|
|
203
205
|
growing[curSlot] = cell;
|