@yuneta/gobj-ui 7.26.34 → 7.26.36

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.
Files changed (43) hide show
  1. package/README.md +223 -53
  2. package/dist/gobj-ui.cjs.js +1109 -143
  3. package/dist/gobj-ui.es.js +1109 -143
  4. package/package.json +2 -2
  5. package/scripts/validate-locales.mjs +672 -96
  6. package/src/c_yui_form.js +186 -9
  7. package/src/c_yui_gclass.js +2 -0
  8. package/src/c_yui_gobj_tree_js.js +2 -0
  9. package/src/c_yui_json.js +2 -0
  10. package/src/c_yui_json_graph.js +2 -0
  11. package/src/c_yui_nav.js +23 -5
  12. package/src/c_yui_shell.css +16 -0
  13. package/src/c_yui_shell.js +282 -26
  14. package/src/c_yui_treedb_graph.js +26 -8
  15. package/src/c_yui_treedb_topic_with_form.js +40 -18
  16. package/src/c_yui_treedb_topics.js +34 -11
  17. package/src/c_yui_window.js +132 -32
  18. package/src/c_yui_window_manager.js +250 -46
  19. package/src/form_json_editor_names.js +448 -0
  20. package/src/form_json_editor_names.wiring.test.js +595 -0
  21. package/src/form_label_i18n.wiring.test.js +169 -0
  22. package/src/i18n_key_functions.js +14 -6
  23. package/src/i18n_key_functions.test.js +124 -2
  24. package/src/lib_maplibre.js +26 -2
  25. package/src/lib_maplibre_relocalize.test.js +78 -0
  26. package/src/named_controls.wiring.test.js +468 -0
  27. package/src/nav_cards_helpers.js +9 -1
  28. package/src/nav_item_badge.wiring.test.js +182 -1
  29. package/src/shell_route_map.js +25 -8
  30. package/src/table_editor_name.wiring.test.js +201 -0
  31. package/src/treedb_column_title.wiring.test.js +151 -0
  32. package/src/treedb_json_drill.wiring.test.js +41 -1
  33. package/src/window_layer.wiring.test.js +348 -43
  34. package/src/window_show.wiring.test.js +6 -2
  35. package/src/window_stacking.wiring.test.js +334 -0
  36. package/src/yui_badge.js +58 -13
  37. package/src/yui_badge.test.js +40 -3
  38. package/src/yui_dev.js +2 -1
  39. package/src/yui_file_field.js +2 -0
  40. package/src/yui_frontend_view.js +2 -1
  41. package/src/yui_gclass_view.js +9 -6
  42. package/src/yui_tabulator_i18n.js +42 -0
  43. package/src/yui_tabulator_i18n.test.js +48 -1
@@ -0,0 +1,468 @@
1
+ /***********************************************************************
2
+ * named_controls.wiring.test.js
3
+ *
4
+ * Controls the library drew without a name (REV-CLOUD V23.5.6):
5
+ * each carries a `title` AND an `aria-label` now, through t() and
6
+ * with its key where the key is the library's, as every control.
7
+ *
8
+ * - the find boxes of C_YUI_JSON, C_YUI_JSON_GRAPH,
9
+ * C_YUI_GOBJ_TREE_JS and C_YUI_GCLASS (an aria-label, no title);
10
+ * - the buttons of a file field (a title only after a pass of
11
+ * refresh_language());
12
+ * - a topic tab of C_YUI_TREEDB_TOPICS (only its text);
13
+ * - a link of the site map (named by ALL its content: label,
14
+ * route, gclass, event);
15
+ * - a crumb (no title; no name without a `name`) and a card of
16
+ * C_YUI_NAV (a title only with a `tooltip`);
17
+ * - a chip of the dock (a role=button holding its ✕ button,
18
+ * named by all its content, "close" included): two sibling
19
+ * buttons now, each named;
20
+ * - the ✕ of an option of a Tom Select ("Remove", in English,
21
+ * title only, out of the keyboard's reach).
22
+ *
23
+ * The real gclasses on a document double; G6 and Tom Select as
24
+ * doubles (what is checked is built before either draws).
25
+ *
26
+ * Copyright (c) 2026, ArtGins.
27
+ * All Rights Reserved.
28
+ ***********************************************************************/
29
+ import {describe, test, expect, beforeAll, beforeEach, vi} from "vitest";
30
+ import {install_dom_double} from "../test/dom_double.js";
31
+
32
+ install_dom_double();
33
+
34
+ if(typeof globalThis.ResizeObserver === "undefined") {
35
+ globalThis.ResizeObserver = class {
36
+ observe() {}
37
+ unobserve() {}
38
+ disconnect() {}
39
+ };
40
+ }
41
+
42
+ /* G6: the views build their toolbar before any graph draws. */
43
+ vi.mock("@antv/g6", async (importOriginal) => {
44
+ const real = await importOriginal();
45
+ class FakeGraph {
46
+ constructor(options) {
47
+ this.options = options;
48
+ return new Proxy(this, {
49
+ get: (t, k) => (k in t) ? t[k] : (() => undefined),
50
+ });
51
+ }
52
+ render() {
53
+ return Promise.resolve();
54
+ }
55
+ draw() {
56
+ return Promise.resolve();
57
+ }
58
+ on() {}
59
+ getZoom() {
60
+ return 1;
61
+ }
62
+ getData() {
63
+ return {nodes: [], edges: [], combos: []};
64
+ }
65
+ getNodeData() {
66
+ return [];
67
+ }
68
+ getEdgeData() {
69
+ return [];
70
+ }
71
+ getPlugins() {
72
+ return [];
73
+ }
74
+ getBehaviors() {
75
+ return [];
76
+ }
77
+ getOptions() {
78
+ return {behaviors: []};
79
+ }
80
+ destroy() {}
81
+ }
82
+ return {...real, Graph: FakeGraph};
83
+ });
84
+
85
+ /* Tabulator: the topic forms build one; nothing here reads a table,
86
+ * and the real one lays its columns out with DOM the double lacks. */
87
+ vi.mock("tabulator-tables", () => {
88
+ class FakeTabulator {
89
+ constructor($el, settings) {
90
+ this.element = $el;
91
+ this.options = settings;
92
+ this.externalEvents = {events: {}};
93
+ return new Proxy(this, {
94
+ get(target, prop) {
95
+ if(prop in target) {
96
+ return target[prop];
97
+ }
98
+ return () => [];
99
+ }
100
+ });
101
+ }
102
+ on(ev, fn) {
103
+ (this.externalEvents.events[ev] = this.externalEvents.events[ev] || []).push(fn);
104
+ }
105
+ off(ev) {
106
+ delete this.externalEvents.events[ev];
107
+ }
108
+ destroy() {}
109
+ }
110
+ return {TabulatorFull: FakeTabulator, Tabulator: FakeTabulator};
111
+ });
112
+
113
+ /* Tom Select: keeps the settings it was built with. */
114
+ const selects = vi.hoisted(() => []);
115
+ vi.mock("tom-select", () => ({
116
+ default: class FakeTomSelect {
117
+ constructor(el, settings) {
118
+ this.el = el;
119
+ this.settings = settings;
120
+ selects.push(this);
121
+ }
122
+ on() {}
123
+ disable() {}
124
+ enable() {}
125
+ getValue() {
126
+ return [];
127
+ }
128
+ clear() {}
129
+ setValue() {}
130
+ addOption() {}
131
+ destroy() {}
132
+ }
133
+ }));
134
+
135
+ const {
136
+ SDATA, SDATA_END, data_type_t, gclass_create, gobj_write_attr,
137
+ gobj_start_up, gobj_create_yuno, gobj_create, gobj_create_pure_child, gobj_create_service,
138
+ gobj_start, gobj_read_attr, gobj_send_event, gobj_change_state, gobj_current_state,
139
+ gobj_is_destroying, set_log_callback, register_c_timer,
140
+ } = await import("@yuneta/gobj-js");
141
+ const {register_c_yui_shell} = await import("./c_yui_shell.js");
142
+ const {register_c_yui_nav} = await import("./c_yui_nav.js");
143
+ const {register_c_yui_window} = await import("./c_yui_window.js");
144
+ const {register_c_yui_window_manager} = await import("./c_yui_window_manager.js");
145
+ const {register_c_yui_json} = await import("./c_yui_json.js");
146
+ const {register_c_yui_json_graph} = await import("./c_yui_json_graph.js");
147
+ const {register_c_yui_gobj_tree_js} = await import("./c_yui_gobj_tree_js.js");
148
+ const {register_c_yui_form} = await import("./c_yui_form.js");
149
+ const {register_c_yui_treedb_topics} = await import("./c_yui_treedb_topics.js");
150
+ const {register_c_yui_treedb_topic_with_form} = await import("./c_yui_treedb_topic_with_form.js");
151
+ const {register_c_yui_gclass} = await import("./c_yui_gclass.js");
152
+ const {yui_file_control} = await import("./yui_file_field.js");
153
+ const {yui_shell_show_route_map} = await import("./shell_route_map.js");
154
+
155
+ const logged = [];
156
+ let yuno = null;
157
+ let seq = 0;
158
+
159
+ beforeAll(async () => {
160
+ const i18next = (await import("i18next")).default;
161
+ await i18next.init({lng: "en", resources: {}});
162
+ gobj_start_up(null, null, null, null, null, null, null);
163
+ set_log_callback((level, msg) => {
164
+ logged.push({level: String(level), msg: String(msg)});
165
+ });
166
+ register_c_timer();
167
+ gclass_create("C_TEST_NAMED_HOST", [], [["ST_IDLE", []]], {}, 0, [SDATA_END()], {}, 0, 0, 0, 0);
168
+ gclass_create(
169
+ "C_TEST_NAMED_APP",
170
+ [["EV_RECORD_CHANGED", 0], ["EV_SAVE_RECORD", 0],
171
+ ["EV_RECORD_WRITTEN", 0], ["EV_TOPIC_SELECTED", 0], ["EV_OPERATION_MODE_CHANGED", 0]],
172
+ [["ST_IDLE", [
173
+ ["EV_RECORD_CHANGED", () => 0, null],
174
+ ["EV_SAVE_RECORD", () => 0, null],
175
+ ["EV_RECORD_WRITTEN", () => 0, null],
176
+ ["EV_TOPIC_SELECTED", () => 0, null],
177
+ ["EV_OPERATION_MODE_CHANGED", () => 0, null],
178
+ ]]],
179
+ {}, 0, [SDATA_END()], {}, 0, 0, 0, 0
180
+ );
181
+ gclass_create(
182
+ "C_TEST_NAMED_VIEW", [], [["ST_IDLE", []]], {
183
+ mt_create: (gobj) => {
184
+ gobj_write_attr(gobj, "$container", document.createElement("div"));
185
+ }
186
+ }, 0, [
187
+ SDATA(data_type_t.DTP_POINTER, "$container", 0, null, "view root"),
188
+ SDATA_END()
189
+ ], {}, 0, 0, 0, 0
190
+ );
191
+ gclass_create(
192
+ "C_TEST_NAMED_REMOTE",
193
+ [],
194
+ [["ST_SESSION", []]],
195
+ {mt_command_parser: () => null},
196
+ 0, [SDATA_END()], {}, 0, 0, 0, 0
197
+ );
198
+ register_c_yui_shell();
199
+ register_c_yui_nav();
200
+ register_c_yui_window();
201
+ register_c_yui_window_manager();
202
+ register_c_yui_json();
203
+ register_c_yui_json_graph();
204
+ register_c_yui_gobj_tree_js();
205
+ register_c_yui_form();
206
+ register_c_yui_treedb_topics();
207
+ register_c_yui_treedb_topic_with_form();
208
+ register_c_yui_gclass();
209
+ yuno = gobj_create_yuno("named_controls_yuno", "C_TEST_NAMED_HOST", {});
210
+ gobj_start(yuno);
211
+ });
212
+
213
+ beforeEach(() => {
214
+ logged.length = 0;
215
+ selects.length = 0;
216
+ });
217
+
218
+ /* title and aria-label, both, and the key of each when it is `key`. */
219
+ function expect_named($el, key)
220
+ {
221
+ expect(!!$el, "the control is drawn").toBe(true);
222
+ expect($el.getAttribute("title")).toBe(key);
223
+ expect($el.getAttribute("aria-label")).toBe(key);
224
+ expect($el.getAttribute("data-i18n-title")).toBe(key);
225
+ expect($el.getAttribute("data-i18n-aria-label")).toBe(key);
226
+ }
227
+
228
+ describe("the find boxes", () => {
229
+ test("of the JSON viewer", () => {
230
+ const jv = gobj_create_pure_child(`jv${++seq}`, "C_YUI_JSON", {json_data: {a: 1}}, yuno);
231
+ expect_named(gobj_read_attr(jv, "$container").querySelector(".JSON_SEARCH"), "search");
232
+ });
233
+
234
+ test("of the JSON graph", () => {
235
+ const g = gobj_create_pure_child(`jg${++seq}`, "C_YUI_JSON_GRAPH",
236
+ {$container: document.createElement("div"), json_data: {a: 1}}, yuno);
237
+ expect_named(gobj_read_attr(g, "$container").querySelector(".JSON_GRAPH_FIND_INPUT"), "search");
238
+ });
239
+
240
+ test("of the gobj tree", () => {
241
+ const g = gobj_create_pure_child(`gt${++seq}`, "C_YUI_GOBJ_TREE_JS",
242
+ {$container: document.createElement("div")}, yuno);
243
+ expect_named(gobj_read_attr(g, "$container").querySelector(".GOBJ_TREE_FIND_INPUT"), "search");
244
+ });
245
+
246
+ test("of the gclass view", () => {
247
+ const v = gobj_create(`gc${++seq}`, "C_YUI_GCLASS", {
248
+ description: {id: "C_X", gcflag: [], attrs: [], commands: [],
249
+ gclass_methods: [], internal_methods: [],
250
+ FSM: {events: [], states: {}}},
251
+ current_state: "ST_IDLE",
252
+ }, yuno);
253
+ gobj_start(v);
254
+ expect_named(gobj_read_attr(v, "$container").querySelector(".GCLASS_SEARCH"), "search");
255
+ });
256
+ });
257
+
258
+ describe("the buttons of a file field", () => {
259
+ test("carry their title where they are built, not only after a translation pass", () => {
260
+ const app = gobj_create_pure_child(`fapp${++seq}`, "C_TEST_NAMED_APP", {}, yuno);
261
+ const file = yui_file_control(app, {name: "logo", value: "", readonly: false});
262
+ expect_named(file.$control.querySelector(".FILE_PICK"), "choose a file");
263
+ expect_named(file.$control.querySelector(".FILE_CLEAR"), "remove the file");
264
+ });
265
+ });
266
+
267
+ describe("a topic tab of the treedb view", () => {
268
+ test("carries a title and an aria-label, with the topic as key", () => {
269
+ const remote = gobj_create_service(`tremote${++seq}`, "C_TEST_NAMED_REMOTE", {}, yuno);
270
+ gobj_change_state(remote, "ST_SESSION");
271
+ expect(gobj_current_state(remote)).toBe("ST_SESSION");
272
+ const host = gobj_create(`thost${seq}`, "C_TEST_NAMED_APP", {}, yuno);
273
+ const topics = gobj_create_service(`ttopics${seq}`, "C_YUI_TREEDB_TOPICS", {
274
+ treedb_name: "treedb_test",
275
+ gobj_remote_yuno: remote,
276
+ subscriber: host
277
+ }, host);
278
+ gobj_start(topics);
279
+
280
+ /* The backend's answer to `descs`: one topic, one tab. */
281
+ gobj_send_event(topics, "EV_MT_COMMAND_ANSWER", {
282
+ result: 0, comment: "",
283
+ data: {users: {topic_name: "users", pkey: "id", cols: [
284
+ {id: "id", header: "Id", type: "string", flag: ["persistent", "required"]}
285
+ ]}},
286
+ __md_iev__: {command_stack: [{command: "descs", kw: {}}]}
287
+ }, remote);
288
+
289
+ const $tab = gobj_read_attr(topics, "$container").querySelector(`a[href="ttopics${seq}?users"]`);
290
+ expect_named($tab, "users");
291
+ });
292
+ });
293
+
294
+ describe("a link of the site map", () => {
295
+ test("is named by its label, and says where the reader is", () => {
296
+ const shell = gobj_create(`rshell${++seq}`, "C_YUI_SHELL", {
297
+ config: {
298
+ shell: {
299
+ zones: {left: {host: "menu.primary"}, center: {host: "stage.main"}},
300
+ stages: {main: {zone: "center", default_route: "/home"}}
301
+ },
302
+ menu: {primary: {items: [
303
+ {id: "home", name: "home", icon: "yi-house", route: "/home",
304
+ target: {stage: "main", gclass: "C_TEST_NAMED_VIEW", kw: {}}},
305
+ {id: "devices", name: "devices", route: "/devices",
306
+ target: {stage: "main", gclass: "C_TEST_NAMED_VIEW", kw: {}}}
307
+ ]}}
308
+ },
309
+ use_hash: false
310
+ }, yuno);
311
+ gobj_start(shell);
312
+ logged.length = 0;
313
+
314
+ const win = yui_shell_show_route_map(shell);
315
+ expect(!!win).toBe(true);
316
+ const $body = gobj_read_attr(win, "$container");
317
+ const $link = $body.querySelector('.ROUTEMAP_LINK[href="#/devices"]');
318
+ expect(!!$link).toBe(true);
319
+ expect($link.getAttribute("aria-label")).toBe("devices");
320
+ expect($link.getAttribute("data-i18n-aria-label")).toBe("devices");
321
+ expect($link.getAttribute("title")).toBe("/devices"); // the route: data
322
+ expect($body.querySelector(".ROUTEMAP_ICON i").getAttribute("aria-hidden")).toBe("true");
323
+ /* The shell rests on /home: that row is "you are here". */
324
+ const $here = $body.querySelector(".ROUTEMAP_LINK.ROUTEMAP_CURRENT");
325
+ expect(!!$here).toBe(true);
326
+ expect($here.getAttribute("href")).toBe("#/home");
327
+ expect($here.getAttribute("aria-current")).toBe("page");
328
+ expect($link.getAttribute("aria-current")).toBe(null);
329
+ });
330
+ });
331
+
332
+ describe("the links of a nav", () => {
333
+ function nav(kw, items)
334
+ {
335
+ const n = gobj_create(`nnav${++seq}`, "C_YUI_NAV",
336
+ Object.assign({menu_id: "lab", menu_items: items}, kw), yuno);
337
+ gobj_start(n);
338
+ return gobj_read_attr(n, "$container");
339
+ }
340
+
341
+ test("a crumb: a title and a name, also without a `name`", () => {
342
+ const $c = nav({layout: "breadcrumb"}, [
343
+ {id: "b", name: "devices", route: "/b"},
344
+ {id: "i", icon: "yi-eye", route: "/i"}
345
+ ]);
346
+ expect_named($c.querySelector('a[data-item-id="b"]'), "devices");
347
+ const $icon_only = $c.querySelector('a[data-item-id="i"]');
348
+ expect($icon_only.getAttribute("aria-label")).toBe("i"); // its id: no key
349
+ expect($icon_only.getAttribute("data-i18n-aria-label")).toBe(null);
350
+ });
351
+
352
+ test("a card: a title without a tooltip", () => {
353
+ const $c = nav({layout: "cards"}, [{id: "c", name: "reports", icon: "yi-eye", route: "/c"}]);
354
+ expect_named($c.querySelector('a[data-item-id="c"]'), "reports");
355
+ });
356
+ });
357
+
358
+ describe("a chip of the dock", () => {
359
+ function chip(dock)
360
+ {
361
+ return gobj_read_attr(dock, "$container").querySelector(".DOCK_CHIP");
362
+ }
363
+
364
+ test("is no control itself: two sibling buttons, each named, neither inside the other", () => {
365
+ const dock = gobj_create_service(`ndock${++seq}`, "C_YUI_WINDOW_MANAGER", {}, yuno);
366
+ gobj_start(dock);
367
+ const w = gobj_create_service(`nwin${seq}`, "C_YUI_WINDOW",
368
+ {header: "x", body: ["div", {}, "x"], title: "developer", manager: dock}, yuno);
369
+ gobj_start(w);
370
+
371
+ const $chip = chip(dock);
372
+ /* Up to 7.26.34: a role=button div holding the ✕ button. */
373
+ expect($chip.getAttribute("role")).toBe(null);
374
+ expect($chip.getAttribute("tabindex")).toBe(null);
375
+
376
+ const $toggle = $chip.querySelector(".DOCK_CHIP_TOGGLE");
377
+ const $close = $chip.querySelector(".DOCK_CHIP_CLOSE");
378
+ expect($toggle.tagName).toBe("BUTTON");
379
+ expect($close.tagName).toBe("BUTTON");
380
+ expect($toggle.parentNode === $chip && $close.parentNode === $chip).toBe(true);
381
+ expect($toggle.querySelector("button")).toBe(null);
382
+
383
+ expect_named($toggle, "developer");
384
+ expect_named($close, "close");
385
+ const ids = ($toggle.getAttribute("aria-labelledby") || "").split(" ").filter(Boolean);
386
+ expect(ids.length).toBe(1);
387
+ const $label = $toggle.querySelector(`#${ids[0]}`);
388
+ expect($label.classList.contains("yui-dock-label-kind")).toBe(true);
389
+ expect($label.getAttribute("data-i18n")).toBe("developer");
390
+ });
391
+
392
+ test("with a data half: both halves name the toggle", () => {
393
+ const dock = gobj_create_service(`ndock${++seq}`, "C_YUI_WINDOW_MANAGER", {}, yuno);
394
+ gobj_start(dock);
395
+ const w = gobj_create_service(`nwin${seq}`, "C_YUI_WINDOW", {
396
+ header: "x", body: ["div", {}, "x"],
397
+ title: "keys", title_prefix: "raw_tracks", manager: dock
398
+ }, yuno);
399
+ gobj_start(w);
400
+
401
+ const $toggle = chip(dock).querySelector(".DOCK_CHIP_TOGGLE");
402
+ const names = ($toggle.getAttribute("aria-labelledby") || "").split(" ")
403
+ .map((id) => $toggle.querySelector(`#${id}`).textContent);
404
+ expect(names).toEqual(["raw_tracks", "keys"]);
405
+ });
406
+
407
+ test("its toggle minimises and brings back, through the FSM; its ✕ closes", () => {
408
+ const dock = gobj_create_service(`ndock${++seq}`, "C_YUI_WINDOW_MANAGER", {}, yuno);
409
+ gobj_start(dock);
410
+ const w = gobj_create_service(`nwin${seq}`, "C_YUI_WINDOW",
411
+ {header: "x", body: ["div", {}, "x"], title: "developer", manager: dock}, yuno);
412
+ gobj_start(w);
413
+ const $win = gobj_read_attr(w, "$container");
414
+ const $chip = chip(dock);
415
+
416
+ $chip.querySelector(".DOCK_CHIP_TOGGLE").click(); // active: to the dock
417
+ expect($win.style.display).toBe("none");
418
+ expect($chip.classList.contains("is-min")).toBe(true);
419
+ $chip.querySelector(".DOCK_CHIP_TOGGLE").click(); // back
420
+ expect($win.style.display).toBe(undefined);
421
+ expect($chip.classList.contains("is-active")).toBe(true);
422
+
423
+ /* Only the dock's own chip toggles a window. */
424
+ logged.length = 0;
425
+ gobj_send_event(dock, "EV_TOGGLE_WINDOW", {window: "x^y"}, w);
426
+ expect(logged.some((l) => l.level === "error" && l.msg.includes("dock's own chip"))).toBe(true);
427
+
428
+ $chip.querySelector(".DOCK_CHIP_CLOSE").click();
429
+ expect(gobj_is_destroying(w)).toBe(true);
430
+ expect(chip(dock)).toBe(null);
431
+ });
432
+ });
433
+
434
+ describe("the ✕ of an option of a Tom Select", () => {
435
+ test("is named through t() with its key, and a key presses it", () => {
436
+ const app = gobj_create_pure_child(`sapp${++seq}`, "C_TEST_NAMED_APP", {}, yuno);
437
+ gobj_start(app);
438
+ const form = gobj_create_pure_child(`sform${seq}`, "C_YUI_FORM", {
439
+ template: [
440
+ {id: "id", header: "id", type: "string", flag: ["persistent", "required"]},
441
+ {id: "roles", header: "roles", type: "array",
442
+ flag: ["enum", "persistent", "writable"], enum: ["admin", "user"]},
443
+ ],
444
+ record: {id: "a", roles: []},
445
+ render_mode: "exec",
446
+ editable: true,
447
+ }, app);
448
+ gobj_start(form);
449
+ expect(selects.length).toBe(1);
450
+
451
+ const plugin = selects[0].settings.plugins.remove_button;
452
+ expect(typeof plugin.html).toBe("function");
453
+ const $x = plugin.html({});
454
+ expect_named($x, "remove this option");
455
+ expect($x.getAttribute("role")).toBe("button");
456
+ expect($x.getAttribute("tabindex")).toBe("0");
457
+ expect($x.textContent).toBe("×");
458
+
459
+ let clicks = 0;
460
+ $x.addEventListener("click", () => {
461
+ clicks++;
462
+ });
463
+ $x.dispatchEvent({type: "keydown", key: "Enter", preventDefault() {}, stopPropagation() {}});
464
+ $x.dispatchEvent({type: "keydown", key: " ", preventDefault() {}, stopPropagation() {}});
465
+ $x.dispatchEvent({type: "keydown", key: "a", preventDefault() {}, stopPropagation() {}});
466
+ expect(clicks).toBe(2);
467
+ });
468
+ });
@@ -12,6 +12,7 @@
12
12
  * All Rights Reserved.
13
13
  ***********************************************************************/
14
14
  import {text_node} from "./yui_text.js";
15
+ import {name_control_with_badge} from "./yui_badge.js";
15
16
 
16
17
  /************************************************************
17
18
  * Descriptor of one card: an <a> carrying the same data-*
@@ -58,7 +59,10 @@ export function card_descriptor(it, show_label, badge_of)
58
59
  if(label) {
59
60
  a_attrs["data-i18n-aria-label"] = label;
60
61
  }
61
- let tip = it.tooltip || it.aria_label;
62
+ /* Every control carries a title: the tooltip when one is declared,
63
+ * else its name -- as the other layouts of the nav do. Up to
64
+ * 7.26.34 a card without a `tooltip` had none. */
65
+ let tip = it.tooltip || it.aria_label || label;
62
66
  if(tip) {
63
67
  a_attrs.title = tip;
64
68
  a_attrs["data-i18n-title"] = tip;
@@ -67,6 +71,10 @@ export function card_descriptor(it, show_label, badge_of)
67
71
  a_attrs["aria-disabled"] = "true";
68
72
  a_attrs["tabindex"] = "-1";
69
73
  }
74
+ if(badge) {
75
+ /* The count is part of its name (yui_badge.js) */
76
+ name_control_with_badge(a_attrs, badge);
77
+ }
70
78
  return ["a", a_attrs, children];
71
79
  }
72
80