@gjsify/adwaita-web 0.16.3 → 0.16.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.16.3",
3
+ "version": "0.16.4",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -41,13 +41,13 @@
41
41
  "@codemirror/language": "^6.12.4",
42
42
  "@codemirror/state": "^6.7.0",
43
43
  "@codemirror/view": "^6.43.4",
44
- "@gjsify/adwaita-fonts": "^0.16.3",
45
- "@gjsify/adwaita-icons": "^0.16.3",
44
+ "@gjsify/adwaita-fonts": "^0.16.4",
45
+ "@gjsify/adwaita-icons": "^0.16.4",
46
46
  "@lezer/highlight": "^1.2.3"
47
47
  },
48
48
  "devDependencies": {
49
- "@gjsify/cli": "^0.16.3",
50
- "@gjsify/unit": "^0.16.3",
49
+ "@gjsify/cli": "^0.16.4",
50
+ "@gjsify/unit": "^0.16.4",
51
51
  "sass": "^1.101.0",
52
52
  "typescript": "^6.0.3"
53
53
  },
@@ -102,21 +102,26 @@ export const AdwDropDownTest = async () => {
102
102
  });
103
103
 
104
104
  await describe('adw-drop-down notify-on-change', async () => {
105
- await it('fires notify::selected + change on a real change only', async () => {
105
+ await it('a programmatic set fires notify::selected but NOT change (DOM <select> semantics)', async () => {
106
106
  const dd = makeDropDown();
107
107
  dd.options = ['a', 'b', 'c'];
108
108
  let notifyCount = 0;
109
- let lastChange: unknown = null;
109
+ let changeCount = 0;
110
110
  dd.addEventListener('notify::selected', () => notifyCount++);
111
- dd.addEventListener('change', (e) => {
112
- lastChange = (e as CustomEvent).detail;
113
- });
114
- dd.selected = 2;
115
- expect(notifyCount).toBe(1);
116
- expect(lastChange).toStrictEqual({ index: 2, value: 'c', label: 'c' });
117
- // Re-selecting the same index must NOT fire again.
111
+ dd.addEventListener('change', () => changeCount++);
112
+ // Programmatic `.selected`: notify::selected fires (GObject property-notify), but the
113
+ // user-facing DOM `change` must NOT — native `select.value = x` fires no `change` either.
118
114
  dd.selected = 2;
119
115
  expect(notifyCount).toBe(1);
116
+ expect(changeCount).toBe(0);
117
+ // Programmatic `.selectedValue` is likewise silent on `change`.
118
+ dd.selectedValue = 'a';
119
+ expect(notifyCount).toBe(2);
120
+ expect(changeCount).toBe(0);
121
+ // Re-selecting the same value fires neither event again.
122
+ dd.selectedValue = 'a';
123
+ expect(notifyCount).toBe(2);
124
+ expect(changeCount).toBe(0);
120
125
  dd.remove();
121
126
  });
122
127
 
@@ -190,7 +190,7 @@ export class AdwDropDown extends HTMLElement {
190
190
  this._renderAll();
191
191
  } else if (name === 'selected') {
192
192
  const index = Number.parseInt(value ?? '', 10);
193
- if (!Number.isNaN(index)) this._selectIndex(index);
193
+ if (!Number.isNaN(index)) this._selectIndex(index, { fromAttr: true });
194
194
  } else if (name === 'enable-search') {
195
195
  this._renderPopover();
196
196
  } else if (name === 'disabled') {
@@ -212,26 +212,33 @@ export class AdwDropDown extends HTMLElement {
212
212
  }
213
213
  }
214
214
 
215
- private _selectIndex(index: number, fromAttr = false): void {
215
+ private _selectIndex(index: number, opts: { fromAttr?: boolean; fromUser?: boolean } = {}): void {
216
216
  if (!Number.isFinite(index) || index < 0 || index >= this._options.length) return;
217
217
  const changed = index !== this._selected;
218
218
  this._selected = index;
219
219
  this._updateLabel();
220
220
  this._updateSelectedStates();
221
221
  // Keep the attribute in sync without re-entering the guarded callback.
222
- if (!fromAttr && this.getAttribute('selected') !== String(index)) {
222
+ if (!opts.fromAttr && this.getAttribute('selected') !== String(index)) {
223
223
  this.setAttribute('selected', String(index));
224
224
  }
225
- // Notify only on an actual change (GObject notify semantics).
226
225
  if (changed) {
227
226
  const option = this._options[index];
227
+ // `notify::selected` mirrors GObject property-notify: it fires on EVERY change,
228
+ // programmatic included.
228
229
  this.dispatchEvent(new CustomEvent('notify::selected', { bubbles: true, detail: { selected: index } }));
229
- this.dispatchEvent(
230
- new CustomEvent('change', {
231
- bubbles: true,
232
- detail: { index, value: option?.value ?? '', label: option?.label ?? '' },
233
- }),
234
- );
230
+ // `change` mirrors the DOM <select> contract: it fires ONLY on a user-initiated change.
231
+ // A programmatic `.selected`/`.selectedValue`/`selected`-attribute assignment stays silent
232
+ // (just as native `select.value = x` fires no `change`), so consumers can set the value
233
+ // without guarding against a spurious user-style event.
234
+ if (opts.fromUser) {
235
+ this.dispatchEvent(
236
+ new CustomEvent('change', {
237
+ bubbles: true,
238
+ detail: { index, value: option?.value ?? '', label: option?.label ?? '' },
239
+ }),
240
+ );
241
+ }
235
242
  }
236
243
  }
237
244
 
@@ -302,7 +309,7 @@ export class AdwDropDown extends HTMLElement {
302
309
  const match = this._options.findIndex((o) => o.label.toLowerCase().startsWith(this._typeAheadBuffer));
303
310
  if (match < 0) return;
304
311
  if (this._active) this._itemButtons[match]?.focus();
305
- else this._selectIndex(match);
312
+ else this._selectIndex(match, { fromUser: true });
306
313
  }
307
314
 
308
315
  /** Filter the visible items by a search query (case-insensitive substring). */
@@ -378,7 +385,7 @@ export class AdwDropDown extends HTMLElement {
378
385
 
379
386
  item.append(label, check);
380
387
  item.addEventListener('click', () => {
381
- this._selectIndex(index);
388
+ this._selectIndex(index, { fromUser: true });
382
389
  this._setActive(false);
383
390
  this._buttonEl.focus();
384
391
  });