@cosmicdrift/kumiko-renderer-web 0.248.0 → 0.250.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.
- package/package.json +5 -5
- package/src/__tests__/kumiko-screen.test.tsx +183 -0
- package/src/__tests__/render-edit.test.tsx +33 -0
- package/src/primitives/__tests__/select-display-request.test.tsx +3 -3
- package/src/primitives/__tests__/select-segmented.test.tsx +57 -5
- package/src/primitives/index.tsx +13 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.250.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.250.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.250.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.250.0",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@types/react-dom": "^19.2.3",
|
|
65
65
|
"jsdom": "^29.1.1",
|
|
66
66
|
"tailwindcss": "^4.3.0",
|
|
67
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
67
|
+
"@cosmicdrift/kumiko-locale-de": "0.250.0"
|
|
68
68
|
},
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
@@ -816,6 +816,69 @@ describe("KumikoScreen", () => {
|
|
|
816
816
|
expect(screen.queryByText("raw fallback — must NOT appear")).toBeNull();
|
|
817
817
|
});
|
|
818
818
|
|
|
819
|
+
// fw#2752: navigate rowActions may now carry style="danger" — it renders
|
|
820
|
+
// red like the writeHandler variant, but does NOT force a confirm dialog
|
|
821
|
+
// (the target form/screen is itself the confirmation).
|
|
822
|
+
test("entityList rowActions kind=navigate mit style=danger: rot, aber navigiert sofort ohne Dialog", async () => {
|
|
823
|
+
const navigateCalls: { screenId: string }[] = [];
|
|
824
|
+
const memoryNav = {
|
|
825
|
+
route: { screenId: "task-list" },
|
|
826
|
+
navigate: (target: NavTarget) => {
|
|
827
|
+
if ("screenId" in target) navigateCalls.push(target);
|
|
828
|
+
},
|
|
829
|
+
replace: () => undefined,
|
|
830
|
+
hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
|
|
831
|
+
searchParams: {},
|
|
832
|
+
setSearchParams: () => undefined,
|
|
833
|
+
};
|
|
834
|
+
const dispatcher = makeDispatcher({
|
|
835
|
+
query: (async () => ({
|
|
836
|
+
isSuccess: true,
|
|
837
|
+
data: {
|
|
838
|
+
rows: [{ id: "r1", title: "Alpha", count: 1, done: false }],
|
|
839
|
+
nextCursor: null,
|
|
840
|
+
},
|
|
841
|
+
})) as unknown as Dispatcher["query"],
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
const screenWithDangerNav: EntityListScreenDefinition = {
|
|
845
|
+
id: "task-list",
|
|
846
|
+
type: "entityList",
|
|
847
|
+
entity: "task",
|
|
848
|
+
columns: ["title"],
|
|
849
|
+
rowActions: [
|
|
850
|
+
{
|
|
851
|
+
kind: "navigate",
|
|
852
|
+
id: "terminate",
|
|
853
|
+
label: "actions.terminate",
|
|
854
|
+
screen: "task-edit",
|
|
855
|
+
style: "danger",
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
|
|
861
|
+
const user = userEvent.setup();
|
|
862
|
+
render(
|
|
863
|
+
<NavProvider value={memoryNav}>
|
|
864
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
865
|
+
<KumikoScreen
|
|
866
|
+
schema={{ ...schema, screens: [screenWithDangerNav] }}
|
|
867
|
+
qn="tasks:screen:task-list"
|
|
868
|
+
/>
|
|
869
|
+
</DispatcherProvider>
|
|
870
|
+
</NavProvider>,
|
|
871
|
+
);
|
|
872
|
+
await waitFor(() => expect(screen.queryByTestId("kumiko-screen-loading")).toBeNull());
|
|
873
|
+
|
|
874
|
+
const button = screen.getByTestId("row-r1-action-terminate");
|
|
875
|
+
expect(button.className).toContain("text-destructive");
|
|
876
|
+
|
|
877
|
+
await user.click(button);
|
|
878
|
+
await waitFor(() => expect(navigateCalls).toEqual([{ screenId: "task-edit" }]));
|
|
879
|
+
expect(screen.queryByTestId("row-r1-action-terminate-dialog")).toBeNull();
|
|
880
|
+
});
|
|
881
|
+
|
|
819
882
|
// Tier 2.7e-1: rowAction kind="navigate" — Click ruft nav.navigate
|
|
820
883
|
// mit screen-id, ggf. mit URL-Search-Params aus params(row).
|
|
821
884
|
// Reihenfolge ist Teil des Contracts: navigate ZUERST, dann
|
|
@@ -1229,6 +1292,65 @@ describe("KumikoScreen", () => {
|
|
|
1229
1292
|
expect(navigateCalls).toEqual([{ screenId: "task-edit" }]);
|
|
1230
1293
|
});
|
|
1231
1294
|
|
|
1295
|
+
// fw#2752: same decoupling as the rowAction variant — style="danger" on a
|
|
1296
|
+
// navigate toolbarAction renders the destructive button variant without
|
|
1297
|
+
// forcing a confirm dialog.
|
|
1298
|
+
test("entityList toolbarActions navigate-kind mit style=danger: destructive Button, navigiert sofort ohne Dialog", async () => {
|
|
1299
|
+
const navigateCalls: { screenId: string }[] = [];
|
|
1300
|
+
const dispatcher = makeDispatcher({
|
|
1301
|
+
query: (async () => ({
|
|
1302
|
+
isSuccess: true,
|
|
1303
|
+
data: { rows: [{ id: "r1", title: "x", count: 0, done: false }], nextCursor: null },
|
|
1304
|
+
})) as unknown as Dispatcher["query"],
|
|
1305
|
+
});
|
|
1306
|
+
const memoryNav = {
|
|
1307
|
+
route: { screenId: "task-list" },
|
|
1308
|
+
navigate: (target: NavTarget) => {
|
|
1309
|
+
if ("screenId" in target) navigateCalls.push(target);
|
|
1310
|
+
},
|
|
1311
|
+
replace: () => undefined,
|
|
1312
|
+
hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
|
|
1313
|
+
searchParams: {},
|
|
1314
|
+
setSearchParams: () => undefined,
|
|
1315
|
+
};
|
|
1316
|
+
const screenWithToolbar: EntityListScreenDefinition = {
|
|
1317
|
+
id: "task-list",
|
|
1318
|
+
type: "entityList",
|
|
1319
|
+
entity: "task",
|
|
1320
|
+
columns: ["title"],
|
|
1321
|
+
toolbarActions: [
|
|
1322
|
+
{
|
|
1323
|
+
kind: "navigate",
|
|
1324
|
+
id: "terminate-all",
|
|
1325
|
+
label: "actions.terminate-all",
|
|
1326
|
+
screen: "task-edit",
|
|
1327
|
+
style: "danger",
|
|
1328
|
+
},
|
|
1329
|
+
],
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
|
|
1333
|
+
const user = userEvent.setup();
|
|
1334
|
+
render(
|
|
1335
|
+
<NavProvider value={memoryNav}>
|
|
1336
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
1337
|
+
<KumikoScreen
|
|
1338
|
+
schema={{ ...schema, screens: [screenWithToolbar] }}
|
|
1339
|
+
qn="tasks:screen:task-list"
|
|
1340
|
+
/>
|
|
1341
|
+
</DispatcherProvider>
|
|
1342
|
+
</NavProvider>,
|
|
1343
|
+
);
|
|
1344
|
+
await waitFor(() => expect(screen.queryByTestId("kumiko-screen-loading")).toBeNull());
|
|
1345
|
+
|
|
1346
|
+
const button = screen.getByTestId("render-list-toolbar-action-terminate-all");
|
|
1347
|
+
expect(button.getAttribute("data-variant")).toBe("destructive");
|
|
1348
|
+
|
|
1349
|
+
await user.click(button);
|
|
1350
|
+
await waitFor(() => expect(navigateCalls).toEqual([{ screenId: "task-edit" }]));
|
|
1351
|
+
expect(screen.queryByTestId("render-list-toolbar-action-terminate-all-dialog")).toBeNull();
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1232
1354
|
test("entityList toolbarActions writeHandler-kind: Click → dispatcher.write", async () => {
|
|
1233
1355
|
const writeCalls: { type: string; payload: unknown }[] = [];
|
|
1234
1356
|
const dispatcher = makeDispatcher({
|
|
@@ -1789,6 +1911,67 @@ describe("KumikoScreen", () => {
|
|
|
1789
1911
|
expect(writeCalls[0]?.payload).toEqual({ title: "New Task", priority: 1 });
|
|
1790
1912
|
});
|
|
1791
1913
|
|
|
1914
|
+
// fw#2752: actionForm submitStyle reaches the submit button.
|
|
1915
|
+
test("actionForm submitStyle='danger': submit button renders destructive variant", async () => {
|
|
1916
|
+
const dispatcher = makeDispatcher({
|
|
1917
|
+
write: (async () => ({
|
|
1918
|
+
isSuccess: true,
|
|
1919
|
+
data: { id: "new-id" },
|
|
1920
|
+
})) as unknown as Dispatcher["write"],
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
const actionScreen: ActionFormScreenDefinition = {
|
|
1924
|
+
id: "quick-add",
|
|
1925
|
+
type: "actionForm",
|
|
1926
|
+
handler: "tasks:write:task:quick-add",
|
|
1927
|
+
submitStyle: "danger",
|
|
1928
|
+
fields: {
|
|
1929
|
+
title: { type: "text", required: true },
|
|
1930
|
+
priority: { type: "number", default: 1 },
|
|
1931
|
+
},
|
|
1932
|
+
layout: { sections: [{ title: "Basics", fields: ["title", "priority"] }] },
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
render(
|
|
1936
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
1937
|
+
<KumikoScreen schema={{ ...schema, screens: [actionScreen] }} qn="tasks:screen:quick-add" />
|
|
1938
|
+
</DispatcherProvider>,
|
|
1939
|
+
);
|
|
1940
|
+
|
|
1941
|
+
expect(screen.getByTestId("render-edit-submit").getAttribute("data-variant")).toBe(
|
|
1942
|
+
"destructive",
|
|
1943
|
+
);
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// fw#2752: actionForm submitStyle reaches the submit button.
|
|
1947
|
+
test("actionForm ohne submitStyle: submit button renders default variant", async () => {
|
|
1948
|
+
const dispatcher = makeDispatcher({
|
|
1949
|
+
write: (async () => ({
|
|
1950
|
+
isSuccess: true,
|
|
1951
|
+
data: { id: "new-id" },
|
|
1952
|
+
})) as unknown as Dispatcher["write"],
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
const actionScreen: ActionFormScreenDefinition = {
|
|
1956
|
+
id: "quick-add",
|
|
1957
|
+
type: "actionForm",
|
|
1958
|
+
handler: "tasks:write:task:quick-add",
|
|
1959
|
+
fields: {
|
|
1960
|
+
title: { type: "text", required: true },
|
|
1961
|
+
priority: { type: "number", default: 1 },
|
|
1962
|
+
},
|
|
1963
|
+
layout: { sections: [{ title: "Basics", fields: ["title", "priority"] }] },
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
render(
|
|
1967
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
1968
|
+
<KumikoScreen schema={{ ...schema, screens: [actionScreen] }} qn="tasks:screen:quick-add" />
|
|
1969
|
+
</DispatcherProvider>,
|
|
1970
|
+
);
|
|
1971
|
+
|
|
1972
|
+
expect(screen.getByTestId("render-edit-submit").getAttribute("data-variant")).toBe("default");
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1792
1975
|
test("actionForm mit redirect auf entityList: nach success → navigate ohne entityId (#2419)", async () => {
|
|
1793
1976
|
const navigateCalls: NavTarget[] = [];
|
|
1794
1977
|
const dispatcher = makeDispatcher({
|
|
@@ -102,6 +102,39 @@ describe("RenderEdit", () => {
|
|
|
102
102
|
expect(screen.queryByTestId("field-notes")).toBeNull();
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
// fw#2752: submitVariant carries an actionForm's submitStyle through to the
|
|
106
|
+
// submit button — "danger" for a destructive handler (terminate, revoke).
|
|
107
|
+
test("submitVariant='danger' renders the destructive submit button; default stays primary", () => {
|
|
108
|
+
const { rerender } = render(
|
|
109
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
110
|
+
<RenderEdit<TestValues>
|
|
111
|
+
screen={makeScreen()}
|
|
112
|
+
entity={orderEntity}
|
|
113
|
+
featureName="orders"
|
|
114
|
+
initial={{ title: "", count: 0, isUrgent: false }}
|
|
115
|
+
writeCommand="order:create"
|
|
116
|
+
submitVariant="danger"
|
|
117
|
+
/>
|
|
118
|
+
</DispatcherProvider>,
|
|
119
|
+
);
|
|
120
|
+
expect(screen.getByTestId("render-edit-submit").getAttribute("data-variant")).toBe(
|
|
121
|
+
"destructive",
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
rerender(
|
|
125
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
126
|
+
<RenderEdit<TestValues>
|
|
127
|
+
screen={makeScreen()}
|
|
128
|
+
entity={orderEntity}
|
|
129
|
+
featureName="orders"
|
|
130
|
+
initial={{ title: "", count: 0, isUrgent: false }}
|
|
131
|
+
writeCommand="order:create"
|
|
132
|
+
/>
|
|
133
|
+
</DispatcherProvider>,
|
|
134
|
+
);
|
|
135
|
+
expect(screen.getByTestId("render-edit-submit").getAttribute("data-variant")).toBe("default");
|
|
136
|
+
});
|
|
137
|
+
|
|
105
138
|
// A hidden field must not leave an empty grid cell behind — the cell count
|
|
106
139
|
// has to track the visible field count exactly, in both directions.
|
|
107
140
|
test("a hidden field claims no grid cell; toggling visibility adds/removes exactly one cell", () => {
|
|
@@ -10,8 +10,8 @@ import { defaultPrimitives } from "../index";
|
|
|
10
10
|
|
|
11
11
|
const { Field, Input } = defaultPrimitives;
|
|
12
12
|
|
|
13
|
-
// Six options,
|
|
14
|
-
//
|
|
13
|
+
// Six options, past the heuristic's four-option threshold — it would render a
|
|
14
|
+
// dropdown for these.
|
|
15
15
|
const HEURISTIC_REJECTS = [
|
|
16
16
|
"Background Jobs Queue",
|
|
17
17
|
"Inbound Mail Processing",
|
|
@@ -21,7 +21,7 @@ const HEURISTIC_REJECTS = [
|
|
|
21
21
|
"Search Index Rebuild",
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
// Three
|
|
24
|
+
// Three options — the heuristic would render the radio group for these.
|
|
25
25
|
const HEURISTIC_ACCEPTS = ["Draft", "Review", "Done"];
|
|
26
26
|
|
|
27
27
|
function renderSelect(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Segment control for `kind: "select"` with a small closed option set
|
|
2
2
|
// (edit-existing screenshot feedback): a Status field with 3 short values
|
|
3
|
-
// no longer stretches into a full-width dropdown. Threshold: ≤4 options
|
|
4
|
-
//
|
|
3
|
+
// no longer stretches into a full-width dropdown. Threshold: ≤4 options —
|
|
4
|
+
// otherwise the unchanged ComboboxInput dropdown. Label length is explicitly
|
|
5
|
+
// not part of the threshold (#2606).
|
|
5
6
|
|
|
6
7
|
import { describe, expect, test } from "bun:test";
|
|
7
8
|
import { fireEvent } from "@testing-library/react";
|
|
@@ -52,10 +53,10 @@ describe("DefaultInput select → segmented control (edit-existing feedback)", (
|
|
|
52
53
|
expect(screen.getByTestId("combobox-status")).toBeTruthy();
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
test("
|
|
56
|
+
test("a long label no longer pushes the field into the dropdown", () => {
|
|
56
57
|
renderSelect(["Draft", "Review", "Published Status"]);
|
|
57
|
-
expect(screen.
|
|
58
|
-
expect(screen.
|
|
58
|
+
expect(screen.queryByTestId("combobox-status")).toBeNull();
|
|
59
|
+
expect(screen.getAllByRole("radio")).toHaveLength(3);
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
test("clicking a segment reports the same value the dropdown's onChange would give", () => {
|
|
@@ -82,3 +83,54 @@ describe("DefaultInput select → segmented control (edit-existing feedback)", (
|
|
|
82
83
|
expect(changes).toEqual([]);
|
|
83
84
|
});
|
|
84
85
|
});
|
|
86
|
+
|
|
87
|
+
// offlot-app's language picker from #2606: same four option values, the German
|
|
88
|
+
// translations land at 14 chars and the English ones above it.
|
|
89
|
+
const LANGUAGE_OPTIONS_EN = [
|
|
90
|
+
{ value: "", label: "Select a language" },
|
|
91
|
+
{ value: "de", label: "German" },
|
|
92
|
+
{ value: "en", label: "English" },
|
|
93
|
+
{ value: "es", label: "Spanish" },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const LANGUAGE_OPTIONS_DE = [
|
|
97
|
+
{ value: "", label: "Sprache wählen" },
|
|
98
|
+
{ value: "de", label: "Deutsch" },
|
|
99
|
+
{ value: "en", label: "Englisch" },
|
|
100
|
+
{ value: "es", label: "Spanisch" },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
function renderedSelectWidget(
|
|
104
|
+
options: readonly { readonly value: string; readonly label: string }[],
|
|
105
|
+
): "radiogroup" | "combobox" | "neither" {
|
|
106
|
+
const { container, unmount } = render(
|
|
107
|
+
<Field id="language" label="Language" testId="field-language">
|
|
108
|
+
<Input
|
|
109
|
+
kind="select"
|
|
110
|
+
id="language"
|
|
111
|
+
name="language"
|
|
112
|
+
value=""
|
|
113
|
+
onChange={() => {}}
|
|
114
|
+
options={options}
|
|
115
|
+
/>
|
|
116
|
+
</Field>,
|
|
117
|
+
);
|
|
118
|
+
const hasRadioGroup = container.querySelector('[role="radiogroup"]') !== null;
|
|
119
|
+
const hasCombobox = container.querySelector('[data-testid="combobox-language"]') !== null;
|
|
120
|
+
unmount();
|
|
121
|
+
if (hasRadioGroup) return "radiogroup";
|
|
122
|
+
return hasCombobox ? "combobox" : "neither";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
describe("DefaultInput select → widget choice is independent of the UI language", () => {
|
|
126
|
+
test("translated labels of different lengths pick the same widget", () => {
|
|
127
|
+
expect(renderedSelectWidget(LANGUAGE_OPTIONS_EN)).toBe(
|
|
128
|
+
renderedSelectWidget(LANGUAGE_OPTIONS_DE),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("and that widget is the segmented control both times", () => {
|
|
133
|
+
expect(renderedSelectWidget(LANGUAGE_OPTIONS_EN)).toBe("radiogroup");
|
|
134
|
+
expect(renderedSelectWidget(LANGUAGE_OPTIONS_DE)).toBe("radiogroup");
|
|
135
|
+
});
|
|
136
|
+
});
|
package/src/primitives/index.tsx
CHANGED
|
@@ -426,16 +426,14 @@ function withUnitSuffix(unit: string | undefined, input: ReactNode): ReactNode {
|
|
|
426
426
|
// Default presentation for `kind: "select"` with a small closed option set —
|
|
427
427
|
// a 4-value Status field looked wrong stretched into a full-width dropdown
|
|
428
428
|
// (edit-existing screenshot feedback). Only consulted when the caller states
|
|
429
|
-
// no `display` of its own; an explicit `display` wins (#2711).
|
|
429
|
+
// no `display` of its own; an explicit `display` wins (#2711). The option
|
|
430
|
+
// count is the only criterion: labels arrive already translated, so the former
|
|
431
|
+
// label-length threshold made the widget type depend on the active UI language
|
|
432
|
+
// (#2606). Labels that no longer fit wrap inside the group.
|
|
430
433
|
const SEGMENTED_SELECT_MAX_OPTIONS = 4;
|
|
431
|
-
const SEGMENTED_SELECT_MAX_LABEL_LENGTH = 14;
|
|
432
434
|
|
|
433
|
-
function isSegmentedSelectEligible(options: readonly
|
|
434
|
-
return
|
|
435
|
-
options.length > 0 &&
|
|
436
|
-
options.length <= SEGMENTED_SELECT_MAX_OPTIONS &&
|
|
437
|
-
options.every((o) => o.label.length <= SEGMENTED_SELECT_MAX_LABEL_LENGTH)
|
|
438
|
-
);
|
|
435
|
+
function isSegmentedSelectEligible(options: readonly unknown[]): boolean {
|
|
436
|
+
return options.length > 0 && options.length <= SEGMENTED_SELECT_MAX_OPTIONS;
|
|
439
437
|
}
|
|
440
438
|
|
|
441
439
|
// WAI-ARIA radiogroup pattern (role="radiogroup" + role="radio" children):
|
|
@@ -701,7 +699,7 @@ function DefaultInput(props: InputProps): ReactNode {
|
|
|
701
699
|
);
|
|
702
700
|
// An explicit `display` is an author decision and outranks the
|
|
703
701
|
// heuristic in both directions — a requested radio group renders as
|
|
704
|
-
// one even when the
|
|
702
|
+
// one even when the options outnumber the heuristic's threshold (#2711).
|
|
705
703
|
const wantsRadioGroup =
|
|
706
704
|
props.display === "radio" ||
|
|
707
705
|
(props.display === undefined && isSegmentedSelectEligible(comboOptions));
|
|
@@ -1327,10 +1325,13 @@ function RowActionsCell({
|
|
|
1327
1325
|
// Components hatten denselben State-Block dupliziert + parallel zur
|
|
1328
1326
|
// Confirm-Dialog-Render-Logic — der Hook konsolidiert das.
|
|
1329
1327
|
//
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1328
|
+
// The rule: an explicit confirm OR style=danger opens the dialog,
|
|
1329
|
+
// everything else fires straight through.
|
|
1330
|
+
// `confirmRequired` overrides the danger-implies-confirm default (e.g.
|
|
1331
|
+
// schema-driven navigate/drawer actions where the target form is itself
|
|
1332
|
+
// the confirmation).
|
|
1332
1333
|
function needsConfirm(action: DataTableRowAction): boolean {
|
|
1333
|
-
return action.confirm !== undefined || action.style === "danger";
|
|
1334
|
+
return action.confirm !== undefined || (action.confirmRequired ?? action.style === "danger");
|
|
1334
1335
|
}
|
|
1335
1336
|
|
|
1336
1337
|
function useRowActionTrigger(row: ListRowViewModel) {
|