Standardize action and region loading states

Keep progress inside the initiating Button/IconButton or centered within a
LoadingRegion without changing layout or unmounting live content. Document
ownership and completion/error behavior in the registry and living catalog.

Validation: qualified NET02 DG build/typecheck/registry/loading tests;
existing browser geometry, theme and error lifecycle acceptance. All eleven
committed files match the qualified source artifact byte for byte.
This commit is contained in:
Codex
2026-09-10 09:19:12 +03:00
parent b10fd5d645
commit 8c53f73ee5
11 changed files with 188 additions and 9 deletions
+26 -2
View File
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
import test from "node:test";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { ActivityIndicator } from "../packages/ui-react/dist/index.js";
import { ActivityIndicator, Button, IconButton, LoadingRegion } from "../packages/ui-react/dist/index.js";
test("ActivityIndicator separates decorative and announced progress", () => {
const decorative = renderToStaticMarkup(createElement(ActivityIndicator, { size: "compact" }));
@@ -37,5 +37,29 @@ test("ActivityIndicator is registered, cataloged and motion-safe", async () => {
assert.match(styles, /@media \(prefers-reduced-motion: reduce\) \{\s*\.nodedc-activity-indicator,[\s\S]*?animation: none/);
assert.match(docs, /## ActivityIndicator/);
assert.match(catalog, /<ActivityIndicator label="Загружаем данные"/);
assert.match(catalog, /icon=\{<ActivityIndicator size="compact" \/>\}/);
assert.match(catalog, /<Button loading>/);
assert.match(catalog, /<LoadingRegion loading/);
});
test("pending actions preserve their name and content while disabling only the pending control", () => {
for (const [Component, props] of [[Button, {}], [IconButton, {label: "Обновить"}]]) {
const busy = renderToStaticMarkup(createElement(Component, {...props, loading: true, disabled: false}, "Обновить"));
const idle = renderToStaticMarkup(createElement(Component, props, "Обновить"));
assert.match(busy, /disabled=""/);
assert.match(busy, /aria-busy="true"/);
assert.match(busy, /Обновить/);
assert.equal((busy.match(/class="nodedc-activity-indicator"/g) || []).length, 1);
assert.doesNotMatch(idle, /nodedc-action-loading|disabled=""/);
}
});
test("content remains mounted throughout loading and the status disappears on completion", () => {
const view = createElement("video", {"data-source": "synthetic"});
const pending = renderToStaticMarkup(createElement(LoadingRegion, {loading:true, label:"Ожидаем изображение"}, view));
const ready = renderToStaticMarkup(createElement(LoadingRegion, {loading:false, label:"Ожидаем изображение"}, view));
assert.match(pending, /<video data-source="synthetic"/);
assert.match(ready, /<video data-source="synthetic"/);
assert.equal((pending.match(/role="status"/g) || []).length, 1);
assert.doesNotMatch(ready, /role="status"|nodedc-activity-indicator/);
});