@stackoverflow/stacks 1.3.0 → 1.3.1
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 +715 -527
- package/dist/css/stacks.min.css +1 -1
- package/dist/js/stacks.js +153 -84
- package/dist/js/stacks.min.js +1 -1
- package/lib/css/atomic/misc.less +1 -1
- package/lib/css/atomic/typography.less +0 -6
- package/lib/css/components/activity-indicator.less +18 -17
- package/lib/css/components/avatars.less +50 -131
- package/lib/css/components/breadcrumbs.less +4 -4
- package/lib/css/components/buttons.less +8 -48
- 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 +37 -101
- package/lib/css/components/labels.less +98 -0
- package/lib/css/components/notices.less +190 -163
- package/lib/css/components/post-summary.less +4 -4
- 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/uploader.less +70 -84
- package/lib/css/exports/constants-colors.less +14 -0
- 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/stacks.ts +8 -4
- package/package.json +17 -17
- package/lib/css/components/banners.less +0 -80
- package/lib/css/components/blank-states.less +0 -26
|
@@ -7,106 +7,91 @@
|
|
|
7
7
|
// visit https://stackoverflow.design/
|
|
8
8
|
|
|
9
9
|
.s-uploader {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
min-height: var(--su-static128);
|
|
15
|
-
justify-content: center;
|
|
16
|
-
padding: var(--su8) var(--su16);
|
|
17
|
-
position: relative;
|
|
18
|
-
text-align: center;
|
|
19
|
-
|
|
20
|
-
// Add the dashed border as an SVG background mask
|
|
21
|
-
&:before {
|
|
22
|
-
--s-uploader-background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='5' ry='5' stroke='%23000000' stroke-width='8' stroke-dasharray='7%2c 22' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
|
23
|
-
content: '';
|
|
24
|
-
display: block;
|
|
25
|
-
position: absolute;
|
|
26
|
-
top: 0;
|
|
27
|
-
left: 0;
|
|
28
|
-
right: 0;
|
|
29
|
-
bottom: 0;
|
|
30
|
-
background-color: var(--black-150);
|
|
31
|
-
-webkit-mask-image: var(--s-uploader-background-image);
|
|
32
|
-
mask-image: var(--s-uploader-background-image);
|
|
33
|
-
border-radius: var(--br-lg);
|
|
10
|
+
--_bg: var(--black-025);
|
|
11
|
+
--_bg-focus: var(--black-050);
|
|
12
|
+
--_bg-bc: var(--black-150);
|
|
13
|
+
--_focus-ring-color: var(--focus-ring);
|
|
34
14
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
}
|
|
15
|
+
// Static custom properties (not redefined but repeated enough to warrant a custom property)
|
|
16
|
+
--_bg-b-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='5' ry='5' stroke='%23000000' stroke-width='8' stroke-dasharray='7%2c 22' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
|
39
17
|
|
|
40
|
-
|
|
41
|
-
|
|
18
|
+
.highcontrast-mode({
|
|
19
|
+
--_bg-bc-hc: var(--black-400);
|
|
20
|
+
});
|
|
42
21
|
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
&.is-active {
|
|
23
|
+
--_bg: var(--black-050);
|
|
24
|
+
--_bg-bc: var(--black-200);
|
|
25
|
+
}
|
|
45
26
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
27
|
+
&.is-disabled {
|
|
28
|
+
opacity: var(--_o-disabled-static);
|
|
29
|
+
}
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
&.has-error {
|
|
32
|
+
--_bg: var(--red-050);
|
|
33
|
+
--_bg-focus: var(--red-100);
|
|
34
|
+
--_bg-bc: var(--red-400);
|
|
35
|
+
--_bg-bc-hc-state: var(--red-400);
|
|
36
|
+
--_focus-ring-color: var(--focus-ring-error);
|
|
37
|
+
--_link-fc: var(--red-900);
|
|
54
38
|
}
|
|
55
39
|
|
|
56
40
|
&.has-success {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
background-color: var(--green-400);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.s-link {
|
|
68
|
-
color: var(--green-900);
|
|
69
|
-
}
|
|
41
|
+
--_bg: var(--green-025);
|
|
42
|
+
--_bg-focus: var(--green-050);
|
|
43
|
+
--_bg-bc: var(--green-400);
|
|
44
|
+
--_bg-bc-hc-state: var(--green-400);
|
|
45
|
+
--_focus-ring-color: var(--focus-ring-success);
|
|
46
|
+
--_link-fc: var(--green-900);
|
|
70
47
|
}
|
|
71
48
|
|
|
72
49
|
&.has-warning {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
background-color: var(--yellow-400);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.s-link {
|
|
84
|
-
color: var(--yellow-900);
|
|
85
|
-
}
|
|
50
|
+
--_bg: var(--yellow-050);
|
|
51
|
+
--_bg-focus: var(--yellow-100);
|
|
52
|
+
--_bg-bc: var(--yellow-400);
|
|
53
|
+
--_bg-bc-hc-state: var(--yellow-400);
|
|
54
|
+
--_focus-ring-color: var(--focus-ring-warning);
|
|
55
|
+
--_link-fc: var(--yellow-900);
|
|
86
56
|
}
|
|
87
57
|
|
|
88
|
-
&.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.highcontrast-mode({
|
|
95
|
-
background-color: var(--black);
|
|
96
|
-
});
|
|
58
|
+
&.has-error,
|
|
59
|
+
&.has-success,
|
|
60
|
+
&.has-warning {
|
|
61
|
+
.s-link {
|
|
62
|
+
color: var(--_link-fc);
|
|
97
63
|
}
|
|
98
64
|
}
|
|
99
65
|
|
|
100
|
-
&.is-disabled {
|
|
101
|
-
opacity: 0.5;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
66
|
// This is to for safari shadow DOM
|
|
105
67
|
// see https://github.com/StackExchange/Stacks/pull/690#issuecomment-861028193
|
|
106
68
|
input[type="file"]::file-selector-button {
|
|
107
69
|
cursor: pointer;
|
|
108
70
|
}
|
|
109
|
-
|
|
71
|
+
.s-uploader--container {
|
|
72
|
+
align-items: center;
|
|
73
|
+
background-color: var(--_bg);
|
|
74
|
+
border-radius: var(--br-lg);
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
justify-content: center;
|
|
78
|
+
min-height: var(--su-static128);
|
|
79
|
+
padding: var(--su8) var(--su16);
|
|
80
|
+
position: relative;
|
|
81
|
+
text-align: center;
|
|
82
|
+
|
|
83
|
+
// Add the dashed border as an SVG background mask
|
|
84
|
+
&:before {
|
|
85
|
+
-webkit-mask-image: var(--_bg-b-image);
|
|
86
|
+
mask-image: var(--_bg-b-image);
|
|
87
|
+
background-color: var(--_bg-bc-hc-state, var(--_bg-bc-hc, var(--_bg-bc)));
|
|
88
|
+
content: '';
|
|
89
|
+
border-radius: var(--br-lg);
|
|
90
|
+
display: block;
|
|
91
|
+
position: absolute;
|
|
92
|
+
inset: 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
110
95
|
.s-uploader--input {
|
|
111
96
|
cursor: pointer;
|
|
112
97
|
height: 100%;
|
|
@@ -114,13 +99,13 @@
|
|
|
114
99
|
opacity: 0;
|
|
115
100
|
position: absolute;
|
|
116
101
|
width: 100%;
|
|
102
|
+
z-index: var(--zi-selected);
|
|
117
103
|
|
|
118
104
|
&:focus:focus-visible + .s-uploader--container {
|
|
119
|
-
background-color: var(--
|
|
120
|
-
box-shadow: 0 0 0 var(--su-static4) var(--
|
|
105
|
+
background-color: var(--_bg-focus);
|
|
106
|
+
box-shadow: 0 0 0 var(--su-static4) var(--_focus-ring-color);
|
|
121
107
|
}
|
|
122
108
|
}
|
|
123
|
-
|
|
124
109
|
.s-uploader--preview {
|
|
125
110
|
max-width: 100%;
|
|
126
111
|
pointer-events: none;
|
|
@@ -185,8 +170,7 @@
|
|
|
185
170
|
object-fit: cover;
|
|
186
171
|
}
|
|
187
172
|
&:not(img) {
|
|
188
|
-
|
|
189
|
-
background-image: var(--s-uploader--preview-document-icon);
|
|
173
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' aria-hidden='true' fill='%23535A60' width='18' height='18' viewBox='0 0 18 18'%3E%3Cpath d='M3 3a2 2 0 012-2h6l4 4v10a2 2 0 01-2 2H5a2 2 0 01-2-2V3zm7-1.5V6h4.5L10 1.5z'%3E%3C/path%3E%3C/svg%3E");
|
|
190
174
|
background-position: center;
|
|
191
175
|
background-repeat: no-repeat;
|
|
192
176
|
}
|
|
@@ -207,4 +191,6 @@
|
|
|
207
191
|
top: var(--su8);
|
|
208
192
|
z-index: var(--zi-active);
|
|
209
193
|
}
|
|
194
|
+
|
|
195
|
+
position: relative;
|
|
210
196
|
}
|
|
@@ -409,6 +409,10 @@
|
|
|
409
409
|
--focus-ring-error: hsla(@red-h, 62%, 47%, 15%);
|
|
410
410
|
--focus-ring-muted: hsla(@black-h, @black-s, 15%, 10%);
|
|
411
411
|
|
|
412
|
+
// Opacity
|
|
413
|
+
--_o-disabled: 0.5;
|
|
414
|
+
--_o-disabled-static: 0.5;
|
|
415
|
+
|
|
412
416
|
// Shadows
|
|
413
417
|
--bs-sm: 0 1px 2px hsla(0, 0%, 0%, 0.05), 0 1px 4px hsla(0, 0%, 0%, 0.05), 0 2px 8px hsla(0, 0%, 0%, 0.05);
|
|
414
418
|
--bs-md: 0 1px 3px hsla(0, 0%, 0%, 0.06), 0 2px 6px hsla(0, 0%, 0%, 0.06), 0 3px 8px hsla(0, 0%, 0%, 0.09);
|
|
@@ -605,6 +609,10 @@
|
|
|
605
609
|
--focus-ring-error: hsla(@red-h, 62%, 52%, 30%);
|
|
606
610
|
--focus-ring-muted: hsla(@black-h, @black-s, 100%, 10%);
|
|
607
611
|
|
|
612
|
+
// Opacity
|
|
613
|
+
--_o-disabled: 0.5;
|
|
614
|
+
--_o-disabled-static: 0.5;
|
|
615
|
+
|
|
608
616
|
// Shadows
|
|
609
617
|
--bs-sm: 0 1px 2px hsla(0, 0%, 0%, 0.1), 0 1px 4px hsla(0, 0%, 0%, 0.1), 0 2px 8px hsla(0, 0%, 0%, 0.1);
|
|
610
618
|
--bs-md: 0 1px 3px hsla(0, 0%, 0%, 0.11), 0 2px 6px hsla(0, 0%, 0%, 0.11), 0 3px 8px hsla(0, 0%, 0%, 0.14);
|
|
@@ -816,6 +824,9 @@
|
|
|
816
824
|
--focus-ring-error: fade(@red-600, 90%);
|
|
817
825
|
--focus-ring-muted: fade(@black-400, 95%);
|
|
818
826
|
|
|
827
|
+
// Opacity
|
|
828
|
+
--_o-disabled: 0.8;
|
|
829
|
+
|
|
819
830
|
// Shadows
|
|
820
831
|
--bs-sm: none;
|
|
821
832
|
--bs-md: none;
|
|
@@ -972,6 +983,9 @@
|
|
|
972
983
|
--fc-medium: var(--black-700);
|
|
973
984
|
--fc-light: var(--black-500);
|
|
974
985
|
|
|
986
|
+
// Opacity
|
|
987
|
+
--_o-disabled: 0.8;
|
|
988
|
+
|
|
975
989
|
// Shadows
|
|
976
990
|
--bs-sm: none;
|
|
977
991
|
--bs-md: none;
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
@import "components/activity-indicator.less";
|
|
21
21
|
@import "components/avatars.less";
|
|
22
22
|
@import "components/badges.less";
|
|
23
|
-
@import "components/
|
|
23
|
+
@import "components/empty-states.less";
|
|
24
24
|
@import "components/breadcrumbs.less";
|
|
25
25
|
@import "components/button-groups.less";
|
|
26
26
|
@import "components/cards.less";
|
|
27
27
|
@import "components/code-blocks.less";
|
|
28
|
-
@import "components/
|
|
28
|
+
@import "components/expandable.less";
|
|
29
29
|
@import "components/inputs.less";
|
|
30
|
+
@import "components/labels.less";
|
|
30
31
|
@import "components/menu.less";
|
|
31
32
|
@import "components/modals.less";
|
|
32
33
|
@import "components/navigation.less";
|
|
@@ -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');
|