@stackoverflow/stacks 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,19 +23,19 @@
23
23
  // MODIFIERS
24
24
  // Sizes
25
25
  &&__sm {
26
- --_la-fs: var(--fs-caption);
26
+ .size-styles(sm; la; @styles: fs);
27
27
  }
28
28
 
29
29
  &&__md {
30
- --_la-fs: var(--fs-body3);
30
+ .size-styles(md; la; @styles: fs);
31
31
  }
32
32
 
33
33
  &&__lg {
34
- --_la-fs: var(--fs-title);
34
+ .size-styles(lg; la; @styles: fs);
35
35
  }
36
36
 
37
37
  &&__xl {
38
- --_la-fs: var(--fs-headline1);
38
+ .size-styles(xl; la; @styles: fs);
39
39
  }
40
40
 
41
41
  // CHILD ELEMENTS
@@ -83,6 +83,13 @@ a,
83
83
  }
84
84
  }
85
85
 
86
+ // MODIFIERS
87
+ fieldset[disabled] & {
88
+ box-shadow: none !important;
89
+ opacity: var(--_o-disabled-static);
90
+ pointer-events: none;
91
+ }
92
+
86
93
  // INTERACTION
87
94
  &:active,
88
95
  &:hover {
@@ -206,8 +206,12 @@
206
206
  margin: 0 !important;
207
207
  }
208
208
 
209
- padding: var(--su8) !important; // To override .s-btn class attributes
210
- position: absolute !important; // To override .s-btn class attributes
209
+ &,
210
+ &.s-btn { // To override .s-btn class attributes
211
+ padding: var(--su8);
212
+ position: absolute;
213
+ }
214
+
211
215
  right: var(--su8);
212
216
  top: var(--su8);
213
217
  }
@@ -0,0 +1,148 @@
1
+ .s-select {
2
+ --_se-arrow-bc: currentColor transparent;
3
+ --_se-arrow-size: var(--su-static4); // Constant
4
+ --_se-select-bc: var(--bc-darker);
5
+ --_se-select-bc-focus: var(--theme-secondary-300);
6
+ --_se-select-bs-focus: 0 0 0 var(--su-static4) var(--focus-ring);
7
+ --_se-select-bg: var(--white);
8
+ --_se-select-br: var(--br-sm);
9
+ --_se-select-fc: var(--black);
10
+ --_se-select-px: 0.7em;
11
+ --_se-select-py: 0.6em;
12
+ --_se-select-fs: var(--fs-body1);
13
+
14
+ // CONTEXTUAL STYLES
15
+ @supports (-webkit-overflow-scrolling: touch) {
16
+ --_se-select-fs: 16px; // Increase font size for mobile safari. This keeps our inputs from zooming the page while focused
17
+ --_se-select-px: 0.55em; // Compensate for the larger font size so we generally keep the input the same size
18
+ --_se-select-py: 0.4em; // Compensate for the larger font size so we generally keep the input the same size
19
+ }
20
+
21
+ // MODIFIERS
22
+ .input-states({
23
+ position: relative;
24
+ });
25
+
26
+ .validation-states(se-select);
27
+
28
+ .is-disabled & {
29
+ --_se-arrow-bc: var(--bc-darker) transparent;
30
+ }
31
+
32
+ &&__sm {
33
+ .size-styles(sm; se-select; @styles: fs);
34
+ // --_se-select-fs: var(--fs-caption);
35
+ }
36
+
37
+ &&__md {
38
+ .size-styles(md; se-select; @styles: br, fs);
39
+ --_se-select-py: 0.5em;
40
+ }
41
+
42
+ &&__lg {
43
+ .size-styles(lg; se-select; @styles: br, fs);
44
+ --_se-select-px: 0.6em;
45
+ --_se-select-py: 0.45em;
46
+ }
47
+
48
+ &&__xl {
49
+ .size-styles(xl; se-select; @styles: br, fs);
50
+ --_se-select-px: 0.5em;
51
+ --_se-select-py: 0.4em;
52
+ }
53
+
54
+ // CHILD ELEMENTS
55
+ select&,
56
+ & > select {
57
+ .webkit-autofill();
58
+ }
59
+
60
+ &:before,
61
+ &:after { // menu arrows
62
+ border-color: var(--_se-arrow-bc);
63
+ border-style: solid;
64
+ border-width: var(--_se-arrow-size);
65
+ content: "";
66
+ pointer-events: none;
67
+ position: absolute;
68
+ right: calc(var(--su-static12) + var(--su-static1));
69
+ z-index: var(--zi-selected);
70
+ }
71
+
72
+ &:after {
73
+ border-bottom-width: 0;
74
+ top: calc(50% + var(--su-static1));
75
+ }
76
+
77
+ &:before {
78
+ border-top-width: 0;
79
+ top: calc(50% - calc(var(--_se-arrow-size) + var(--su-static1)));
80
+ }
81
+
82
+ > select { // Menu
83
+ // CONTEXTUAL STYLES
84
+ fieldset[disabled] &,
85
+ &[disabled] {
86
+ cursor: not-allowed;
87
+ opacity: var(--_o-disabled-static);
88
+ }
89
+
90
+ &[readonly],
91
+ .is-readonly & { // [1]
92
+ --_se-select-bc: var(--bc-light);
93
+ --_se-select-bg: var(--black-050);
94
+ --_se-select-fc: var(--black-200);
95
+
96
+ .highcontrast-mode({
97
+ --_se-select-fc: var(--fc-light);
98
+ });
99
+
100
+ cursor: not-allowed;
101
+ }
102
+
103
+ // CHILD ELEMENTS
104
+ &::-moz-focus-inner {
105
+ outline: none !important;
106
+ }
107
+
108
+ // INTERACTION
109
+ &:focus {
110
+ .highcontrast-mode({
111
+ --_se-select-bc-focus: var(--black);
112
+ });
113
+
114
+ border-color: var(--_se-select-bc-focus);
115
+ box-shadow: var(--_se-select-bs-focus);
116
+
117
+ color: var(--black);
118
+ outline: 0;
119
+ }
120
+
121
+ background-color: var(--_se-select-bg);
122
+ border: var(--su-static1) solid var(--_se-select-bc);
123
+ border-radius: var(--_se-select-br);
124
+ color: var(--_se-select-fc);
125
+ font-size: var(--_se-select-fs);
126
+ padding: var(--_se-select-py) var(--_se-select-px);
127
+
128
+ appearance: none;
129
+ font-family: inherit;
130
+ height: 100%; // Fill the height of its parent
131
+ line-height: var(--lh-sm);
132
+ outline: 0;
133
+ padding-right: var(--su32);
134
+ position: relative; // This prevents Firefox from requiring a second click to select options
135
+ width: 100%;
136
+ }
137
+
138
+ .s-input-icon {
139
+ right: var(--su32);
140
+ }
141
+
142
+ color: var(--fc-dark);
143
+ position: relative;
144
+ }
145
+
146
+ // [1] The readonly attribute is not supported on select elements
147
+ // See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/readonly
148
+ // and https://github.com/StackExchange/Stacks/pull/1040#discussion_r931144086
@@ -42,12 +42,12 @@
42
42
  // MODIFIERS
43
43
  // Sizes
44
44
  &&__xs {
45
- --_ta-fs: var(--fs-fine);
45
+ .size-styles(xs; ta; @styles: fs);
46
46
  --_ta-lh: 1.4;
47
47
  --_ta-px: var(--su2);
48
48
  }
49
49
  &&__sm {
50
- --_ta-fs: var(--fs-caption);
50
+ .size-styles(sm; ta; @styles: fs);
51
51
  --_ta-lh: 1.5;
52
52
  }
53
53
  &&__md {
@@ -56,7 +56,7 @@
56
56
  --_ta-lh: 1.733333333;
57
57
  }
58
58
  &&__lg {
59
- --_ta-br: calc(var(--br-sm) + 1px);
59
+ --_ta-br: calc(var(--br-sm) + var(--su-static1));
60
60
  --_ta-fs: var(--fs-subheading);
61
61
  --_ta-lh: 1.684210526;
62
62
  --_ta-px: var(--su6);
@@ -5,6 +5,14 @@
5
5
  --_ts-multiple-bg: unset;
6
6
  --_ts-multiple-fc: var(--black-500);
7
7
 
8
+ fieldset[disabled] & {
9
+ &,
10
+ & label {
11
+ cursor: not-allowed;
12
+ opacity: var(--_o-disabled-static);
13
+ }
14
+ }
15
+
8
16
  &&__multiple { // TODO split single and multiple toggle into two seperate components
9
17
  input[type="radio"] {
10
18
  &:checked {
@@ -88,17 +88,6 @@
88
88
  }
89
89
  }
90
90
 
91
- // ===========================================================================
92
- // -- APPEARANCE
93
- // Use this to overwrite the default appearance properties
94
- // ---------------------------------------------------------------------------
95
- .appearance(@all) {
96
- -webkit-appearance: @all;
97
- -moz-appearance: @all;
98
- appearance: @all;
99
- }
100
-
101
-
102
91
  // ===========================================================================
103
92
  // -- FONT FACE
104
93
  // Used to load hosted, custom webfonts. You must provide the font's
@@ -194,6 +183,79 @@
194
183
  @amountDecimal);
195
184
  }
196
185
 
186
+ // =============================================================================
187
+ // -- SIZE STYLES
188
+ // The following mixins let us generate pseudo-private custom properties
189
+ // for common sizes. They expect a size (@size), a prefix for the custom
190
+ // property (@prefix), and an array of comma-separates abbreviated styles
191
+ // (@styles).
192
+ // -----------------------------------------------------------------------------
193
+
194
+ .size-styles(@size, @prefix, @styles, @index: length(@styles)) {
195
+ & when (@index > 0) {
196
+ @style: extract(@styles, @index);
197
+
198
+ // xs
199
+ & when (@size = xs) and (@style = fs) {
200
+ --_@{prefix}-fs: var(--fs-fine);
201
+ }
202
+
203
+ // sm
204
+ & when (@size = sm) and (@style = fs) {
205
+ --_@{prefix}-fs: var(--fs-caption);
206
+ }
207
+
208
+ // md
209
+ & when (@size = md) and (@style = br) {
210
+ --_@{prefix}-br: calc(var(--br-sm) + var(--su-static1));
211
+ }
212
+ & when (@size = md) and (@style = fs) {
213
+ --_@{prefix}-fs: var(--fs-body3);
214
+ }
215
+
216
+ // lg
217
+ & when (@size = lg) and (@style = br) {
218
+ --_@{prefix}-br: calc(var(--br-sm) + var(--su-static1));
219
+ }
220
+ & when (@size = lg) and (@style = fs) {
221
+ --_@{prefix}-fs: var(--fs-title);
222
+ }
223
+
224
+ // xl
225
+ & when (@size = xl) and (@style = br) {
226
+ --_@{prefix}-br: var(--br-md);
227
+ }
228
+ & when (@size = xl) and (@style = fs) {
229
+ --_@{prefix}-fs: var(--fs-headline1);
230
+ }
231
+
232
+ .size-styles(@size, @prefix, @styles, @index: @index - 1);
233
+ }
234
+ }
235
+
236
+ // =============================================================================
237
+ // -- WEBKIT AUTOFILL
238
+ // -----------------------------------------------------------------------------
239
+ .webkit-autofill() {
240
+ &:-webkit-autofill {
241
+ &:focus {
242
+ border-color: var(--blue-300);
243
+ // Since the box shadow is overwritten to show a background, we have to re-add the focus outline
244
+ -webkit-box-shadow: 0 0 0 1000px var(--blue-050) inset, 0 0 0 var(--su-static4) var(--focus-ring);
245
+ }
246
+
247
+ -webkit-box-shadow: 0 0 0 1000px var(--theme-secondary-050) inset; // This acts as a background color by stretching an inset box shadow across the input
248
+ -webkit-text-fill-color: var(--black);
249
+ border-color: var(--blue-300);
250
+ transition: background-color 0s 50000s; // A hack to infinitely delay background styles that come from the browser.
251
+ }
252
+
253
+ &::-webkit-contacts-auto-fill-button {
254
+ background-color: var(--black); // In Safari, make the autocomplete button darkmode-aware
255
+ }
256
+
257
+ }
258
+
197
259
  // ===========================================================================
198
260
  // Internals only beyond this point -- helpers for .font-face()
199
261
  // ---------------------------------------------------------------------------
@@ -0,0 +1,44 @@
1
+ .input-states(@rules) {
2
+ .is-disabled &,
3
+ .is-readonly &,
4
+ .has-success &,
5
+ .has-error &,
6
+ .has-warning & {
7
+ @rules();
8
+ }
9
+ };
10
+
11
+ .validation-states(@prefix, @error: {}, @success: {}, @warning: {}) {
12
+ .has-error &,
13
+ .has-success &,
14
+ .has-warning & {
15
+ --_@{prefix}-bc-focus: ~"var(--_@{prefix}-bc)";
16
+ }
17
+
18
+ .has-error & {
19
+ --_@{prefix}-bc: var(--red-400);
20
+ --_@{prefix}-bs-focus: 0 0 0 var(--su-static4) var(--focus-ring-error);
21
+ @error();
22
+ }
23
+
24
+ .has-success & {
25
+ --_@{prefix}-bc: var(--green-400);
26
+ --_@{prefix}-bs-focus: 0 0 0 var(--su-static4) var(--focus-ring-success);
27
+ @success();
28
+ }
29
+
30
+ .has-warning & {
31
+ --_@{prefix}-bc: var(--yellow-600);
32
+ --_@{prefix}-bs-focus: 0 0 0 var(--su-static4) var(--focus-ring-warning);
33
+ @warning();
34
+ }
35
+ }
36
+
37
+ .is-disabled,
38
+ .is-readonly,
39
+ .has-success,
40
+ .has-error,
41
+ .has-warning {
42
+ position: relative;
43
+ }
44
+
@@ -1,22 +1,11 @@
1
- //
2
- // STACK OVERFLOW
3
- // STATIC STACK ELEMENTS
4
- //
5
- // This CSS comes from Stacks, our CSS & Pattern library for rapidly building
6
- // Stack Overflow. For documentation of all these classes and how to contribute,
7
- // visit https://stackoverflow.design/
8
- //
9
- // This is where all the magic happens.
10
- //
11
- // ============================================================================
12
- // $ STATIC ELEMENTS
13
- // The following items are elements which we DO NOT allow communities
14
- // to modify via variables.
15
- // ----------------------------------------------------------------------------
1
+ // stacks-static.less contains styles which we DO NOT allow communities to modify via variables
2
+ // BASE
16
3
  @import "base/reset.less";
4
+ @import "base/fieldset.less";
17
5
  @import "base/icons.less";
6
+ @import "input-utils.less";
18
7
 
19
- // -- COMPONENTS
8
+ // COMPONENTS
20
9
  @import "components/activity-indicator.less";
21
10
  @import "components/anchors.less";
22
11
  @import "components/award-bling.less";
@@ -28,7 +17,9 @@
28
17
  @import "components/breadcrumbs.less";
29
18
  @import "components/button-groups.less";
30
19
  @import "components/cards.less";
20
+ @import "components/checkboxes-radios.less";
31
21
  @import "components/code-blocks.less";
22
+ @import "components/description.less";
32
23
  @import "components/expandable.less";
33
24
  @import "components/inputs.less";
34
25
  @import "components/labels.less";
@@ -44,6 +35,7 @@
44
35
  @import "components/post-summary.less";
45
36
  @import "components/progress-bars.less";
46
37
  @import "components/prose.less";
38
+ @import "components/select.less";
47
39
  @import "components/sidebar-widgets.less";
48
40
  @import "components/spinner.less";
49
41
  @import "components/table.less";
@@ -53,10 +45,10 @@
53
45
  @import "components/uploader.less";
54
46
  @import "components/user-cards.less";
55
47
 
56
- // -- LESS CONSTANTS AND MIXINS
48
+ // LESS CONSTANTS AND MIXINS
57
49
  @import "exports/exports.less";
58
50
 
59
- // -- ATOMIC CLASSES
51
+ // ATOMIC CLASSES
60
52
  @import "atomic/borders.less";
61
53
  @import "atomic/colors.less";
62
54
  @import "atomic/flex.less";
@@ -78,20 +70,18 @@
78
70
  #stacks-internals-collect-small();
79
71
  });
80
72
 
81
- // We need printing styles to be last so they can override all other styles.
82
- .print\:d-block {
83
- @media print {
73
+ @media print { // We need printing styles to be last so they can override all other styles.
74
+ .print\:d-block & {
84
75
  display: block !important;
85
76
  }
86
- }
87
-
88
- .print\:d-none {
89
- @media print {
77
+ .print\:d-none & {
90
78
  display: none !important;
91
79
  }
92
80
  }
93
81
  /* stylelint-enable */
94
82
 
95
- // -- CONFIG
83
+ // CONFIG
96
84
  @import "base/configuration-static.less";
97
85
  @import "base/internals.less";
86
+
87
+ // [1] The following items are elements which we DO NOT allow communities to modify via variables.
@@ -12,7 +12,7 @@ const avatarStyles = {
12
12
  };
13
13
 
14
14
  const makeTest = ({ testid, theme, classes, child = "" }) => {
15
- it(`a11y: ${testid} styles in should be accessible`, async () => {
15
+ it(`a11y: ${testid} should be accessible`, async () => {
16
16
  await fixture(html`<a
17
17
  href="#"
18
18
  class="s-avatar${classes}"
@@ -40,27 +40,30 @@ const makeTest = ({ testid, theme, classes, child = "" }) => {
40
40
  });
41
41
  };
42
42
 
43
+ // TODO move to test utils
44
+ const buildTestid = (arr) => arr.filter(Boolean).join("-");
45
+
43
46
  describe("s-avatar", () => {
44
47
  // Test default, high contrast themes
45
48
  baseThemes.forEach((baseTheme) => {
46
49
  // Test light, dark theme
47
50
  colorThemes.forEach((colorTheme) => {
48
- const testidBase = `s-avatar-${
49
- baseTheme ? `${baseTheme}-` : ""
50
- }${colorTheme}`;
51
51
  const theme = [baseTheme, colorTheme].filter(Boolean);
52
+ const testidBase = buildTestid(["s-avatar", ...theme]);
52
53
 
53
54
  // Test each size
54
55
  ["", ...avatarStyles.sizes].forEach((size) => {
55
56
  const sizeClasses = size ? ` s-avatar__${size}` : "";
56
57
  const classesSize = ` ${sizeClasses}`;
57
- const testidSize = `${testidBase}-${size}`;
58
+ const testidSize = buildTestid([testidBase, size]);
58
59
 
59
60
  // Test each size with each child
60
61
  ["", ...avatarStyles.children].forEach((child) => {
61
- const testidChildren = `${testidSize}${
62
- child ? `-with-${child}` : ""
63
- }`;
62
+ const testidChildren = buildTestid([
63
+ testidSize,
64
+ `with-${child}`,
65
+ ]);
66
+
64
67
  makeTest({
65
68
  child,
66
69
  classes: classesSize,
@@ -0,0 +1,123 @@
1
+ import { html, fixture, expect } from "@open-wc/testing";
2
+ import { screen } from "@testing-library/dom";
3
+ import "../ts/index";
4
+
5
+ // TODO abstract to a helper file…
6
+ // TODO reinstate "theme-dark" test once we add ability to skip tests or resolve dark mode contrast issues
7
+ // const colorThemes = ["theme-dark", "theme-light"];
8
+ const colorThemes = ["theme-light"];
9
+ const baseThemes = ["", "theme-highcontrast"];
10
+
11
+ const btnStyles = {
12
+ variants: ["danger", "muted", "primary"],
13
+ modifiers: ["filled", "outlined", "filled-outlined"],
14
+ secondaryModifiers: ["dropdown", "icon"],
15
+ globalModifiers: ["is-loading"],
16
+ sizes: ["xs", "sm", "md"],
17
+ resets: ["link", "unset"],
18
+ social: ["facebook", "github", "google"],
19
+ // TODO reinstate children test once we add ability to skip tests or resolve badge contrast issues
20
+ // children: ["badge"],
21
+ };
22
+
23
+ const makeTest = ({ testid, theme, classes, child = "" }) => {
24
+ it(`a11y: ${testid} should be accessible`, async () => {
25
+ await fixture(html`<button
26
+ class="s-btn${classes}"
27
+ role="button"
28
+ data-testid="${testid}"
29
+ >
30
+ Ask question
31
+ <span class="${child === "badge" ? "s-btn--badge" : "d-none"}">
32
+ <span class="s-btn--number">198</span>
33
+ </span>
34
+ </button>`);
35
+
36
+ document.body.className = "";
37
+ document.body.classList.add(...theme);
38
+ const button = screen.getByTestId(testid);
39
+ // TODO add conditional option for high contrast mode to test against AAA
40
+ await expect(button).to.be.accessible();
41
+ });
42
+ };
43
+
44
+ // TODO move to test utils
45
+ const buildTestid = (arr) => arr.filter(Boolean).join("-");
46
+
47
+ describe("s-btn", () => {
48
+ // Test default, high contrast themes
49
+ baseThemes.forEach((baseTheme) => {
50
+ // Test light, dark theme
51
+ colorThemes.forEach((colorTheme) => {
52
+ const theme = [baseTheme, colorTheme].filter(Boolean);
53
+ const testidBase = buildTestid(["s-btn", ...theme]);
54
+
55
+ // Test each combination of base modifiers
56
+ ["", ...btnStyles.modifiers].forEach((modifier) => {
57
+ const modifierClasses = modifier
58
+ ? ` s-btn__${modifier.replace("-", " s-btn__")}`
59
+ : "";
60
+
61
+ // Test each variant
62
+ ["", ...btnStyles.variants].forEach((variant) => {
63
+ const variantClasses = variant ? ` s-btn__${variant}` : "";
64
+ const classesVariant = ` ${variantClasses}${modifierClasses}`;
65
+ const testidVariant = buildTestid([
66
+ testidBase,
67
+ variant,
68
+ modifier,
69
+ ]);
70
+
71
+ // Test each variant with each child
72
+ // TODO reinstate children test once we add ability to skip tests or resolve badge contrast issues
73
+ // [...btnStyles.children].forEach((child) => {
74
+ // const testidChildren = `${testidVariant}${
75
+ // child ? `-with-${child}` : ""
76
+ // }`;
77
+ // makeTest({
78
+ // child,
79
+ // classes: classesVariant,
80
+ // testid: testidChildren,
81
+ // theme,
82
+ // });
83
+ // });
84
+
85
+ [
86
+ "", // Test no additional classes
87
+ ...btnStyles.sizes, // Test each size
88
+ ...btnStyles.resets, // Test each reset
89
+ ...btnStyles.social, // Test each social style
90
+ ].forEach((style) => {
91
+ const testidStyle = buildTestid([testidVariant, style]);
92
+ const classesStyle = `${classesVariant}${
93
+ style ? ` s-btn__${style}` : ""
94
+ }`;
95
+
96
+ makeTest({
97
+ classes: classesStyle,
98
+ testid: testidStyle,
99
+ theme,
100
+ });
101
+ });
102
+
103
+ // Test each globalModifier
104
+ [...btnStyles.globalModifiers].forEach((globalModifier) => {
105
+ const testidGlobal = buildTestid([
106
+ testidVariant,
107
+ globalModifier,
108
+ ]);
109
+ const classesGlobal = `${classesVariant}${
110
+ globalModifier ? ` ${globalModifier}` : ""
111
+ }`;
112
+
113
+ makeTest({
114
+ classes: classesGlobal,
115
+ testid: testidGlobal,
116
+ theme,
117
+ });
118
+ });
119
+ });
120
+ });
121
+ });
122
+ });
123
+ });
@@ -0,0 +1,16 @@
1
+ import { html, fixture } from "@open-wc/testing";
2
+ import { visualDiff } from "@web/test-runner-visual-regression";
3
+ import "../ts/index";
4
+
5
+ describe("s-btn", () => {
6
+ it("should not introduce visual regressions for loading button", async () => {
7
+ // Adding a padded wrapper to avoid GitHub Actions diff discrepancies
8
+ const btn = await fixture(html`
9
+ <div style="height: 38px; width: 88px; display: inline-block;">
10
+ <button class="s-btn is-loading" type="button">Loading</button>
11
+ </div>
12
+ `);
13
+
14
+ await visualDiff(btn, "s-btn-is-loading");
15
+ });
16
+ });