@stackoverflow/stacks 1.6.1 → 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 +14 -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/badges.less +1 -0
- 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/notices.less +4 -0
- package/lib/css/components/pagination.less +1 -1
- package/lib/css/components/popovers.less +6 -7
- package/lib/css/components/prose.less +9 -2
- 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
|
@@ -609,34 +609,18 @@ fieldset {
|
|
|
609
609
|
font-size: var(--fs-caption);
|
|
610
610
|
}
|
|
611
611
|
|
|
612
|
-
// $$ SIZES
|
|
612
|
+
// $$ PADDING ADJUSTMENTS AND SIZES
|
|
613
613
|
// ----------------------------------------------------------------------------
|
|
614
614
|
.s-input__sm,
|
|
615
615
|
.s-textarea__sm,
|
|
616
616
|
.s-select__sm > select {
|
|
617
617
|
font-size: var(--fs-caption);
|
|
618
618
|
}
|
|
619
|
-
.s-input__md,
|
|
620
|
-
.s-textarea__md,
|
|
621
|
-
.s-select__md > select {
|
|
622
|
-
font-size: var(--fs-body3);
|
|
623
|
-
}
|
|
624
|
-
.s-input__lg,
|
|
625
|
-
.s-textarea__lg,
|
|
626
|
-
.s-select__lg > select {
|
|
627
|
-
font-size: var(--fs-title);
|
|
628
|
-
}
|
|
629
|
-
.s-input__xl,
|
|
630
|
-
.s-textarea__xl,
|
|
631
|
-
.s-select__xl > select {
|
|
632
|
-
font-size: var(--fs-headline1);
|
|
633
|
-
}
|
|
634
619
|
|
|
635
|
-
// $$ PADDING ADJUSTMENTS WITHIN SIZES
|
|
636
|
-
// ----------------------------------------------------------------------------
|
|
637
620
|
.s-input__md,
|
|
638
621
|
.s-textarea__md,
|
|
639
622
|
.s-select__md > select {
|
|
623
|
+
font-size: var(--fs-body3);
|
|
640
624
|
padding-top: 0.5em;
|
|
641
625
|
padding-bottom: 0.5em;
|
|
642
626
|
border-radius: calc(var(--br-sm) + 1px);
|
|
@@ -650,6 +634,7 @@ fieldset {
|
|
|
650
634
|
.s-input__lg,
|
|
651
635
|
.s-textarea__lg,
|
|
652
636
|
.s-select__lg > select {
|
|
637
|
+
font-size: var(--fs-title);
|
|
653
638
|
padding: 0.45em 0.6em;
|
|
654
639
|
border-radius: calc(var(--br-sm) + 1px);
|
|
655
640
|
}
|
|
@@ -657,6 +642,7 @@ fieldset {
|
|
|
657
642
|
.s-input__xl,
|
|
658
643
|
.s-textarea__xl,
|
|
659
644
|
.s-select__xl > select {
|
|
645
|
+
font-size: var(--fs-headline1);
|
|
660
646
|
padding: 0.4em 0.5em;
|
|
661
647
|
border-radius: var(--br-md);
|
|
662
648
|
}
|
|
@@ -152,6 +152,10 @@
|
|
|
152
152
|
background: var(--_no-code-bg, transparent);
|
|
153
153
|
}
|
|
154
154
|
& &--btn {
|
|
155
|
+
// TODO: decouple .s-notice--btn from .s-btn
|
|
156
|
+
&:not(:focus) {
|
|
157
|
+
box-shadow: none; // This will prevent default .s-btn box-shadow from showing
|
|
158
|
+
}
|
|
155
159
|
&:focus,
|
|
156
160
|
&:hover {
|
|
157
161
|
background: var(--_no-btn-bg-focus);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
--_po-arrow-after-r: unset;
|
|
18
18
|
--_po-arrow-after-t: unset;
|
|
19
19
|
--_po-arrow-after-bs: unset;
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
// CONTEXTUAL STYLES
|
|
22
22
|
.dark-mode({
|
|
23
23
|
--_po-bg: var(--black-075);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
--_po-bs: var(--bs-lg);
|
|
26
26
|
--_po-arrow-fc: var(--black-075);
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
// MODIFIERS
|
|
30
30
|
&.is-visible {
|
|
31
31
|
--_po-d: block;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
--_po-wmn: unset;
|
|
35
35
|
--_po-w: auto;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
// CHILD ELEMENTS
|
|
39
39
|
// Arrow
|
|
40
40
|
&[data-popper-placement^="top"] > &--arrow,
|
|
@@ -128,14 +128,14 @@
|
|
|
128
128
|
right: calc(var(--su8) * -1); // Compensate for s-popover's padding
|
|
129
129
|
padding: var(--su8) !important;
|
|
130
130
|
}
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
background-color: var(--_po-bg);
|
|
133
133
|
border: 1px solid var(--_po-bc);
|
|
134
134
|
box-shadow: var(--_po-bs);
|
|
135
135
|
display: var(--_po-d);
|
|
136
136
|
min-width: var(--_po-wmn);
|
|
137
137
|
width: var(--_po-w);
|
|
138
|
-
|
|
138
|
+
|
|
139
139
|
border-radius: var(--br-md);
|
|
140
140
|
color: var(--fc-dark);
|
|
141
141
|
font-size: var(--fs-body1);
|
|
@@ -144,5 +144,4 @@
|
|
|
144
144
|
position: absolute;
|
|
145
145
|
white-space: normal; // Guard against popovers being in a container with white-space: nowrap. Without this, the content pops *out* of the popover.
|
|
146
146
|
z-index: var(--zi-popovers);
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
}
|
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
--_pr-spacing: 1.1em;
|
|
25
25
|
--_pr-spacing-condensed: calc(var(--_pr-spacing) / 2); // Reduce the base spacing by half in the context of lists, etc.
|
|
26
26
|
--_pr-spoiler-transition: opacity 0.1s ease-in-out;
|
|
27
|
+
// TEMP HOT FIX FOR .s-editor [1]
|
|
28
|
+
--s-prose-spacing: var(--_pr-spacing); // TODO remove once addressed in StackExchange/Stacks-Editor
|
|
29
|
+
--s-prose-spacing-condensed: var(--_pr-spacing-condensed); // TODO remove once addressed in StackExchange/Stacks-Editor
|
|
27
30
|
|
|
28
31
|
// CONDITIONAL STYLES
|
|
29
32
|
.dark-mode({
|
|
@@ -164,6 +167,7 @@
|
|
|
164
167
|
quotes: none;
|
|
165
168
|
}
|
|
166
169
|
dd,
|
|
170
|
+
div,
|
|
167
171
|
dl,
|
|
168
172
|
.s-table-container,
|
|
169
173
|
.s-link-preview {
|
|
@@ -371,7 +375,7 @@
|
|
|
371
375
|
color: var(--black-800);
|
|
372
376
|
min-height: var(--su-static48); // TODO: Let's find a solution that doesn't have a magic number
|
|
373
377
|
}
|
|
374
|
-
.youtube-embed { // [
|
|
378
|
+
.youtube-embed { // [2]
|
|
375
379
|
> div {
|
|
376
380
|
iframe {
|
|
377
381
|
height: 100%;
|
|
@@ -395,7 +399,10 @@
|
|
|
395
399
|
overflow-wrap: break-word;
|
|
396
400
|
}
|
|
397
401
|
|
|
398
|
-
// [1]
|
|
402
|
+
// [1] StackExchange/Stacks-Editor references `--s-prose-spacing` and `--s-prose-spacing-condensed`.
|
|
403
|
+
// Going forward, it shouldn't but for now, I've introduced a hotfix to define those custom properties.
|
|
404
|
+
|
|
405
|
+
// [2] The outer div enforces a max-width of 640px. The inner div has a height of 35 pixels, and a
|
|
399
406
|
// padding-bottom of 56.25%. Padding percentages, even for top/bottom, are always relative
|
|
400
407
|
// to the *width*, so the padding always has an aspect ratio of 1/0.5625, or 16/9. Thus in total,
|
|
401
408
|
// the iframe has a height of 35 + width * 9/16. 35 pixels is the height of youtube's player controls,
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
// export all controllers *with helpers* so they can be bulk re-exported by the package entry point
|
|
2
|
-
export { ExpandableController } from
|
|
3
|
-
export { hideModal, ModalController, showModal } from
|
|
4
|
-
export { TabListController } from
|
|
5
|
-
export {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
export { ExpandableController } from "./s-expandable-control";
|
|
3
|
+
export { hideModal, ModalController, showModal } from "./s-modal";
|
|
4
|
+
export { TabListController } from "./s-navigation-tablist";
|
|
5
|
+
export {
|
|
6
|
+
attachPopover,
|
|
7
|
+
detachPopover,
|
|
8
|
+
hidePopover,
|
|
9
|
+
BasePopoverController,
|
|
10
|
+
PopoverController,
|
|
11
|
+
showPopover,
|
|
12
|
+
} from "./s-popover";
|
|
13
|
+
export { TableController } from "./s-table";
|
|
14
|
+
export { setTooltipHtml, setTooltipText, TooltipController } from "./s-tooltip";
|
|
15
|
+
export { UploaderController } from "./s-uploader";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as Stacks from
|
|
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
4
|
// they're *unchecked*. Therefore, if we have an active `s-expandable-control` in
|
|
@@ -12,10 +12,15 @@ const RADIO_OFF_EVENT = "s-expandable-control:radio-off";
|
|
|
12
12
|
|
|
13
13
|
function globalChangeListener(e: UIEvent) {
|
|
14
14
|
const target = e.target;
|
|
15
|
-
if (
|
|
15
|
+
if (
|
|
16
|
+
!(target instanceof HTMLInputElement) ||
|
|
17
|
+
target.nodeName !== "INPUT" ||
|
|
18
|
+
target.type !== "radio"
|
|
19
|
+
) {
|
|
16
20
|
return;
|
|
17
21
|
}
|
|
18
|
-
document
|
|
22
|
+
document
|
|
23
|
+
.querySelectorAll('input[type="radio"][name="' + target.name + '"]')
|
|
19
24
|
.forEach(function (other) {
|
|
20
25
|
if (other === e.target) {
|
|
21
26
|
return;
|
|
@@ -37,12 +42,18 @@ function globalChangeListenerRequired(required: boolean) {
|
|
|
37
42
|
if (required) {
|
|
38
43
|
refCount++;
|
|
39
44
|
if (refCount === 1) {
|
|
40
|
-
document.body.addEventListener(
|
|
45
|
+
document.body.addEventListener(
|
|
46
|
+
"change",
|
|
47
|
+
globalChangeListener as EventListener
|
|
48
|
+
);
|
|
41
49
|
}
|
|
42
50
|
} else {
|
|
43
51
|
refCount--;
|
|
44
52
|
if (refCount === 0) {
|
|
45
|
-
document.body.removeEventListener(
|
|
53
|
+
document.body.removeEventListener(
|
|
54
|
+
"change",
|
|
55
|
+
globalChangeListener as EventListener
|
|
56
|
+
);
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
}
|
|
@@ -55,7 +66,12 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
55
66
|
private lastKeydownClickTimestamp = 0;
|
|
56
67
|
|
|
57
68
|
initialize() {
|
|
58
|
-
if (
|
|
69
|
+
if (
|
|
70
|
+
this.element.nodeName === "INPUT" &&
|
|
71
|
+
["radio", "checkbox"].indexOf(
|
|
72
|
+
(<HTMLInputElement>this.element).type
|
|
73
|
+
) >= 0
|
|
74
|
+
) {
|
|
59
75
|
this.isCollapsed = this._isCollapsedForCheckable.bind(this);
|
|
60
76
|
this.events = ["change", RADIO_OFF_EVENT];
|
|
61
77
|
this.isCheckable = true;
|
|
@@ -65,40 +81,41 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
65
81
|
this.events = ["click", "keydown"];
|
|
66
82
|
}
|
|
67
83
|
this.listener = this.listener.bind(this);
|
|
68
|
-
}
|
|
69
|
-
|
|
84
|
+
}
|
|
70
85
|
|
|
71
86
|
// for non-checkable elements, the initial source of truth is the collapsed/expanded
|
|
72
87
|
// state of the controlled element (unless the element doesn't exist)
|
|
73
88
|
_isCollapsedForClickable() {
|
|
74
89
|
const cc = this.controlledExpandables;
|
|
75
90
|
// the element is considered collapsed if *any* target element is collapsed
|
|
76
|
-
return cc.length > 0
|
|
77
|
-
|
|
91
|
+
return cc.length > 0
|
|
92
|
+
? !cc.every((element) => element.classList.contains("is-expanded"))
|
|
93
|
+
: this.element.getAttribute("aria-expanded") === "false";
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
// for checkable elements, the initial source of truth is the checked state
|
|
80
97
|
_isCollapsedForCheckable() {
|
|
81
98
|
return !(<HTMLInputElement>this.element).checked;
|
|
82
|
-
}
|
|
83
|
-
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
get controlledExpandables() {
|
|
86
102
|
const attr = this.element.getAttribute("aria-controls");
|
|
87
103
|
if (!attr) {
|
|
88
104
|
throw `[aria-controls="targetId1 ... targetIdN"] attribute required`;
|
|
89
105
|
}
|
|
90
|
-
const result = attr
|
|
91
|
-
.
|
|
106
|
+
const result = attr
|
|
107
|
+
.split(/\s+/g)
|
|
108
|
+
.map((s) => document.getElementById(s))
|
|
92
109
|
.filter((e): e is HTMLElement => !!e);
|
|
93
110
|
if (!result.length) {
|
|
94
|
-
throw "couldn't find controls"
|
|
111
|
+
throw "couldn't find controls";
|
|
95
112
|
}
|
|
96
113
|
return result;
|
|
97
|
-
}
|
|
114
|
+
}
|
|
98
115
|
|
|
99
116
|
_dispatchShowHideEvent(isShow: boolean) {
|
|
100
117
|
this.triggerEvent(isShow ? "show" : "hide");
|
|
101
|
-
}
|
|
118
|
+
}
|
|
102
119
|
|
|
103
120
|
_toggleClass(doAdd: boolean) {
|
|
104
121
|
if (!this.data.has("toggle-class")) {
|
|
@@ -107,22 +124,30 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
107
124
|
const cl = this.element.classList;
|
|
108
125
|
const toggleClass = this.data.get("toggle-class");
|
|
109
126
|
if (!toggleClass) {
|
|
110
|
-
throw "couldn't find toggle class"
|
|
127
|
+
throw "couldn't find toggle class";
|
|
111
128
|
}
|
|
112
129
|
toggleClass.split(/\s+/).forEach(function (cls) {
|
|
113
130
|
cl.toggle(cls, !!doAdd);
|
|
114
131
|
});
|
|
115
|
-
}
|
|
132
|
+
}
|
|
116
133
|
|
|
117
134
|
listener(e: Event) {
|
|
118
135
|
let newCollapsed;
|
|
119
136
|
if (this.isCheckable) {
|
|
120
137
|
newCollapsed = !(<HTMLInputElement>this.element).checked;
|
|
121
138
|
} else {
|
|
122
|
-
if (
|
|
139
|
+
if (
|
|
140
|
+
e.type == "keydown" &&
|
|
141
|
+
e instanceof KeyboardEvent &&
|
|
142
|
+
e.keyCode != 13 &&
|
|
143
|
+
e.keyCode != 32
|
|
144
|
+
) {
|
|
123
145
|
return;
|
|
124
146
|
}
|
|
125
|
-
if (
|
|
147
|
+
if (
|
|
148
|
+
e.target !== e.currentTarget &&
|
|
149
|
+
["A", "BUTTON"].indexOf((<HTMLElement>e.target).nodeName) >= 0
|
|
150
|
+
) {
|
|
126
151
|
return;
|
|
127
152
|
}
|
|
128
153
|
|
|
@@ -133,24 +158,31 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
133
158
|
// doesn't guarantee it.
|
|
134
159
|
if (e.type == "keydown") {
|
|
135
160
|
this.lastKeydownClickTimestamp = Date.now();
|
|
136
|
-
} else if (
|
|
161
|
+
} else if (
|
|
162
|
+
e.type == "click" &&
|
|
163
|
+
Date.now() - this.lastKeydownClickTimestamp < 300
|
|
164
|
+
) {
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
139
|
-
newCollapsed =
|
|
167
|
+
newCollapsed =
|
|
168
|
+
this.element.getAttribute("aria-expanded") === "true";
|
|
140
169
|
if (e.type === "click") {
|
|
141
170
|
(<HTMLInputElement>this.element).blur();
|
|
142
171
|
}
|
|
143
172
|
}
|
|
144
|
-
this.element.setAttribute(
|
|
173
|
+
this.element.setAttribute(
|
|
174
|
+
"aria-expanded",
|
|
175
|
+
newCollapsed ? "false" : "true"
|
|
176
|
+
);
|
|
145
177
|
for (const controlledElement of this.controlledExpandables) {
|
|
146
178
|
controlledElement.classList.toggle("is-expanded", !newCollapsed);
|
|
147
179
|
}
|
|
148
180
|
this._dispatchShowHideEvent(!newCollapsed);
|
|
149
181
|
this._toggleClass(!newCollapsed);
|
|
150
|
-
}
|
|
182
|
+
}
|
|
151
183
|
|
|
152
184
|
connect() {
|
|
153
|
-
this.events.forEach(e => {
|
|
185
|
+
this.events.forEach((e) => {
|
|
154
186
|
this.element.addEventListener(e, this.listener.bind(this));
|
|
155
187
|
}, this);
|
|
156
188
|
|
|
@@ -163,31 +195,44 @@ export class ExpandableController extends Stacks.StacksController {
|
|
|
163
195
|
// Note: aria-expanded is currently an invalid attribute on radio elements
|
|
164
196
|
// Support for aria-expanded is being debated by the W3C https://github.com/w3c/aria/issues/1404 as recently as June 2022
|
|
165
197
|
if (!this.isRadio) {
|
|
166
|
-
this.element.setAttribute(
|
|
198
|
+
this.element.setAttribute(
|
|
199
|
+
"aria-expanded",
|
|
200
|
+
this.isCollapsed() ? "false" : "true"
|
|
201
|
+
);
|
|
167
202
|
}
|
|
168
203
|
if (this.isCheckable) {
|
|
169
204
|
const cc = this.controlledExpandables;
|
|
170
205
|
if (cc.length) {
|
|
171
206
|
const expected = !this.isCollapsed();
|
|
172
207
|
// if any element does not match the expected state, set them all to the expected state
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
if (
|
|
209
|
+
cc.some(
|
|
210
|
+
(element) =>
|
|
211
|
+
element.classList.contains("is-expanded") !==
|
|
212
|
+
expected
|
|
213
|
+
)
|
|
214
|
+
) {
|
|
215
|
+
for (const controlledElement of this
|
|
216
|
+
.controlledExpandables) {
|
|
217
|
+
controlledElement.classList.toggle(
|
|
218
|
+
"is-expanded",
|
|
219
|
+
expected
|
|
220
|
+
);
|
|
176
221
|
}
|
|
177
222
|
this._dispatchShowHideEvent(expected);
|
|
178
223
|
this._toggleClass(expected);
|
|
179
224
|
}
|
|
180
225
|
}
|
|
181
226
|
}
|
|
182
|
-
}
|
|
227
|
+
}
|
|
183
228
|
|
|
184
229
|
disconnect() {
|
|
185
|
-
this.events.forEach(e => {
|
|
230
|
+
this.events.forEach((e) => {
|
|
186
231
|
this.element.removeEventListener(e, this.listener.bind(this));
|
|
187
232
|
}, this);
|
|
188
233
|
|
|
189
234
|
if (this.isRadio) {
|
|
190
235
|
globalChangeListenerRequired(false);
|
|
191
236
|
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
237
|
+
}
|
|
238
|
+
}
|