@gjsify/adwaita-web 0.38.1 → 0.40.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/lib/types/adw-button.spec.d.ts +1 -0
- package/lib/types/adw-entry.spec.d.ts +1 -0
- package/lib/types/adw-menu-button.spec.d.ts +1 -0
- package/lib/types/elements/adw-entry.d.ts +6 -0
- package/lib/types/elements/adw-header-bar.d.ts +5 -4
- package/lib/types/elements/adw-menu-button.d.ts +18 -7
- package/package.json +6 -6
- package/src/adw-button.spec.ts +59 -0
- package/src/adw-entry.spec.ts +73 -0
- package/src/adw-header-bar.spec.ts +35 -0
- package/src/adw-menu-button.spec.ts +52 -0
- package/src/elements/adw-button.ts +8 -10
- package/src/elements/adw-entry.ts +53 -8
- package/src/elements/adw-header-bar.ts +32 -9
- package/src/elements/adw-menu-button.ts +20 -19
- package/src/elements/adw-status-page.ts +7 -2
- package/src/test.browser.mts +6 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AdwButtonTest: () => Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AdwEntryTest: () => Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AdwMenuButtonTest: () => Promise<void>;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
export declare class AdwEntry extends HTMLElement {
|
|
2
2
|
private _input;
|
|
3
3
|
private _initialized;
|
|
4
|
+
private _maxLength;
|
|
4
5
|
static get observedAttributes(): string[];
|
|
5
6
|
get value(): string;
|
|
6
7
|
set value(v: string);
|
|
8
|
+
/** `Gtk.Entry:max-length` — 0 means unlimited. Counted in CODE POINTS. */
|
|
9
|
+
get maxLength(): number;
|
|
10
|
+
set maxLength(value: number);
|
|
11
|
+
/** `Gtk.Entry:text-length` — code points, not UTF-16 units. */
|
|
12
|
+
get textLength(): number;
|
|
7
13
|
/** The inner native input (for focus/selection). */
|
|
8
14
|
get input(): HTMLInputElement;
|
|
9
15
|
connectedCallback(): void;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import './adw-window-title.js';
|
|
1
2
|
export declare class AdwHeaderBar extends HTMLElement {
|
|
2
3
|
private _initialized;
|
|
3
4
|
private _startEl;
|
|
4
5
|
private _centerEl;
|
|
5
6
|
private _endEl;
|
|
6
7
|
/**
|
|
7
|
-
* The
|
|
8
|
-
* took the centre. `Adw.HeaderBar` has the same either/or:
|
|
9
|
-
* `title-widget` replaces the derived `AdwWindowTitle` outright.
|
|
8
|
+
* The derived `<adw-window-title>`, or `null` when a `slot="center"`
|
|
9
|
+
* title-widget took the centre. `Adw.HeaderBar` has the same either/or:
|
|
10
|
+
* setting `title-widget` replaces the derived `AdwWindowTitle` outright.
|
|
10
11
|
*/
|
|
11
12
|
private _titleEl;
|
|
12
13
|
static get observedAttributes(): string[];
|
|
@@ -24,6 +25,6 @@ export declare class AdwHeaderBar extends HTMLElement {
|
|
|
24
25
|
* kept whatever it was created with. `Adw.HeaderBar`'s derived title widget
|
|
25
26
|
* is bound to the property and re-renders on every change.
|
|
26
27
|
*/
|
|
27
|
-
attributeChangedCallback(
|
|
28
|
+
attributeChangedCallback(): void;
|
|
28
29
|
private _renderTitle;
|
|
29
30
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import type { SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
1
|
+
import type { AdwMenuEntry, SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
2
2
|
import './adw-popover.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
/**
|
|
4
|
+
* A menu entry — `@gjsify/adwaita-core`'s {@link AdwMenuEntry}, re-exported under the
|
|
5
|
+
* name this element has always used.
|
|
6
|
+
*
|
|
7
|
+
* It used to be a SECOND declaration of the same shape, and the NativeScript menu
|
|
8
|
+
* button declared a THIRD that was missing `icon` entirely. One type, so a consumer
|
|
9
|
+
* writing menu entries writes the same object for either renderer.
|
|
10
|
+
*/
|
|
11
|
+
export type AdwMenuItem = AdwMenuEntry;
|
|
9
12
|
export declare class AdwMenuButton extends HTMLElement {
|
|
10
13
|
private _buttonEl;
|
|
11
14
|
private _popoverEl;
|
|
@@ -27,6 +30,14 @@ export declare class AdwMenuButton extends HTMLElement {
|
|
|
27
30
|
set direction(value: SplitButtonDirection);
|
|
28
31
|
connectedCallback(): void;
|
|
29
32
|
attributeChangedCallback(name: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* The `menu` attribute, through the core parser the split button already used.
|
|
35
|
+
*
|
|
36
|
+
* The copy this replaces was weaker in a way markup can reach: it kept `id` and
|
|
37
|
+
* `icon` WHATEVER their runtime type, so `{"label":"Open","id":7}` produced an
|
|
38
|
+
* entry whose `id` is a number in a field typed `string` — and `id` is what the
|
|
39
|
+
* activation event reports. The core parser keeps only string-typed extras.
|
|
40
|
+
*/
|
|
30
41
|
private _parseMenuAttr;
|
|
31
42
|
private _onPopoverToggled;
|
|
32
43
|
private _render;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/adwaita-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Adwaita/Libadwaita web components for browser targets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"@codemirror/language": "^6.12.4",
|
|
42
42
|
"@codemirror/state": "^6.7.0",
|
|
43
43
|
"@codemirror/view": "^6.43.4",
|
|
44
|
-
"@gjsify/adwaita-core": "^0.
|
|
45
|
-
"@gjsify/adwaita-fonts": "^0.
|
|
46
|
-
"@gjsify/adwaita-icons": "^0.
|
|
44
|
+
"@gjsify/adwaita-core": "^0.40.0",
|
|
45
|
+
"@gjsify/adwaita-fonts": "^0.40.0",
|
|
46
|
+
"@gjsify/adwaita-icons": "^0.40.0",
|
|
47
47
|
"@lezer/highlight": "^1.2.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@gjsify/cli": "^0.
|
|
51
|
-
"@gjsify/unit": "^0.
|
|
50
|
+
"@gjsify/cli": "^0.40.0",
|
|
51
|
+
"@gjsify/unit": "^0.40.0",
|
|
52
52
|
"sass": "^1.101.0",
|
|
53
53
|
"typescript": "^6.0.3"
|
|
54
54
|
},
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// DOM-level tests for <adw-button>'s style classes.
|
|
2
|
+
//
|
|
3
|
+
// The element and its NativeScript twin each kept their own attribute→class table,
|
|
4
|
+
// and the two disagreed: `circular` was here and not there. The table now lives in
|
|
5
|
+
// `@gjsify/adwaita-core`, and these drive its vectors through the REAL element, so a
|
|
6
|
+
// renderer that goes back to a private copy fails on the row that names the class.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
9
|
+
import { BUTTON_STYLE_CLASS_VECTORS } from '@gjsify/adwaita-core/conformance';
|
|
10
|
+
|
|
11
|
+
/** The boolean attributes this element exposes — the short spellings. */
|
|
12
|
+
const WEB_ATTRIBUTES = new Set(['flat', 'suggested', 'destructive', 'circular', 'pill']);
|
|
13
|
+
|
|
14
|
+
/** Mount a button carrying the given boolean attributes; return the inner classes. */
|
|
15
|
+
function mountWithStyles(names: readonly string[]): string[] {
|
|
16
|
+
const el = document.createElement('adw-button');
|
|
17
|
+
el.setAttribute('label', 'Click me');
|
|
18
|
+
for (const name of names) el.setAttribute(name, '');
|
|
19
|
+
document.body.appendChild(el);
|
|
20
|
+
const btn = el.querySelector('button') as HTMLButtonElement;
|
|
21
|
+
return Array.from(btn.classList).filter((cls) => cls !== 'adw-button');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unmountAll(): void {
|
|
25
|
+
for (const el of Array.from(document.querySelectorAll('adw-button'))) el.remove();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const AdwButtonTest = async () => {
|
|
29
|
+
// Rows naming a class the LONG way (`suggested-action`) are the NativeScript
|
|
30
|
+
// variant spelling — this element has no such attribute. They are partitioned
|
|
31
|
+
// out here rather than skipped inside the loop, and the partition is asserted,
|
|
32
|
+
// so a row cannot fall out of coverage unnoticed.
|
|
33
|
+
const reachable = BUTTON_STYLE_CLASS_VECTORS.filter((v) => v.names.every((n) => WEB_ATTRIBUTES.has(n)));
|
|
34
|
+
const unreachable = BUTTON_STYLE_CLASS_VECTORS.filter((v) => !v.names.every((n) => WEB_ATTRIBUTES.has(n)));
|
|
35
|
+
|
|
36
|
+
await describe('<adw-button> style classes', async () => {
|
|
37
|
+
for (const { names, classes, rule } of reachable) {
|
|
38
|
+
await it(`[${names.join(', ')}] → [${classes.join(', ')}] — ${rule}`, () => {
|
|
39
|
+
expect(mountWithStyles(names)).toStrictEqual([...classes]);
|
|
40
|
+
unmountAll();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await it('the rows this element cannot express are exactly the long spellings', () => {
|
|
45
|
+
const names = unreachable.flatMap((v) => v.names).filter((n) => !WEB_ATTRIBUTES.has(n));
|
|
46
|
+
expect(names).toStrictEqual(['suggested-action', 'suggessted', 'not-a-style']);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await it('a style attribute added later is picked up', () => {
|
|
50
|
+
const el = document.createElement('adw-button');
|
|
51
|
+
el.setAttribute('label', 'Send');
|
|
52
|
+
document.body.appendChild(el);
|
|
53
|
+
el.setAttribute('suggested', '');
|
|
54
|
+
const btn = el.querySelector('button') as HTMLButtonElement;
|
|
55
|
+
expect(btn.classList.contains('suggested-action')).toBe(true);
|
|
56
|
+
unmountAll();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// DOM-level tests for <adw-entry>'s length arithmetic.
|
|
2
|
+
//
|
|
3
|
+
// The element had NO max length and no text length, while its NativeScript twin has
|
|
4
|
+
// composed `@gjsify/adwaita-core`'s since it shipped — so the same consumer got a
|
|
5
|
+
// limit on one renderer and none on the other. These drive the core's own vectors
|
|
6
|
+
// against the real element, which is what makes the delegation checkable rather
|
|
7
|
+
// than merely claimed: a re-implementation that counts UTF-16 units instead of code
|
|
8
|
+
// points fails on the row that names it.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
11
|
+
import { ENTRY_MAX_LENGTH_VECTORS, ENTRY_TEXT_LENGTH_VECTORS } from '@gjsify/adwaita-core/conformance';
|
|
12
|
+
|
|
13
|
+
import type { AdwEntry } from './elements/adw-entry.js';
|
|
14
|
+
|
|
15
|
+
/** Mount an entry with the given attributes. */
|
|
16
|
+
function mountEntry(attrs: Record<string, string> = {}): AdwEntry {
|
|
17
|
+
const entry = document.createElement('adw-entry') as AdwEntry;
|
|
18
|
+
for (const [name, value] of Object.entries(attrs)) entry.setAttribute(name, value);
|
|
19
|
+
document.body.appendChild(entry);
|
|
20
|
+
return entry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unmountAll(): void {
|
|
24
|
+
for (const entry of Array.from(document.querySelectorAll('adw-entry'))) entry.remove();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const AdwEntryTest = async () => {
|
|
28
|
+
await describe('<adw-entry> text length', async () => {
|
|
29
|
+
for (const { text, length, rule } of ENTRY_TEXT_LENGTH_VECTORS) {
|
|
30
|
+
await it(`${JSON.stringify(text)} → ${length} — ${rule}`, () => {
|
|
31
|
+
const entry = mountEntry();
|
|
32
|
+
entry.value = text;
|
|
33
|
+
expect(entry.textLength).toBe(length);
|
|
34
|
+
unmountAll();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await describe('<adw-entry> max length', async () => {
|
|
40
|
+
for (const { text, maxLength, clamped, length, rule } of ENTRY_MAX_LENGTH_VECTORS) {
|
|
41
|
+
await it(`${JSON.stringify(text)} @ ${maxLength} — ${rule}`, () => {
|
|
42
|
+
const entry = mountEntry({ maxlength: String(maxLength) });
|
|
43
|
+
entry.value = text;
|
|
44
|
+
expect(entry.value).toBe(clamped);
|
|
45
|
+
expect(entry.textLength).toBe(length);
|
|
46
|
+
unmountAll();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await it('clamps what the ATTRIBUTE writes, not only the property', () => {
|
|
51
|
+
const entry = mountEntry({ maxlength: '5' });
|
|
52
|
+
entry.setAttribute('value', 'Ada Lovelace');
|
|
53
|
+
expect(entry.value).toBe('Ada L');
|
|
54
|
+
unmountAll();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await it('re-clamps when the limit is lowered under the current text', () => {
|
|
58
|
+
const entry = mountEntry();
|
|
59
|
+
entry.value = 'Ada Lovelace';
|
|
60
|
+
entry.maxLength = 3;
|
|
61
|
+
expect(entry.value).toBe('Ada');
|
|
62
|
+
unmountAll();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await it('an unusable limit means unlimited, never "truncate to empty"', () => {
|
|
66
|
+
const entry = mountEntry({ maxlength: 'nonsense' });
|
|
67
|
+
entry.value = 'Ada Lovelace';
|
|
68
|
+
expect(entry.maxLength).toBe(0);
|
|
69
|
+
expect(entry.value).toBe('Ada Lovelace');
|
|
70
|
+
unmountAll();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
};
|
|
@@ -78,4 +78,39 @@ export const AdwHeaderBarTest = async () => {
|
|
|
78
78
|
unmountAll();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
// The centre is the `<adw-window-title>` Adw.HeaderBar itself derives, so the three
|
|
83
|
+
// rules that element holds through @gjsify/adwaita-core reach the header bar too.
|
|
84
|
+
// None of them did while the centre was a bare span.
|
|
85
|
+
await describe('<adw-header-bar> derives an <adw-window-title>', async () => {
|
|
86
|
+
await it('builds one rather than a bare span', () => {
|
|
87
|
+
const bar = mountHeaderBar({ title: 'Documents' });
|
|
88
|
+
expect(bar.querySelector('adw-window-title')).toBeTruthy();
|
|
89
|
+
unmountAll();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await it('renders a subtitle, which it could not before', () => {
|
|
93
|
+
const bar = mountHeaderBar({ title: 'Documents', subtitle: '12 items' });
|
|
94
|
+
expect(centerText(bar)).toContain('Documents');
|
|
95
|
+
expect(centerText(bar)).toContain('12 items');
|
|
96
|
+
unmountAll();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await it('hides the title line when the title is empty — the blank-line bug', () => {
|
|
100
|
+
// adw-window-title.c:207-208. A header bar with only a subtitle used to
|
|
101
|
+
// reserve an empty line above it, because the span was always there.
|
|
102
|
+
const bar = mountHeaderBar({ title: '', subtitle: 'Loading…' });
|
|
103
|
+
const titleEl = bar.querySelector('.adw-window-title-title') as HTMLElement | null;
|
|
104
|
+
expect(titleEl?.hidden).toBe(true);
|
|
105
|
+
unmountAll();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await it('a subtitle survives a title write', () => {
|
|
109
|
+
const bar = mountHeaderBar({ title: 'Documents', subtitle: '12 items' });
|
|
110
|
+
bar.setAttribute('title', 'Pictures');
|
|
111
|
+
expect(centerText(bar)).toContain('Pictures');
|
|
112
|
+
expect(centerText(bar)).toContain('12 items');
|
|
113
|
+
unmountAll();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
81
116
|
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// DOM-level tests for <adw-menu-button>'s `menu` attribute.
|
|
2
|
+
//
|
|
3
|
+
// The element parsed the JSON itself, and the copy was weaker than the core parser
|
|
4
|
+
// the split button next to it already used: it kept `id` and `icon` whatever their
|
|
5
|
+
// runtime type, so author markup could put a number in a field typed `string` — and
|
|
6
|
+
// `id` is exactly what the activation event reports. These drive the core's parse
|
|
7
|
+
// vectors against the real element.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
10
|
+
import { SPLIT_BUTTON_MENU_PARSE_VECTORS } from '@gjsify/adwaita-core/conformance';
|
|
11
|
+
|
|
12
|
+
/** Mount a menu button carrying `menu` as JSON, and return its rendered item labels. */
|
|
13
|
+
function mountWithMenu(json: string): { el: HTMLElement; labels: string[] } {
|
|
14
|
+
const el = document.createElement('adw-menu-button');
|
|
15
|
+
el.setAttribute('menu', json);
|
|
16
|
+
document.body.appendChild(el);
|
|
17
|
+
const labels = Array.from(el.querySelectorAll('.adw-popover button')).map((b) => b.textContent?.trim() ?? '');
|
|
18
|
+
return { el, labels };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function unmountAll(): void {
|
|
22
|
+
for (const el of Array.from(document.querySelectorAll('adw-menu-button'))) el.remove();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const AdwMenuButtonTest = async () => {
|
|
26
|
+
await describe('<adw-menu-button> menu attribute', async () => {
|
|
27
|
+
for (const { json, entries, rule } of SPLIT_BUTTON_MENU_PARSE_VECTORS) {
|
|
28
|
+
await it(`${rule}`, () => {
|
|
29
|
+
const { labels } = mountWithMenu(json ?? '');
|
|
30
|
+
expect(labels).toStrictEqual(entries.map((entry) => entry.label));
|
|
31
|
+
unmountAll();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await it('drops a non-string id instead of leaking a number into the event', () => {
|
|
36
|
+
const el = document.createElement('adw-menu-button');
|
|
37
|
+
el.setAttribute('menu', '[{"label":"Open","id":7}]');
|
|
38
|
+
document.body.appendChild(el);
|
|
39
|
+
|
|
40
|
+
let seen: unknown = 'not fired';
|
|
41
|
+
el.addEventListener('menu-item-activated', (e) => {
|
|
42
|
+
seen = (e as CustomEvent<{ id: string }>).detail.id;
|
|
43
|
+
});
|
|
44
|
+
(el.querySelector('.adw-popover button') as HTMLElement | null)?.click();
|
|
45
|
+
|
|
46
|
+
// `id` fell back to the label, which is the documented default — rather
|
|
47
|
+
// than arriving as the number 7 in a field every consumer reads as text.
|
|
48
|
+
expect(seen).toBe('Open');
|
|
49
|
+
unmountAll();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -9,15 +9,12 @@
|
|
|
9
9
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
10
10
|
// icon node is <adw-icon>.
|
|
11
11
|
|
|
12
|
+
import { buttonStyleClasses } from '@gjsify/adwaita-core';
|
|
13
|
+
|
|
12
14
|
import { createAdwIcon } from './adw-icon.js';
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
suggested: 'suggested-action',
|
|
17
|
-
destructive: 'destructive-action',
|
|
18
|
-
circular: 'circular',
|
|
19
|
-
pill: 'pill',
|
|
20
|
-
};
|
|
16
|
+
/** The boolean attributes that select a style class; the mapping lives in the core. */
|
|
17
|
+
const STYLE_ATTRIBUTES = ['flat', 'suggested', 'destructive', 'circular', 'pill'] as const;
|
|
21
18
|
|
|
22
19
|
export class AdwButton extends HTMLElement {
|
|
23
20
|
private _button!: HTMLButtonElement;
|
|
@@ -50,9 +47,10 @@ export class AdwButton extends HTMLElement {
|
|
|
50
47
|
private _render() {
|
|
51
48
|
const btn = this._button;
|
|
52
49
|
btn.className = 'adw-button';
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
// The attribute → class mapping is `@gjsify/adwaita-core`'s, so this element
|
|
51
|
+
// and the NativeScript one cannot disagree about which classes exist —
|
|
52
|
+
// `circular` was in this table and missing from that one.
|
|
53
|
+
btn.classList.add(...buttonStyleClasses(STYLE_ATTRIBUTES.filter((attr) => this.hasAttribute(attr))));
|
|
56
54
|
|
|
57
55
|
const icon = this.getAttribute('icon');
|
|
58
56
|
const label = (this.getAttribute('label') ?? this._label).trim();
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
// <adw-entry> — Adwaita single-line text entry (e.g. a browser URL bar).
|
|
2
|
-
// Attributes: value, placeholder, type, disabled.
|
|
3
|
-
//
|
|
2
|
+
// Attributes: value, placeholder, type, disabled, maxlength.
|
|
3
|
+
// Properties: value (get/set, proxies the inner input), maxLength, textLength.
|
|
4
4
|
// Events: native `input` bubbles from the inner input; `activate` (CustomEvent)
|
|
5
5
|
// fires on Enter — mirroring Gtk.Entry's `activate` signal.
|
|
6
|
+
//
|
|
7
|
+
// The character arithmetic is HEADLESS and lives in `@gjsify/adwaita-core`
|
|
8
|
+
// (ADR 0004): `entryTextLength` counts CODE POINTS and `clampEntryText` truncates
|
|
9
|
+
// on them. `@gjsify/adwaita-nativescript`'s AdwEntry has composed the same two
|
|
10
|
+
// since it shipped; this element counted nothing at all, so the same consumer
|
|
11
|
+
// got a max length on one renderer and none on the other — and the native
|
|
12
|
+
// `maxlength` attribute would not have closed the gap, because the browser
|
|
13
|
+
// counts UTF-16 code units: `'🔒é'` is 2 characters to GTK and to NativeScript,
|
|
14
|
+
// and 3 to `input.maxLength`. That is why the clamp is applied here rather than
|
|
15
|
+
// handed to the platform.
|
|
16
|
+
//
|
|
6
17
|
// Reference: refs/libadwaita/src/stylesheet/widgets/_entries.scss
|
|
7
18
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
8
|
-
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web
|
|
19
|
+
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
20
|
+
// length arithmetic composed from @gjsify/adwaita-core.
|
|
21
|
+
|
|
22
|
+
import { ENTRY_ROW_MAX_LENGTH_LIMIT, clampEntryText, entryTextLength } from '@gjsify/adwaita-core';
|
|
9
23
|
|
|
10
24
|
export class AdwEntry extends HTMLElement {
|
|
11
25
|
private _input!: HTMLInputElement;
|
|
12
26
|
private _initialized = false;
|
|
27
|
+
private _maxLength = 0;
|
|
13
28
|
|
|
14
29
|
static get observedAttributes() {
|
|
15
|
-
return ['value', 'placeholder', 'type', 'disabled'];
|
|
30
|
+
return ['value', 'placeholder', 'type', 'disabled', 'maxlength'];
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
get value(): string {
|
|
@@ -20,8 +35,26 @@ export class AdwEntry extends HTMLElement {
|
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
set value(v: string) {
|
|
23
|
-
|
|
24
|
-
|
|
38
|
+
const clamped = clampEntryText(v ?? '', this._maxLength);
|
|
39
|
+
if (this._input) this._input.value = clamped;
|
|
40
|
+
else this.setAttribute('value', clamped);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** `Gtk.Entry:max-length` — 0 means unlimited. Counted in CODE POINTS. */
|
|
44
|
+
get maxLength(): number {
|
|
45
|
+
return this._maxLength;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
set maxLength(value: number) {
|
|
49
|
+
this._maxLength = Number.isFinite(value)
|
|
50
|
+
? Math.min(ENTRY_ROW_MAX_LENGTH_LIMIT, Math.max(0, Math.trunc(value)))
|
|
51
|
+
: 0;
|
|
52
|
+
if (this._input) this._input.value = clampEntryText(this._input.value, this._maxLength);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `Gtk.Entry:text-length` — code points, not UTF-16 units. */
|
|
56
|
+
get textLength(): number {
|
|
57
|
+
return entryTextLength(this.value);
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
/** The inner native input (for focus/selection). */
|
|
@@ -33,12 +66,20 @@ export class AdwEntry extends HTMLElement {
|
|
|
33
66
|
if (this._initialized) return;
|
|
34
67
|
this._initialized = true;
|
|
35
68
|
|
|
69
|
+
this.maxLength = Number(this.getAttribute('maxlength') ?? 0);
|
|
70
|
+
|
|
36
71
|
const input = document.createElement('input');
|
|
37
72
|
input.className = 'adw-entry';
|
|
38
73
|
input.type = this.getAttribute('type') || 'text';
|
|
39
|
-
input.value = this.getAttribute('value') ?? '';
|
|
74
|
+
input.value = clampEntryText(this.getAttribute('value') ?? '', this._maxLength);
|
|
40
75
|
input.placeholder = this.getAttribute('placeholder') ?? '';
|
|
41
76
|
input.disabled = this.hasAttribute('disabled');
|
|
77
|
+
// Typing past the limit is clamped here, on the way in — the same place
|
|
78
|
+
// NativeScript clamps it, so both renderers refuse the same character.
|
|
79
|
+
input.addEventListener('input', () => {
|
|
80
|
+
const clamped = clampEntryText(input.value, this._maxLength);
|
|
81
|
+
if (clamped !== input.value) input.value = clamped;
|
|
82
|
+
});
|
|
42
83
|
input.addEventListener('keydown', (e) => {
|
|
43
84
|
if (e.key === 'Enter') {
|
|
44
85
|
this.dispatchEvent(new CustomEvent('activate', { bubbles: true, detail: { value: input.value } }));
|
|
@@ -50,8 +91,12 @@ export class AdwEntry extends HTMLElement {
|
|
|
50
91
|
}
|
|
51
92
|
|
|
52
93
|
attributeChangedCallback(name: string, _old: string | null, value: string | null) {
|
|
94
|
+
if (name === 'maxlength') {
|
|
95
|
+
this.maxLength = Number(value ?? 0);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
53
98
|
if (!this._input) return;
|
|
54
|
-
if (name === 'value') this._input.value = value ?? '';
|
|
99
|
+
if (name === 'value') this._input.value = clampEntryText(value ?? '', this._maxLength);
|
|
55
100
|
else if (name === 'placeholder') this._input.placeholder = value ?? '';
|
|
56
101
|
else if (name === 'type') this._input.type = value || 'text';
|
|
57
102
|
else if (name === 'disabled') this._input.disabled = value !== null;
|
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
// <adw-header-bar> — Adwaita header bar with centered title and start/end button slots.
|
|
2
|
+
//
|
|
3
|
+
// The derived centre is an `<adw-window-title>`, which is what `Adw.HeaderBar`
|
|
4
|
+
// itself puts there (`adw-header-bar.c`: the title widget it creates when none is
|
|
5
|
+
// given IS an `AdwWindowTitle`). It used to be a bare span with
|
|
6
|
+
// `textContent = title ?? ''`, and that span carried none of the three rules the
|
|
7
|
+
// window title already held in `@gjsify/adwaita-core`: an EMPTY title still
|
|
8
|
+
// reserved a blank line, re-setting the same value repainted, and there was no
|
|
9
|
+
// subtitle at all. So a header bar could not show what its own NativeScript twin
|
|
10
|
+
// could, and the fix is delegation rather than a fourth copy of the derivation.
|
|
11
|
+
//
|
|
12
|
+
// Reference: refs/libadwaita/src/adw-header-bar.c
|
|
2
13
|
// Adapted from Adwaita Web UI Framework (https://github.com/mclellac/adwaita-web).
|
|
3
14
|
// Copyright (c) 2025 csm. MIT License.
|
|
4
15
|
// Modifications: Reimplemented as Web Component for @gjsify/adwaita-web.
|
|
5
16
|
|
|
17
|
+
// Registers <adw-window-title>: the derived centre is one, so importing the bar
|
|
18
|
+
// alone must still define it.
|
|
19
|
+
import './adw-window-title.js';
|
|
20
|
+
|
|
6
21
|
export class AdwHeaderBar extends HTMLElement {
|
|
7
22
|
private _initialized = false;
|
|
8
23
|
private _startEl: HTMLDivElement | null = null;
|
|
9
24
|
private _centerEl: HTMLDivElement | null = null;
|
|
10
25
|
private _endEl: HTMLDivElement | null = null;
|
|
11
26
|
/**
|
|
12
|
-
* The
|
|
13
|
-
* took the centre. `Adw.HeaderBar` has the same either/or:
|
|
14
|
-
* `title-widget` replaces the derived `AdwWindowTitle` outright.
|
|
27
|
+
* The derived `<adw-window-title>`, or `null` when a `slot="center"`
|
|
28
|
+
* title-widget took the centre. `Adw.HeaderBar` has the same either/or:
|
|
29
|
+
* setting `title-widget` replaces the derived `AdwWindowTitle` outright.
|
|
15
30
|
*/
|
|
16
|
-
private _titleEl:
|
|
31
|
+
private _titleEl: HTMLElement | null = null;
|
|
17
32
|
|
|
18
33
|
static get observedAttributes() {
|
|
19
|
-
return ['title'];
|
|
34
|
+
return ['title', 'subtitle'];
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
/** The start (left) section container — append buttons/widgets here. */
|
|
@@ -56,7 +71,7 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
56
71
|
if (centerChildren.length > 0) {
|
|
57
72
|
for (const child of centerChildren) this._centerEl.appendChild(child);
|
|
58
73
|
} else {
|
|
59
|
-
this._titleEl = document.createElement('
|
|
74
|
+
this._titleEl = document.createElement('adw-window-title');
|
|
60
75
|
this._titleEl.className = 'adw-header-bar-title';
|
|
61
76
|
this._centerEl.appendChild(this._titleEl);
|
|
62
77
|
}
|
|
@@ -77,15 +92,23 @@ export class AdwHeaderBar extends HTMLElement {
|
|
|
77
92
|
* kept whatever it was created with. `Adw.HeaderBar`'s derived title widget
|
|
78
93
|
* is bound to the property and re-renders on every change.
|
|
79
94
|
*/
|
|
80
|
-
attributeChangedCallback(
|
|
81
|
-
if (this._initialized
|
|
95
|
+
attributeChangedCallback() {
|
|
96
|
+
if (this._initialized) this._renderTitle();
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
private _renderTitle() {
|
|
85
100
|
// A `slot="center"` widget replaced the derived title, so there is
|
|
86
101
|
// nothing for the attribute to write to — the same either/or as
|
|
87
102
|
// `Adw.HeaderBar:title-widget`.
|
|
88
|
-
if (this._titleEl)
|
|
103
|
+
if (!this._titleEl) return;
|
|
104
|
+
// Forwarded as ATTRIBUTES, so the window title's own change detection and
|
|
105
|
+
// empty-string collapse do the work. Removing rather than writing `''`
|
|
106
|
+
// keeps "unset" distinguishable from "set to empty" on the child.
|
|
107
|
+
for (const name of ['title', 'subtitle']) {
|
|
108
|
+
const value = this.getAttribute(name);
|
|
109
|
+
if (value === null) this._titleEl.removeAttribute(name);
|
|
110
|
+
else this._titleEl.setAttribute(name, value);
|
|
111
|
+
}
|
|
89
112
|
}
|
|
90
113
|
}
|
|
91
114
|
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
// Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
|
|
21
21
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
|
|
22
22
|
|
|
23
|
-
import { isSplitButtonDirection, menuButtonPopupDirection } from '@gjsify/adwaita-core';
|
|
24
|
-
import type { SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
23
|
+
import { isSplitButtonDirection, menuButtonPopupDirection, parseMenuEntries } from '@gjsify/adwaita-core';
|
|
24
|
+
import type { AdwMenuEntry, SplitButtonDirection } from '@gjsify/adwaita-core';
|
|
25
25
|
|
|
26
26
|
// SIDE-EFFECT import, deliberately separate from the type import below: it guarantees
|
|
27
27
|
// `adw-popover` is defined before this module's `customElements.define` can upgrade a
|
|
@@ -34,12 +34,15 @@ import type { AdwPopover } from './adw-popover.js';
|
|
|
34
34
|
|
|
35
35
|
import { createAdwIcon } from './adw-icon.js';
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A menu entry — `@gjsify/adwaita-core`'s {@link AdwMenuEntry}, re-exported under the
|
|
39
|
+
* name this element has always used.
|
|
40
|
+
*
|
|
41
|
+
* It used to be a SECOND declaration of the same shape, and the NativeScript menu
|
|
42
|
+
* button declared a THIRD that was missing `icon` entirely. One type, so a consumer
|
|
43
|
+
* writing menu entries writes the same object for either renderer.
|
|
44
|
+
*/
|
|
45
|
+
export type AdwMenuItem = AdwMenuEntry;
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Where the popover sits, per `GtkArrowType`. `menuButtonPopupDirection` folds `none`
|
|
@@ -152,18 +155,16 @@ export class AdwMenuButton extends HTMLElement {
|
|
|
152
155
|
this._render();
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The `menu` attribute, through the core parser the split button already used.
|
|
160
|
+
*
|
|
161
|
+
* The copy this replaces was weaker in a way markup can reach: it kept `id` and
|
|
162
|
+
* `icon` WHATEVER their runtime type, so `{"label":"Open","id":7}` produced an
|
|
163
|
+
* entry whose `id` is a number in a field typed `string` — and `id` is what the
|
|
164
|
+
* activation event reports. The core parser keeps only string-typed extras.
|
|
165
|
+
*/
|
|
155
166
|
private _parseMenuAttr(): AdwMenuItem[] {
|
|
156
|
-
|
|
157
|
-
if (!raw) return [];
|
|
158
|
-
try {
|
|
159
|
-
const parsed: unknown = JSON.parse(raw);
|
|
160
|
-
if (!Array.isArray(parsed)) return [];
|
|
161
|
-
return parsed
|
|
162
|
-
.filter((e): e is AdwMenuItem => typeof e === 'object' && e !== null && 'label' in e)
|
|
163
|
-
.map((e) => ({ id: e.id, label: String(e.label), icon: e.icon }));
|
|
164
|
-
} catch {
|
|
165
|
-
return [];
|
|
166
|
-
}
|
|
167
|
+
return parseMenuEntries(this.getAttribute('menu'));
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
private _onPopoverToggled(open: boolean): void {
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
|
|
8
8
|
// icon node is <adw-icon>.
|
|
9
9
|
|
|
10
|
+
import { stringIsNotEmpty } from '@gjsify/adwaita-core';
|
|
11
|
+
|
|
10
12
|
import { type AdwIcon, createAdwIcon } from './adw-icon.js';
|
|
11
13
|
|
|
12
14
|
export class AdwStatusPage extends HTMLElement {
|
|
@@ -51,13 +53,16 @@ export class AdwStatusPage extends HTMLElement {
|
|
|
51
53
|
this._iconEl.iconName = this.getAttribute('icon');
|
|
52
54
|
this._iconEl.hidden = this._iconEl.resolvedIconName === '';
|
|
53
55
|
|
|
56
|
+
// `string_is_not_empty` from the core, not a local `.length === 0`: the same
|
|
57
|
+
// closure the NativeScript port binds its labels to, and the reason a title of
|
|
58
|
+
// `' '` stays VISIBLE — it reads the first byte, it never trims.
|
|
54
59
|
const title = this.getAttribute('title') ?? '';
|
|
55
60
|
this._titleEl.textContent = title;
|
|
56
|
-
this._titleEl.hidden = title
|
|
61
|
+
this._titleEl.hidden = !stringIsNotEmpty(title);
|
|
57
62
|
|
|
58
63
|
const description = this.getAttribute('description') ?? '';
|
|
59
64
|
this._descEl.textContent = description;
|
|
60
|
-
this._descEl.hidden = description
|
|
65
|
+
this._descEl.hidden = !stringIsNotEmpty(description);
|
|
61
66
|
|
|
62
67
|
this._childEl.hidden = this._childEl.childElementCount === 0;
|
|
63
68
|
}
|
package/src/test.browser.mts
CHANGED
|
@@ -33,6 +33,9 @@ import { AdwViewSwitcherBarTest } from './adw-view-switcher-bar.spec.js';
|
|
|
33
33
|
import { AdwStyleIsolationTest } from './style-isolation.spec.js';
|
|
34
34
|
import { AdwWrapBoxTest } from './adw-wrap-box.spec.js';
|
|
35
35
|
import { AdwHeaderBarTest } from './adw-header-bar.spec.js';
|
|
36
|
+
import { AdwEntryTest } from './adw-entry.spec.js';
|
|
37
|
+
import { AdwMenuButtonTest } from './adw-menu-button.spec.js';
|
|
38
|
+
import { AdwButtonTest } from './adw-button.spec.js';
|
|
36
39
|
import { AdwBannerTest } from './adw-banner.spec.js';
|
|
37
40
|
import { AdwButtonContentTest } from './adw-button-content.spec.js';
|
|
38
41
|
import { AdwIconTest } from './adw-icon.spec.js';
|
|
@@ -77,6 +80,9 @@ run({
|
|
|
77
80
|
AdwStyleIsolationTest,
|
|
78
81
|
AdwWrapBoxTest,
|
|
79
82
|
AdwHeaderBarTest,
|
|
83
|
+
AdwEntryTest,
|
|
84
|
+
AdwMenuButtonTest,
|
|
85
|
+
AdwButtonTest,
|
|
80
86
|
AdwViewStackTest,
|
|
81
87
|
AdwNavigationViewTest,
|
|
82
88
|
AdwSidebarTest,
|