@signal9/era-ui 30.1.3 → 30.1.4
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,3 +1,9 @@
|
|
|
1
|
+
## [30.1.4](https://github.com/sig-nine/era-ui/compare/v30.1.3...v30.1.4) (2026-08-16)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **button:** loading no longer changes the control's width ([7931edb](https://github.com/sig-nine/era-ui/commit/7931edbdb7cbcabf496586983836fc8bf1832e42))
|
|
6
|
+
|
|
1
7
|
## [30.1.3](https://github.com/sig-nine/era-ui/compare/v30.1.2...v30.1.3) (2026-08-16)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
|
@@ -28,9 +28,25 @@
|
|
|
28
28
|
active?: boolean;
|
|
29
29
|
icon?: boolean;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* In flight: disables the button, sets `aria-busy`, and puts a spinner
|
|
32
|
+
* over the label.
|
|
33
|
+
*
|
|
34
|
+
* THE GEOMETRY DOES NOT MOVE. That is the requirement, not a detail. A
|
|
35
|
+
* button is loading because it was just CLICKED, so anything `loading`
|
|
36
|
+
* changes about the box moves the page under the pointer that is still
|
|
37
|
+
* on it — and a spinner PREPENDED to the label (what this used to do)
|
|
38
|
+
* costs an icon's width plus a gap, measured on a wrapping header row
|
|
39
|
+
* that reflowed everything beside it. A consumer hit exactly that and
|
|
40
|
+
* stopped using the prop, hand-rolling `disabled` + `aria-busy`
|
|
41
|
+
* instead, which is the report that produced this note.
|
|
42
|
+
*
|
|
43
|
+
* So the spinner is absolutely positioned and takes no part in the
|
|
44
|
+
* flex layout, and the label goes to `opacity-0` rather than being
|
|
45
|
+
* unmounted: the box that holds the width stays, and the text stays in
|
|
46
|
+
* the accessibility tree as the button's accessible name.
|
|
47
|
+
*
|
|
48
|
+
* The visible label is therefore HIDDEN in flight — the one thing this
|
|
49
|
+
* trades away, and the reason `loadingLabel` exists.
|
|
34
50
|
*/
|
|
35
51
|
loading?: boolean;
|
|
36
52
|
/** Screen-reader-only label announced while `loading` is true. */
|
|
@@ -109,7 +125,15 @@
|
|
|
109
125
|
aria-busy={loading || undefined}
|
|
110
126
|
data-state={active ? 'on' : undefined}
|
|
111
127
|
data-size={size}
|
|
112
|
-
class={cn(
|
|
128
|
+
class={cn(
|
|
129
|
+
buttonVariants({ variant, tone, size, icon, active }),
|
|
130
|
+
// Positioned only while loading — the spinner overlay below needs a
|
|
131
|
+
// containing block, and giving EVERY button one would change paint order
|
|
132
|
+
// against its non-positioned neighbours for a state most of them never
|
|
133
|
+
// enter.
|
|
134
|
+
loading && 'relative',
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
113
137
|
{...restProps}
|
|
114
138
|
>
|
|
115
139
|
<!--
|
|
@@ -142,6 +166,13 @@
|
|
|
142
166
|
data-button-content
|
|
143
167
|
class={cn(
|
|
144
168
|
'inline-flex items-center justify-center [gap:inherit]',
|
|
169
|
+
// THE LABEL KEEPS ITS BOX AND LOSES ITS INK. opacity, not `hidden` or
|
|
170
|
+
// `invisible`: the box has to stay (it is what holds the width) and the
|
|
171
|
+
// text has to stay in the accessibility tree (it is the button's
|
|
172
|
+
// accessible name, and a control that loses its name mid-flight is worse
|
|
173
|
+
// than one that shifts). See the `loading` prop for why the spinner
|
|
174
|
+
// cannot simply sit beside it.
|
|
175
|
+
loading && 'opacity-0',
|
|
145
176
|
// A link is running text, so its wrapper flows inline and takes NO ink
|
|
146
177
|
// centring. era-label-ink blockifies the box and trims it to the
|
|
147
178
|
// typographic band — right for a label in a fixed-height control, and
|
|
@@ -151,10 +182,6 @@
|
|
|
151
182
|
variant === 'link' ? 'inline' : 'not-has-[svg]:era-label-ink'
|
|
152
183
|
)}
|
|
153
184
|
>
|
|
154
|
-
{#if loading}
|
|
155
|
-
<LoaderCircle class="size-icon shrink-0 animate-spin" aria-hidden="true" />
|
|
156
|
-
{#if loadingLabel}<span class="sr-only">{loadingLabel}</span>{/if}
|
|
157
|
-
{/if}
|
|
158
185
|
<!-- The label box exists ONLY when `lead`/`trail` are used. Rendering it
|
|
159
186
|
unconditionally changed the DOM for every legacy call site: a
|
|
160
187
|
`display:contents` span has no box, and the geometry audits walk real
|
|
@@ -171,4 +198,15 @@
|
|
|
171
198
|
{@render children?.()}
|
|
172
199
|
{/if}
|
|
173
200
|
</span>
|
|
201
|
+
<!--
|
|
202
|
+
The spinner is OUT OF FLOW, which is the whole point. Absolutely positioned
|
|
203
|
+
children take no part in flex layout, so the button's width, height, gap and
|
|
204
|
+
min-w are the same in flight as at rest.
|
|
205
|
+
-->
|
|
206
|
+
{#if loading}
|
|
207
|
+
<span class="absolute inset-0 flex items-center justify-center">
|
|
208
|
+
<LoaderCircle class="size-icon shrink-0 animate-spin" aria-hidden="true" />
|
|
209
|
+
</span>
|
|
210
|
+
{#if loadingLabel}<span class="sr-only">{loadingLabel}</span>{/if}
|
|
211
|
+
{/if}
|
|
174
212
|
</Button.Root>
|
|
@@ -7,9 +7,25 @@ type $$ComponentProps = Button.RootProps & {
|
|
|
7
7
|
active?: boolean;
|
|
8
8
|
icon?: boolean;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* In flight: disables the button, sets `aria-busy`, and puts a spinner
|
|
11
|
+
* over the label.
|
|
12
|
+
*
|
|
13
|
+
* THE GEOMETRY DOES NOT MOVE. That is the requirement, not a detail. A
|
|
14
|
+
* button is loading because it was just CLICKED, so anything `loading`
|
|
15
|
+
* changes about the box moves the page under the pointer that is still
|
|
16
|
+
* on it — and a spinner PREPENDED to the label (what this used to do)
|
|
17
|
+
* costs an icon's width plus a gap, measured on a wrapping header row
|
|
18
|
+
* that reflowed everything beside it. A consumer hit exactly that and
|
|
19
|
+
* stopped using the prop, hand-rolling `disabled` + `aria-busy`
|
|
20
|
+
* instead, which is the report that produced this note.
|
|
21
|
+
*
|
|
22
|
+
* So the spinner is absolutely positioned and takes no part in the
|
|
23
|
+
* flex layout, and the label goes to `opacity-0` rather than being
|
|
24
|
+
* unmounted: the box that holds the width stays, and the text stays in
|
|
25
|
+
* the accessibility tree as the button's accessible name.
|
|
26
|
+
*
|
|
27
|
+
* The visible label is therefore HIDDEN in flight — the one thing this
|
|
28
|
+
* trades away, and the reason `loadingLabel` exists.
|
|
13
29
|
*/
|
|
14
30
|
loading?: boolean;
|
|
15
31
|
/** Screen-reader-only label announced while `loading` is true. */
|