tosijs-ui 1.16.33 → 1.16.34

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/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.16.34
4
+
5
+ Spinning icons keep spinning while the page is busy, which is the only time a loader is on
6
+ screen. Nothing to change unless your CSS or script reaches inside a spin icon: see the last
7
+ bullet.
8
+
9
+ ### Icons
10
+
11
+ - **`spinN<Icon>` no longer freezes on a busy page** (board #3508). The rotation was started
12
+ with `el.animate()` on the `<svg>`, and WebKit animates an SVG transform on the main thread,
13
+ so on Safari and every iOS browser a loader stood still exactly while the page worked. It is
14
+ now a CSS animation on an HTML element around a static svg, which the browser can run off the
15
+ main thread.
16
+ - **A spin icon turns as a clone and inside a shadow root.** The animation is in the markup
17
+ rather than started by script, so `cloneNode` and list templates keep it. The spinning
18
+ element is a `<tosi-icon-spin>`, which adds one shared stylesheet to the document or shadow
19
+ root it lands in, because a document-level `@keyframes` does not reach a shadow root in
20
+ Chromium or Firefox. Nothing is added inside the icon, so a button holding a spinner still
21
+ has its caption as its `textContent`.
22
+ - **Spin icons hold still under `prefers-reduced-motion: reduce`**, as the live-example spinner
23
+ already did. Shipped in a patch on the maintainer's call: it is an accessibility default. The
24
+ rules are wrapped in `:where()`, so a rule of yours that names `tosi-icon-spin` overrides
25
+ them; for an icon inside a component's shadow root, that rule has to be in the shadow root.
26
+ - **The DOM inside a spin icon changed.** It was `span.tosi-icon-composite > svg`; it is now
27
+ `span.tosi-icon-composite > tosi-icon-spin > svg`, with a span carrying the suffix transform
28
+ around `<tosi-icon-spin>` when the name has `s`/`x`/`y`/`r`/`f` suffixes
29
+ (`spin90Loader40s_30x`). That keeps an offset icon spinning about its own centre, as before,
30
+ and both hold the svg's size in a taller flex container so the icon does not orbit. Speed
31
+ and direction are the `--tosi-icon-spin-duration` and `--tosi-icon-spin-direction` custom
32
+ properties on `<tosi-icon-spin>`.
33
+
3
34
  ## 1.16.33
4
35
 
5
36
  Typing a link on a headset gets much shorter, and a mistyped code no longer sends you off to mint
package/dist/icons.js CHANGED
@@ -438,6 +438,10 @@ that feed back into the resolution pipeline:
438
438
  - `spin<dps><Icon>` — continuous rotation at N°/second (e.g. `spin360Loader`)
439
439
  - `spin_<dps><Icon>` — counter-clockwise (e.g. `spin_180Star`)
440
440
 
441
+ The rotation is a CSS animation on an HTML wrapper, not on the `<svg>`, so the browser can
442
+ run it off the main thread and it keeps turning while the page is busy. Under
443
+ `prefers-reduced-motion: reduce` it does not turn.
444
+
441
445
  The overlay rules are just string rewrites — for example, `unFoo`
442
446
  becomes `slash25o$foo75s75o`. **Overlay icons should have a square
443
447
  viewBox** for best results on non-square base icons.
@@ -683,27 +687,89 @@ export const svg2DataUrl = (icon, fill, stroke, strokeWidth) => {
683
687
  };
684
688
  // Appends _ to names ending in digits to prevent suffix ambiguity
685
689
  const safeIconSuffix = (name) => /\d$/.test(name) ? name + '_' : name;
686
- // The rule list and how names match it live in icon-rules.ts, shared with iconSvg. Spin
687
- // animates, so its renderer is installed here.
690
+ /*
691
+ The rule list and how names match it live in icon-rules.ts, shared with iconSvg. Spin
692
+ animates, so its renderer is installed here.
693
+
694
+ The rotation is a CSS animation on an HTML span, never on the svg (board #3508). WebKit runs
695
+ an animated transform on an SVG element on the main thread, whichever API starts it, so a
696
+ loader froze exactly while the page was busy: the one time a loader is on screen. It was also
697
+ started with `el.animate()`, which lives in script rather than in the markup, so a clone of
698
+ the icon (a list template, `cloneNode`) stood still.
699
+
700
+ The spinning element is a `<tosi-icon-spin>`, which on connecting adds one shared stylesheet
701
+ to whatever root it lands in. A document-level `@keyframes` does not reach an icon in a shadow
702
+ root in Chromium or Firefox (measured), and icons are drawn inside shadow-DOM components. A
703
+ `<style>` carried inside each icon did reach, but its CSS became part of the `textContent` of
704
+ every button holding a spinner, which is what `toHaveText` and caption matching read.
705
+
706
+ The suffix transform (`spin90Loader40s_30x`) moves to a span OUTSIDE the rotation, so the
707
+ icon still spins about its own centre, as it did when the rotation was appended to that
708
+ transform, instead of orbiting the offset. Both are `align-self: flex-start` so they stay the
709
+ svg's size: stretched by a taller flex container, the rotation's centre left the svg's and
710
+ the icon orbited.
711
+
712
+ Speed and direction are custom properties on the element, so the animation is a rule and
713
+ `prefers-reduced-motion` can stop it, as it stops the live-example spinner. The rules are in
714
+ `:where()`, so any rule of an adopter's that names `tosi-icon-spin` overrides them.
715
+ */
716
+ const SPIN_CSS = '@keyframes tosi-icon-spin{to{transform:rotate(360deg)}}' +
717
+ ':where(tosi-icon-spin){animation:tosi-icon-spin var(--tosi-icon-spin-duration) linear ' +
718
+ 'infinite var(--tosi-icon-spin-direction)}' +
719
+ '@media (prefers-reduced-motion:reduce){:where(tosi-icon-spin){animation:none}}';
720
+ const spinStyled = new WeakSet();
721
+ let spinSheet;
722
+ function addSpinStyles(root) {
723
+ if (spinStyled.has(root))
724
+ return;
725
+ // A document or a shadow root; a detached subtree's root is neither and gets styled on
726
+ // connecting.
727
+ const isDocument = root.nodeType === Node.DOCUMENT_NODE;
728
+ if (!isDocument && !root.host)
729
+ return;
730
+ const target = root;
731
+ spinStyled.add(root);
732
+ try {
733
+ if (!spinSheet) {
734
+ spinSheet = new CSSStyleSheet();
735
+ spinSheet.replaceSync(SPIN_CSS);
736
+ }
737
+ target.adoptedStyleSheets = [...target.adoptedStyleSheets, spinSheet];
738
+ }
739
+ catch {
740
+ // No constructable stylesheets (Safari before 16.4): a <style> in the root, not the icon.
741
+ const style = document.createElement('style');
742
+ style.textContent = SPIN_CSS;
743
+ (isDocument ? target.head : target).append(style);
744
+ }
745
+ }
746
+ class TosiIconSpin extends HTMLElement {
747
+ connectedCallback() {
748
+ addSpinStyles(this.getRootNode());
749
+ }
750
+ }
751
+ if (globalThis.customElements && !customElements.get('tosi-icon-spin')) {
752
+ customElements.define('tosi-icon-spin', TosiIconSpin);
753
+ }
754
+ const spinBox = { display: 'inline-flex', alignSelf: 'flex-start' };
688
755
  spinRule.apply = (baseName, match, parts) => {
689
756
  const dps = match[1].replace('_', '-');
690
757
  const duration = 360 / Math.abs(parseFloat(dps));
691
758
  const direction = dps.startsWith('-') ? 'reverse' : 'normal';
692
759
  // Resolve baseName with suffixes intact — they apply to the icon
693
760
  const icon = resolveIcon(baseName, []);
694
- const el = icon;
695
- if (el.animate) {
696
- const base = el.style.transform || '';
697
- el.animate([
698
- { transform: `${base} rotate(0deg)` },
699
- { transform: `${base} rotate(360deg)` },
700
- ], {
701
- duration: duration * 1000,
702
- iterations: Infinity,
703
- direction,
704
- });
761
+ const { transform, transformOrigin } = icon.style;
762
+ icon.style.transform = '';
763
+ icon.style.transformOrigin = '';
764
+ let spun = document.createElement('tosi-icon-spin');
765
+ Object.assign(spun.style, spinBox);
766
+ spun.style.setProperty('--tosi-icon-spin-duration', `${duration}s`);
767
+ spun.style.setProperty('--tosi-icon-spin-direction', direction);
768
+ spun.append(icon);
769
+ if (transform) {
770
+ spun = elements.span({ style: { ...spinBox, transform, transformOrigin } }, spun);
705
771
  }
706
- return wrapIcon(baseName, parts, icon);
772
+ return wrapIcon(baseName, parts, spun);
707
773
  };
708
774
  let iconIdCounter = 0;
709
775
  function makeIcon(spec, parts) {