@yuneta/gobj-ui 7.26.28 → 7.26.30

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.
@@ -0,0 +1,145 @@
1
+ /***********************************************************************
2
+ * yui_badge.js
3
+ *
4
+ * The BADGE: the little count a control carries -- unread
5
+ * notifications, active alarms. The toolbar items of C_YUI_SHELL
6
+ * and the menu items of C_YUI_NAV build and paint theirs here, so
7
+ * the two follow the same rules and cannot drift apart:
8
+ *
9
+ * EMPTY IS 0, "" AND null ALIKE (false and undefined too): a badge
10
+ * reading "0" is worse than no badge -- it draws the eye to say
11
+ * nothing. A count is only worth a pixel when there IS something.
12
+ *
13
+ * CAPPED AT 99+: a badge sits in a row of fixed width (the app bar,
14
+ * a bottom icon bar), and a four digit pill pushes the items next
15
+ * to it off a phone screen. A string passes through, for the
16
+ * states that are not counts ("!", "…").
17
+ *
18
+ * ALWAYS RENDERED, hidden when empty, so the runtime setters have a
19
+ * node to write into and never rebuild the control that holds it.
20
+ *
21
+ * role="status" and NOT aria-hidden, deliberately. The control
22
+ * carries an explicit aria-label, and an explicit label REPLACES
23
+ * the element's content for a screen reader -- so a badge inside
24
+ * it would be silent. As its own live region it is both read and
25
+ * announced when the count changes, which is the whole point of a
26
+ * badge that means "something needs you". For the same reason a
27
+ * write that changes nothing touches no DOM: rewriting the same
28
+ * number would have it announced again on every tick.
29
+ *
30
+ * A TONE, optional: one of BADGE_TONES, drawn as Bulma's
31
+ * `is-<tone>` on the badge. It says HOW BAD the count is -- the
32
+ * alarms of an app coloured by the worst state among them. No
33
+ * tone is the default look (the danger colour: a badge exists to
34
+ * pull the eye). An unknown tone is the caller's error: it is
35
+ * logged, and the default look is drawn instead.
36
+ *
37
+ * The text is a Text node, never a string: a value that came from
38
+ * outside must not reach createElement2() as markup.
39
+ *
40
+ * Copyright (c) 2026, ArtGins.
41
+ * All Rights Reserved.
42
+ ***********************************************************************/
43
+ import {log_error} from "@yuneta/gobj-js";
44
+
45
+ import {text_node} from "./yui_text.js";
46
+
47
+
48
+ /***************************************************************
49
+ * Constants
50
+ ***************************************************************/
51
+ export const BADGE_TONES = Object.freeze([
52
+ "danger", "warning", "info", "success", "primary", "link"
53
+ ]);
54
+
55
+
56
+ /************************************************************
57
+ * The text a badge shows for `value`, "" when it shows none.
58
+ ************************************************************/
59
+ export function badge_text(value)
60
+ {
61
+ if(value === null || value === undefined || value === false) {
62
+ return "";
63
+ }
64
+ if(typeof value === "number") {
65
+ if(!isFinite(value) || value <= 0) {
66
+ return "";
67
+ }
68
+ return (value > 99) ? "99+" : String(Math.floor(value));
69
+ }
70
+ return String(value).trim();
71
+ }
72
+
73
+ /************************************************************
74
+ * The tone to draw: `tone` when it is one of BADGE_TONES, ""
75
+ * (the default look) when it is empty. Anything else is logged,
76
+ * naming `who`, and answers "".
77
+ ************************************************************/
78
+ export function badge_tone(tone, who)
79
+ {
80
+ if(tone === undefined || tone === null || tone === "") {
81
+ return "";
82
+ }
83
+ if(BADGE_TONES.indexOf(tone) < 0) {
84
+ log_error(
85
+ `${who}: unknown badge tone '${tone}', drawn with the default look ` +
86
+ `(the tones: ${BADGE_TONES.join(", ")})`
87
+ );
88
+ return "";
89
+ }
90
+ return tone;
91
+ }
92
+
93
+ /************************************************************
94
+ * The createElement2() description of a badge. `classes` are
95
+ * the host's (its logical name, `yui-badge` for the look, and
96
+ * where it sits); `who` names the item in the log of an unknown
97
+ * tone.
98
+ ************************************************************/
99
+ export function badge_node(value, tone, classes, who)
100
+ {
101
+ let text = badge_text(value);
102
+ let known = badge_tone(tone, who);
103
+ let attrs = {
104
+ class: known ? `${classes} is-${known}` : classes,
105
+ role: "status"
106
+ };
107
+ if(!text) {
108
+ attrs.hidden = "hidden";
109
+ }
110
+ return ["span", attrs, text_node(text)];
111
+ }
112
+
113
+ /************************************************************
114
+ * Write a badge that is already in the DOM, as badge_node()
115
+ * draws it. `tone` is one badge_tone() answered: the setter
116
+ * checks it once for all the badges it paints.
117
+ ************************************************************/
118
+ export function paint_badge($badge, value, tone)
119
+ {
120
+ for(let known of BADGE_TONES) {
121
+ let cls = `is-${known}`;
122
+ if(known === tone) {
123
+ if(!$badge.classList.contains(cls)) {
124
+ $badge.classList.add(cls);
125
+ }
126
+ } else if($badge.classList.contains(cls)) {
127
+ $badge.classList.remove(cls);
128
+ }
129
+ }
130
+
131
+ let text = badge_text(value);
132
+ if(!text) {
133
+ $badge.textContent = "";
134
+ $badge.setAttribute("hidden", "hidden");
135
+ return;
136
+ }
137
+ /* Do not touch the DOM when nothing changed: role="status" is a
138
+ * live region, and rewriting the same number would have a screen
139
+ * reader announce it again on every tick. */
140
+ if($badge.textContent === text && !$badge.hasAttribute("hidden")) {
141
+ return;
142
+ }
143
+ $badge.textContent = text;
144
+ $badge.removeAttribute("hidden");
145
+ }
@@ -0,0 +1,148 @@
1
+ /***********************************************************************
2
+ * yui_badge.test.js
3
+ *
4
+ * The badge shared by the toolbar items of C_YUI_SHELL and the
5
+ * menu items of C_YUI_NAV: what it shows for a value, which tones
6
+ * it takes, how it is drawn and how it is repainted. The text is a
7
+ * Text node, so a document double is installed.
8
+ *
9
+ * Copyright (c) 2026, ArtGins.
10
+ * All Rights Reserved.
11
+ ***********************************************************************/
12
+ import {describe, test, expect, beforeAll, beforeEach} from "vitest";
13
+ import {install_dom_double} from "../test/dom_double.js";
14
+
15
+ install_dom_double();
16
+
17
+ const {createElement2, set_log_callback} = await import("@yuneta/gobj-js");
18
+ const {
19
+ BADGE_TONES, badge_text, badge_tone, badge_node, paint_badge,
20
+ } = await import("./yui_badge.js");
21
+
22
+ const logged = [];
23
+
24
+ beforeAll(() => {
25
+ set_log_callback((level, msg) => {
26
+ logged.push({level: String(level), msg: String(msg)});
27
+ });
28
+ });
29
+
30
+ beforeEach(() => {
31
+ logged.length = 0;
32
+ });
33
+
34
+ function errors()
35
+ {
36
+ return logged.filter((l) => l.level === "error").map((l) => l.msg);
37
+ }
38
+
39
+ function tones_of($badge)
40
+ {
41
+ return BADGE_TONES.filter((tone) => $badge.classList.contains(`is-${tone}`));
42
+ }
43
+
44
+
45
+ describe("badge_text", () => {
46
+ test("empty is 0, '', null, false and undefined alike", () => {
47
+ for(const v of [0, "", null, false, undefined, -3, NaN, Infinity, " "]) {
48
+ expect(badge_text(v), String(v)).toBe("");
49
+ }
50
+ });
51
+
52
+ test("a count, capped at 99+, and a text passes through", () => {
53
+ expect(badge_text(1)).toBe("1");
54
+ expect(badge_text(99)).toBe("99");
55
+ expect(badge_text(100)).toBe("99+");
56
+ expect(badge_text(2.7)).toBe("2");
57
+ expect(badge_text(" ! ")).toBe("!");
58
+ expect(badge_text("3")).toBe("3");
59
+ });
60
+ });
61
+
62
+ describe("badge_tone", () => {
63
+ test("the six tones, and no tone, pass without a word", () => {
64
+ expect(BADGE_TONES).toEqual(["danger", "warning", "info", "success", "primary", "link"]);
65
+ for(const tone of BADGE_TONES) {
66
+ expect(badge_tone(tone, "who")).toBe(tone);
67
+ }
68
+ expect(badge_tone(undefined, "who")).toBe("");
69
+ expect(badge_tone(null, "who")).toBe("");
70
+ expect(badge_tone("", "who")).toBe("");
71
+ expect(errors()).toEqual([]);
72
+ });
73
+
74
+ test("an unknown one is logged, naming who asked, and answers the default", () => {
75
+ expect(badge_tone("red", "C_TEST: item 'x'")).toBe("");
76
+ expect(badge_tone("Danger", "C_TEST: item 'x'")).toBe("");
77
+ expect(badge_tone(3, "C_TEST: item 'x'")).toBe("");
78
+ let e = errors();
79
+ expect(e.length).toBe(3);
80
+ expect(e[0]).toContain("C_TEST: item 'x'");
81
+ expect(e[0]).toContain("'red'");
82
+ });
83
+ });
84
+
85
+ describe("badge_node", () => {
86
+ test("a live region with the host's classes, its tone and its text as text", () => {
87
+ let $b = createElement2(badge_node(3, "warning", "X_BADGE yui-badge", "who"));
88
+ expect($b.tagName).toBe("SPAN");
89
+ expect($b.getAttribute("role")).toBe("status");
90
+ expect($b.hasAttribute("hidden")).toBe(false);
91
+ expect([...$b.classList]).toEqual(["X_BADGE", "yui-badge", "is-warning"]);
92
+ expect($b.textContent).toBe("3");
93
+ expect($b.firstChild.nodeType).toBe(3);
94
+ });
95
+
96
+ test("empty: drawn all the same, hidden, so a setter can write into it", () => {
97
+ let $b = createElement2(badge_node(0, "", "X_BADGE yui-badge", "who"));
98
+ expect($b.hasAttribute("hidden")).toBe(true);
99
+ expect($b.textContent).toBe("");
100
+ expect(tones_of($b)).toEqual([]);
101
+ });
102
+
103
+ test("markup in a value is text", () => {
104
+ let $b = createElement2(badge_node("<img src=x>", "", "X_BADGE", "who"));
105
+ expect($b.children.length).toBe(0);
106
+ expect($b.textContent).toBe("<img src=x>");
107
+ });
108
+
109
+ test("an unknown tone is logged and drawn with the default look", () => {
110
+ let $b = createElement2(badge_node(3, "red", "X_BADGE", "C_TEST: item 'x'"));
111
+ expect(tones_of($b)).toEqual([]);
112
+ expect(errors().length).toBe(1);
113
+ });
114
+ });
115
+
116
+ describe("paint_badge", () => {
117
+ test("sets, changes and clears the tone, one at a time", () => {
118
+ let $b = createElement2(badge_node(3, "danger", "X_BADGE", "who"));
119
+ paint_badge($b, 3, "info");
120
+ expect(tones_of($b)).toEqual(["info"]);
121
+ paint_badge($b, 3, "");
122
+ expect(tones_of($b)).toEqual([]);
123
+ paint_badge($b, 3, "success");
124
+ expect(tones_of($b)).toEqual(["success"]);
125
+ });
126
+
127
+ test("hides on empty, shows again on a count", () => {
128
+ let $b = createElement2(badge_node(3, "", "X_BADGE", "who"));
129
+ paint_badge($b, 0, "");
130
+ expect($b.hasAttribute("hidden")).toBe(true);
131
+ expect($b.textContent).toBe("");
132
+ paint_badge($b, 150, "");
133
+ expect($b.hasAttribute("hidden")).toBe(false);
134
+ expect($b.textContent).toBe("99+");
135
+ });
136
+
137
+ test("the same text again, or only a new tone, does not rewrite the text", () => {
138
+ let $b = createElement2(badge_node(5, "", "X_BADGE", "who"));
139
+ let text = $b.firstChild;
140
+ paint_badge($b, 5, "");
141
+ paint_badge($b, 5, "warning");
142
+ expect($b.firstChild).toBe(text);
143
+ expect(tones_of($b)).toEqual(["warning"]);
144
+ paint_badge($b, 6, "warning");
145
+ expect($b.firstChild).not.toBe(text);
146
+ expect($b.textContent).toBe("6");
147
+ });
148
+ });
@@ -197,7 +197,7 @@ function open_gclass_view(host, gclass_name, opts)
197
197
  dialog: true,
198
198
  logical_class: "GCLASS_VIEW_SHEET",
199
199
  title_prefix: opts.title_prefix || "",
200
- title: /* i18n: data */ gclass_name,
200
+ title: gclass_name,
201
201
  t: t,
202
202
  on_close: on_close,
203
203
  });
package/src/yui_time.js CHANGED
@@ -544,7 +544,7 @@ function period_name(period, t)
544
544
  {
545
545
  let spec = period_spec(period);
546
546
  if(!spec) {
547
- return tr(t, /* i18n: data */ String((period && period.id) || period || ""));
547
+ return tr(t, String((period && period.id) || period || ""));
548
548
  }
549
549
  return tr(t, /* i18n: YUI_TIME_I18N_KEYS */ spec.id);
550
550
  }