diff --git a/.changeset/nine-gifts-learn.md b/.changeset/nine-gifts-learn.md new file mode 100644 index 0000000000..6e364509a9 --- /dev/null +++ b/.changeset/nine-gifts-learn.md @@ -0,0 +1,10 @@ +--- +"@stackoverflow/stacks": minor +"@stackoverflow/stacks-svelte": minor +--- + +Update Loader (formerly known as Spinner) component to SHINE designs + +BREAKING CHANGES: + +- Spinner component has been replace with the Loader component \ No newline at end of file diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index c173481b5c..379e87bb1b 100755 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -72,6 +72,10 @@ - `s-input__xl` removed - **Nested inputs** html will require slight tweaking on consumers' side +#### Loader +- `Spinner` component replaced with new `Loader` component +- `xs` and `xl` sized have been removed, leaving only the default, `sm`, and `lg` sizes + #### Menu The menu component has been updated to use new class names and structure. The following changes are breaking: diff --git a/packages/stacks-classic/lib/components/spinner/spinner.a11y.test.ts b/packages/stacks-classic/lib/components/loader/loader.a11y.test.ts similarity index 69% rename from packages/stacks-classic/lib/components/spinner/spinner.a11y.test.ts rename to packages/stacks-classic/lib/components/loader/loader.a11y.test.ts index 51b9dbf0b7..498d9404a7 100644 --- a/packages/stacks-classic/lib/components/spinner/spinner.a11y.test.ts +++ b/packages/stacks-classic/lib/components/loader/loader.a11y.test.ts @@ -1,11 +1,11 @@ import { runA11yTests } from "../../test/a11y-test-utils"; import "../../index"; -describe("spinner", () => { +describe("loader", () => { runA11yTests({ - baseClass: "s-spinner", + baseClass: "s-loader", modifiers: { - primary: ["xs", "sm", "md", "lg"], + primary: ["sm", "lg"], }, children: { default: `
Loading…
`, diff --git a/packages/stacks-classic/lib/components/loader/loader.less b/packages/stacks-classic/lib/components/loader/loader.less new file mode 100644 index 0000000000..6faf984997 --- /dev/null +++ b/packages/stacks-classic/lib/components/loader/loader.less @@ -0,0 +1,78 @@ +.s-loader { + --_ld-color: var(--black-600); + --_ld-gap: calc(var(--_ld-size) / 2); + --_ld-size: calc(var(--su4) + var(--su1)); // 5px + --_ld-offset: calc(calc(var(--_ld-size) / 8) * -5); // -5/8ths of the size + + // MODIFIERS + &__sm { + --_ld-size: calc(calc(var(--su8) - var(--su1)) / 2); // 3.5px + margin-left: var(--su1); + margin-right: var(--su1); + } + + &__lg { + --_ld-size: var(--su8); + } + + // CHILD ELEMENTS + & &--block, + &:before, + &:after { + background-color: currentColor; + content: ""; + display: block; + height: var(--_ld-size); + width: var(--_ld-size); + + animation: loader-animation .8s cubic-bezier(1, 1, 0, 1) infinite; + } + + & &--block { + animation-delay: .25s; + } + + &:after { + animation-delay: .5s; + } + + display: flex; + gap: var(--_ld-gap); + margin-top: var(--_ld-gap); +} + +@media (prefers-reduced-motion:reduce){ + .s-loader { + & &--block, + &:before, + &:after { + animation: loader-animation-reduced-motion 2s ease-in-out infinite; + } + } +} + +@keyframes loader-animation { + 0%,1%,99%,to{ + opacity: 0.2; + transform: translateY(0); + } + 49%,50%{ + opacity: 1; + transform: translateY(var(--_ld-offset)); + } + 51%{ + opacity: 0.2; + transform: translateY(var(--_ld-offset)); + } +} + +@keyframes loader-animation-reduced-motion { + 0%,to{ + opacity: 0.3; + transform: none; + } + 50%{ + opacity: 1; + transform: none; + } +} diff --git a/packages/stacks-classic/lib/components/spinner/spinner.visual.test.ts b/packages/stacks-classic/lib/components/loader/loader.visual.test.ts similarity index 87% rename from packages/stacks-classic/lib/components/spinner/spinner.visual.test.ts rename to packages/stacks-classic/lib/components/loader/loader.visual.test.ts index 7c7c23a217..9eb4972e48 100644 --- a/packages/stacks-classic/lib/components/spinner/spinner.visual.test.ts +++ b/packages/stacks-classic/lib/components/loader/loader.visual.test.ts @@ -6,12 +6,12 @@ import "../../index"; const template = ({ component, testid }: any) => html`
${component}
`; -describe("spinner", () => { +describe("loader", () => { // default, sizes runVisualTests({ - baseClass: "s-spinner", + baseClass: "s-loader", modifiers: { - primary: ["xs", "sm", "md", "lg"], + primary: ["sm", "lg"], }, children: { default: `
Loading…
`, @@ -20,7 +20,7 @@ describe("spinner", () => { }); // applied font color runVisualTests({ - baseClass: "s-spinner", + baseClass: "s-loader", modifiers: { global: ["fc-theme-primary"], }, diff --git a/packages/stacks-classic/lib/components/spinner/spinner.less b/packages/stacks-classic/lib/components/spinner/spinner.less deleted file mode 100644 index 8f30b0e17a..0000000000 --- a/packages/stacks-classic/lib/components/spinner/spinner.less +++ /dev/null @@ -1,103 +0,0 @@ -.s-spinner { - --_sp-baw: calc(var(--su-static1) * 3); // 3px - --_sp-size: var(--su-static24); - - // MODIFIERS - &&__xs { - --_sp-baw: var(--su-static1); - --_sp-size: var(--su-static12); - } - - &&__sm { - --_sp-baw: var(--su-static2); - --_sp-size: var(--su-static16); - } - - &&__md { - --_sp-baw: var(--su-static4); - --_sp-size: var(--su-static32); - } - - &&__lg { - --_sp-baw: var(--su-static6); - --_sp-size: var(--su-static48); - } - - // CHILD ELEMENTS - &:after, - &:before { - border: var(--_sp-baw) solid currentColor; - - border-radius: var(--br-circle); - content: ''; - height: 100%; - position: absolute; - width: 100%; - } - - &:after { - border-top-color: transparent; - border-right-color: transparent; - border-bottom-color: transparent; - animation: s-spinner-rotate 0.9s infinite cubic-bezier(0.5, 0.1, 0.5, 0.9); - } - - &:before { - opacity: 0.25; - transform: rotate(90deg); // [1] - } - - height: var(--_sp-size); - width: var(--_sp-size); - - position: relative; - text-align: left; // [2] -} - -.is-loading { - --_li-offset: 0.6em; - --_il-size: 1.23076923em; - - &:after, - &:before { - border-radius: var(--br-circle); - border-style: solid; - border-width: var(--su-static2); - content: ""; - height: var(--_il-size); - left: var(--_li-offset); - position: absolute; - top: calc(50% - var(--_li-offset)); - width: var(--_il-size); - } - - &:after { - animation: s-spinner-rotate 0.9s infinite cubic-bezier(0.5, 0.1, 0.5, 0.9); - border-color: transparent; - border-left-color: currentColor; - filter: invert(0); // [1] - transform-origin: 50% 50% var(--su-static1); // [1] - } - - &:before { - border-color: currentColor; - opacity: 0.3; - } - - padding-left: 2.2em; - position: relative; -} - -// Keyframes -@keyframes s-spinner-rotate { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -// [1] no-op to reduce wobble in Edge. More info: https://github.com/StackExchange/Stacks/blob/d2af26aca06c47e3f1f7a638e49b221a9e28e450/lib/css/components/_stacks-spinner.less#L16-L26 - -// [2] When within a parent that has text-align: center, the spinner's entire container spins, not just the spinner. Let's force text-align left so things spin internally. diff --git a/packages/stacks-classic/lib/stacks-static.less b/packages/stacks-classic/lib/stacks-static.less index 9a8680da21..0ec3510d8a 100644 --- a/packages/stacks-classic/lib/stacks-static.less +++ b/packages/stacks-classic/lib/stacks-static.less @@ -42,7 +42,7 @@ @import "components/prose/prose.less"; @import "components/select/select.less"; @import "components/sidebar-widget/sidebar-widget.less"; -@import "components/spinner/spinner.less"; +@import "components/loader/loader.less"; @import "components/table/table.less"; @import "components/table-container/table-container.less"; @import "components/tag/tag.less"; diff --git a/packages/stacks-docs/_data/loader.json b/packages/stacks-docs/_data/loader.json new file mode 100644 index 0000000000..0c37314c0e --- /dev/null +++ b/packages/stacks-docs/_data/loader.json @@ -0,0 +1,25 @@ +{ + "classes": [ + { + "class": ".s-loader", + "applies": "N/A", + "description": "Base class for the loader component" + }, + { + "class": ".s-loader--block", + "applies": ".s-loader", + "description": "Child necessary to render the center loader block" + }, + { + "class": ".s-loader__sm", + "applies": ".s-loader", + "description": "A small variant of the loader component" + }, + { + "class": ".s-loader__lg", + "applies": ".s-loader", + "description": "A large variant of the loader component" + } + ], + "sizes": ["s-loader__sm", "", "s-loader__lg"] +} diff --git a/packages/stacks-docs/_data/site-navigation.json b/packages/stacks-docs/_data/site-navigation.json index d9d23eb950..112abaabfd 100644 --- a/packages/stacks-docs/_data/site-navigation.json +++ b/packages/stacks-docs/_data/site-navigation.json @@ -287,6 +287,10 @@ "title": "Link previews", "url": "/product/components/link-previews/" }, + { + "title": "Loader", + "url": "/product/components/loader/" + }, { "title": "Menus", "url": "/product/components/menus/", @@ -337,10 +341,6 @@ "title": "Sidebar widgets", "url": "/product/components/sidebar-widgets/" }, - { - "title": "Spinner", - "url": "/product/components/spinner/" - }, { "title": "Tables", "url": "/product/components/tables/" @@ -577,4 +577,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/packages/stacks-docs/_data/spinner.json b/packages/stacks-docs/_data/spinner.json deleted file mode 100644 index 70856d481b..0000000000 --- a/packages/stacks-docs/_data/spinner.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "spinner": [ - { - "class": ".s-spinner", - "applies": "N/A", - "description": "Base loading style that is used almost universally" - }, - { - "class": ".s-spinner__xs", - "applies": ".s-spinner", - "description": "An extra small loading style for compact layouts" - }, - { - "class": ".s-spinner__sm", - "applies": ".s-spinner", - "description": "A small style for compact layouts" - }, - { - "class": ".s-spinner__md", - "applies": ".s-spinner", - "description": "A medium style for larger layouts" - }, - { - "class": ".s-spinner__lg", - "applies": ".s-spinner", - "description": "A large style for the largest layouts" - }, - { - "class": ".is-loading", - "applies": "Any text-based elements", - "description": "Prepends a spinner pseudo-element to the element. Prefer using the spinner component when possible." - } - ] -} diff --git a/packages/stacks-docs/product/components/buttons.html b/packages/stacks-docs/product/components/buttons.html index c76c607394..73ec2cdc65 100644 --- a/packages/stacks-docs/product/components/buttons.html +++ b/packages/stacks-docs/product/components/buttons.html @@ -268,10 +268,16 @@
{% header "h2", "Loading" %} -

Any button can have a loading state applied by adding the .is-loading state class.

+

Indicate a loading state by adding a .s-loader component to a button.

{% highlight html %} - + {% endhighlight %}
@@ -298,12 +304,30 @@ {% if btn.modifier != nil %} .{{ btn.modifier }} {% endif %} - .is-loading + .s-loader s-loader__sm
- - - + + + {% endfor %} diff --git a/packages/stacks-docs/product/components/loader.html b/packages/stacks-docs/product/components/loader.html new file mode 100644 index 0000000000..b6bca8c51f --- /dev/null +++ b/packages/stacks-docs/product/components/loader.html @@ -0,0 +1,94 @@ +--- +layout: page +title: Loader +svelte: https://beta.svelte.stackoverflow.design/?path=/docs/components-loader--docs +description: "The loader component indicates an active wait state for a page, section, or interactive element." +tags: components +--- +
+ {% header "h2", "Classes" %} +
+ + + + + + + + + + {% for item in loader.classes %} + + + + + + {% endfor %} + +
ClassApplied toDescription
{{ item.class }}{% if item.applies == "N/A" %}{{ item.applies }}{% else %}{{ item.applies }}{% endif %}{{ item.description }}
+
+
+
+ {% header "h2", "Examples" %} + {% header "h3", "Base" %} +

The base loader component displays three animated squares.

+
+{% highlight html %} +
+
+
Loading…
+
+{% endhighlight %} +
+
+
+
+
Loading…
+
+
+
+
+
+ +
+ {% header "h3", "Sizes" %} + + + + + + + + + + {% for size in loader.sizes %} + + + + + + {% endfor %} + +
ClassApplied toExample
+ + {% if size == "" %} + .s-loader + {% else %} + .{{ size }} + {% endif %} + + + + {% if size == "" %} + N/A + {% else %} + .s-loader + {% endif %} + + +
+
+
Loading…
+
+
+
\ No newline at end of file diff --git a/packages/stacks-docs/product/components/spinner.html b/packages/stacks-docs/product/components/spinner.html deleted file mode 100644 index d794d8d4fd..0000000000 --- a/packages/stacks-docs/product/components/spinner.html +++ /dev/null @@ -1,91 +0,0 @@ ---- -layout: page -title: Spinner -svelte: https://beta.svelte.stackoverflow.design/?path=/docs/components-spinner--docs -description: A loading spinner is used for indicating a loading state of a page or section. It is colored according to the currently applying font color. -tags: components ---- -
- {% header "h2", "Classes" %} -
- - - - - - - - - - {% for item in spinner.spinner %} - - - - - - {% endfor %} - -
ClassApplied toDescription
{{ item.class }}{% if item.applies == "N/A" %}{{ item.applies }}{% else %}{{ item.applies }}{% endif %}{{ item.description }}
-
-
-
- {% header "h2", "Examples" %} -

The spinner’s colors are based on the font color of the element, which will usually be inherited from its parent. If you need to apply a color override, the font color classes can provide themability. In most situations, a black and white spinner based on the default font color will be appropriate.

-

For accessibility, it’s important to add fallback loading text that is visible to screen readers. Additionally, you should add aria-busy="true" to the component that triggered the loading state while the spinner is shown.

-
-{% highlight html %} -
-
Loading…
-
-
-
Loading…
-
-
-
Loading…
-
-
-
Loading…
-
-
-
Loading…
-
-
- Loading… -
-{% endhighlight %} -
-
-
-
-
Loading…
-
-
-
-
-
Loading…
-
-
-
-
-
Loading…
-
-
-
-
-
Loading…
-
-
-
-
-
Loading…
-
-
-
-
- Loading… -
-
-
-
-
-
diff --git a/packages/stacks-svelte/src/components/Button/Button.stories.svelte b/packages/stacks-svelte/src/components/Button/Button.stories.svelte index bf6a5d9aa4..50181ac6cb 100644 --- a/packages/stacks-svelte/src/components/Button/Button.stories.svelte +++ b/packages/stacks-svelte/src/components/Button/Button.stories.svelte @@ -124,7 +124,7 @@ {#each [null, "selected", "disabled"] as state (state)}