@jack-henry/jh-elements 2.0.0-beta.13 → 2.0.0-beta.15

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 (37) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +2 -2
  3. package/components/badge/badge.js +4 -4
  4. package/components/button/button.js +106 -52
  5. package/components/card/card.js +4 -4
  6. package/components/checkbox/checkbox.js +19 -31
  7. package/components/checkbox-group/checkbox-group.js +12 -20
  8. package/components/divider/divider.js +5 -5
  9. package/components/element/element.js +94 -0
  10. package/components/icon/icon.js +14 -12
  11. package/components/input/input.js +72 -74
  12. package/components/input-email/input-email.js +1 -1
  13. package/components/input-password/input-password.js +10 -19
  14. package/components/input-search/input-search.js +2 -2
  15. package/components/input-telephone/input-telephone.js +1 -1
  16. package/components/input-textarea/input-textarea.js +4 -13
  17. package/components/input-url/input-url.js +1 -1
  18. package/components/list-group/list-group.js +6 -13
  19. package/components/list-item/list-item.js +131 -143
  20. package/components/menu/menu.js +5 -8
  21. package/components/notification/notification.js +19 -21
  22. package/components/progress/progress.js +62 -56
  23. package/components/radio/radio.js +8 -21
  24. package/components/radio-group/radio-group.js +16 -31
  25. package/components/select/select.js +51 -30
  26. package/components/switch/switch.js +9 -22
  27. package/components/table/table.js +21 -29
  28. package/components/table-data-cell/table-data-cell.js +7 -9
  29. package/components/table-header-cell/table-header-cell.js +15 -25
  30. package/components/table-row/table-row.js +5 -8
  31. package/components/tag/tag.js +12 -14
  32. package/components/tag-group/tag-group.js +5 -7
  33. package/components/toast/toast.js +5 -14
  34. package/components/tooltip/tooltip.js +8 -9
  35. package/custom-elements.json +350 -215
  36. package/jsconfig.json +1 -1
  37. package/package.json +11 -15
@@ -2,8 +2,9 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import { LitElement, css, html } from 'lit';
5
+ import { css, html } from 'lit';
6
6
  import { ifDefined } from 'lit/directives/if-defined.js';
7
+ import { JhElement } from '../element/element.js';
7
8
 
8
9
  /**
9
10
  * @cssprop --jh-progress-label-color - The label text color. Defaults to `--jh-color-content-primary-enabled`.
@@ -11,11 +12,11 @@ import { ifDefined } from 'lit/directives/if-defined.js';
11
12
  * @cssprop --jh-progress-track-color - The track color. Defaults to `--jh-color-control-enabled`.
12
13
  * @cssprop --jh-progress-track-border-radius - The track border-radius. Defaults to `--jh-border-radius-50`.
13
14
  * @cssprop --jh-progress-indicator-color - The indicator color. Defaults to `--jh-color-content-brand-enabled`.
15
+ * @cssprop --jh-progress-track-size-linear - The height of the linear progress bar track. Defaults to the size-based height.
16
+ * @cssprop --jh-progress-track-size-circular - The width and height of the circular progress indicator. Defaults to the size-based dimensions.
14
17
  * @customElement jh-progress
15
18
  */
16
- export class JhProgress extends LitElement {
17
- /** @type {ElementInternals} */
18
- #internals;
19
+ export class JhProgress extends JhElement {
19
20
 
20
21
  /** @type {string} */
21
22
  #label;
@@ -87,14 +88,26 @@ export class JhProgress extends LitElement {
87
88
  );
88
89
  overflow: hidden;
89
90
  }
91
+ :host([size='x-small']) .linear-progress-bar {
92
+ --progress-linear-height: var(--jh-dimension-25);
93
+ }
90
94
  :host([size='small']) .linear-progress-bar {
91
- height: var(--jh-dimension-50);
95
+ --progress-linear-height: var(--jh-dimension-50);
92
96
  }
93
97
  :host([size='medium']) .linear-progress-bar {
94
- height: var(--jh-dimension-100);
98
+ --progress-linear-height: var(--jh-dimension-100);
95
99
  }
96
100
  :host([size='large']) .linear-progress-bar {
97
- height: var(--jh-dimension-200);
101
+ --progress-linear-height: var(--jh-dimension-200);
102
+ }
103
+ :host([size='x-large']) .linear-progress-bar {
104
+ --progress-linear-height: var(--jh-dimension-300);
105
+ }
106
+ :host([size='xx-large']) .linear-progress-bar {
107
+ --progress-linear-height: var(--jh-dimension-400);
108
+ }
109
+ :host .linear-progress-bar {
110
+ height: var(--jh-progress-track-size-linear, var(--progress-linear-height));
98
111
  }
99
112
  :host .linear-progress-bar-value {
100
113
  background-color: var(
@@ -110,30 +123,40 @@ export class JhProgress extends LitElement {
110
123
  align-items: center;
111
124
  }
112
125
  :host([type='circular']) .text-content {
113
- margin-top: var(--jh-dimension-200);
126
+ margin-top: var(--jh-dimension-100);
127
+ margin-bottom: 0;
114
128
  display: flex;
115
129
  flex-direction: column;
116
130
  }
117
131
  svg {
118
132
  transform: rotate(-90deg);
119
- width: var(--progress-size);
120
- height: var(--progress-size);
133
+ width: var(--jh-progress-track-size-circular, var(--progress-circular-size));
134
+ height: var(--jh-progress-track-size-circular, var(--progress-circular-size));
121
135
  overflow: visible;
122
136
  }
137
+ :host([size='x-small']) svg {
138
+ --progress-circular-size: var(--jh-dimension-400);
139
+ }
123
140
  :host([size='small']) svg {
124
- --progress-size: var(--jh-dimension-300);
141
+ --progress-circular-size: var(--jh-dimension-500);
125
142
  }
126
143
  :host([size='medium']) svg {
127
- --progress-size: var(--jh-dimension-400);
128
- margin-bottom: 1px;
144
+ --progress-circular-size: var(--jh-dimension-600);
129
145
  }
130
146
  :host([size='large']) svg {
131
- --progress-size: var(--jh-dimension-800);
132
- margin-bottom: 2px;
147
+ --progress-circular-size: var(--jh-dimension-900);
148
+ }
149
+ :host([size='x-large']) svg {
150
+ --progress-circular-size: var(--jh-dimension-1400);
151
+ }
152
+ :host([size='xx-large']) svg {
153
+ --progress-circular-size: var(--jh-dimension-2100);
133
154
  }
155
+ /* circle with virtual sizes for viewBox */
134
156
  circle {
135
157
  transform-origin: 50% 50%;
136
158
  fill: none;
159
+ stroke-width: 1.5;
137
160
  }
138
161
  .circular-progress-bar {
139
162
  stroke: var(--jh-progress-track-color, var(--jh-color-control-enabled));
@@ -145,18 +168,6 @@ export class JhProgress extends LitElement {
145
168
  var(--jh-color-content-brand-enabled)
146
169
  );
147
170
  }
148
- :host([size='small']) circle {
149
- stroke-width: 1;
150
- stroke-dasharray: 37.8px;
151
- }
152
- :host([size='medium']) circle {
153
- stroke-width: 2;
154
- stroke-dasharray: 50.3px;
155
- }
156
- :host([size='large']) circle {
157
- stroke-width: 4;
158
- stroke-dasharray: 100.8px;
159
- }
160
171
  :host([type='circular']) div {
161
172
  text-align: center;
162
173
  }
@@ -202,29 +213,28 @@ export class JhProgress extends LitElement {
202
213
 
203
214
  constructor() {
204
215
  super();
205
- this.#internals = this.attachInternals();
206
216
  /** @type {Boolean} */
207
217
  this.indeterminate = false;
208
218
  /** @type {?string} */
209
219
  this.label = null;
210
- /** @type {'small'|'medium'|'large'} */
220
+ /** @type { 'x-small'|'small'|'medium'|'large'|'x-large'|'xx-large'} */
211
221
  this.size = 'medium';
212
222
  /** @type {'linear'|'circular'} */
213
223
  this.type = 'linear';
214
224
  /** @type {Boolean} */
215
225
  this.hideValue = false;
216
226
  /** @type {?string} */
217
- this.#internals.ariaLabel;
227
+ this.internals.ariaLabel;
218
228
  /** @type {number} */
219
- this.#internals.ariaValueMax;
229
+ this.internals.ariaValueMax;
220
230
  /** @type {number} */
221
- this.#internals.ariaValueMin;
231
+ this.internals.ariaValueMin;
222
232
  /** @type {?number} */
223
- this.#internals.ariaValueNow;
233
+ this.internals.ariaValueNow;
224
234
  /** @type {?string} */
225
- this.#internals.ariaValueText;
235
+ this.internals.ariaValueText;
226
236
  /** @type {?string} */
227
- this.#internals.role = 'progressbar';
237
+ this.internals.role = 'progressbar';
228
238
  /** @type {?string} */
229
239
  this.accessibleLabel = null;
230
240
  /** @type {?number} */
@@ -251,10 +261,10 @@ export class JhProgress extends LitElement {
251
261
 
252
262
  // Shared setter for elementInternals properties
253
263
  #setAttribute(ariaAttribute, newValue) {
254
- const oldValue = this.#internals[ariaAttribute];
264
+ const oldValue = this.internals[ariaAttribute];
255
265
 
256
266
  if (newValue !== oldValue) {
257
- this.#internals[ariaAttribute] = newValue;
267
+ this.internals[ariaAttribute] = newValue;
258
268
  }
259
269
  this.requestUpdate(ariaAttribute, oldValue);
260
270
  }
@@ -275,7 +285,7 @@ export class JhProgress extends LitElement {
275
285
  }
276
286
 
277
287
  get accessibleLabel() {
278
- return this.#internals.ariaLabel;
288
+ return this.internals.ariaLabel;
279
289
  }
280
290
 
281
291
  set accessibleLabel(newValue) {
@@ -283,7 +293,7 @@ export class JhProgress extends LitElement {
283
293
  }
284
294
 
285
295
  get min() {
286
- return this.#internals.ariaValueMin;
296
+ return this.internals.ariaValueMin;
287
297
  }
288
298
 
289
299
  set min(newValue) {
@@ -291,7 +301,7 @@ export class JhProgress extends LitElement {
291
301
  }
292
302
 
293
303
  get max() {
294
- return this.#internals.ariaValueMax;
304
+ return this.internals.ariaValueMax;
295
305
  }
296
306
 
297
307
  set max(newValue) {
@@ -299,7 +309,7 @@ export class JhProgress extends LitElement {
299
309
  }
300
310
 
301
311
  get accessibleValueText() {
302
- return this.#internals.ariaValueText;
312
+ return this.internals.ariaValueText;
303
313
  }
304
314
 
305
315
  set accessibleValueText(newValue) {
@@ -307,14 +317,14 @@ export class JhProgress extends LitElement {
307
317
  }
308
318
 
309
319
  get value() {
310
- return this.#internals.ariaValueNow;
320
+ return this.internals.ariaValueNow;
311
321
  }
312
322
 
313
323
  set value(newValue) {
314
324
  this.#setAttribute('ariaValueNow', newValue);
315
325
  }
316
326
 
317
- #getCircularIndicator(size, indeterminate, percentage) {
327
+ #getCircularIndicator(indeterminate, percentage) {
318
328
  let percentComplete;
319
329
 
320
330
  if (indeterminate) {
@@ -324,26 +334,23 @@ export class JhProgress extends LitElement {
324
334
  } else {
325
335
  percentComplete = percentage;
326
336
  }
327
-
328
- const circle = {
329
- // Numbers represent radius, x&y axis, and circumference
330
- small: [6, 6, 37.8],
331
- medium: [8, 8, 50.3],
332
- large: [16, 16, 100.8],
333
- };
334
-
335
- const [r, axis, c] = circle[size];
337
+ // circle with virtual sizes for viewBox to allow for scaling.
338
+ const axis = 12;
339
+ const viewBoxSize = axis * 2;
340
+ const r = 9.75;
341
+ const c = +(2 * Math.PI * r).toFixed(1);
336
342
 
337
343
  let percentOfCircleFilled = c - (c * Number(percentComplete)) / 100;
338
344
 
339
345
  return html`
340
- <svg>
346
+ <svg viewBox="0 0 ${viewBoxSize} ${viewBoxSize}" xmlns="http://www.w3.org/2000/svg">
341
347
  <circle
342
348
  class="circular-progress-bar"
343
349
  r=${r}
344
350
  cx=${axis}
345
351
  cy=${axis}
346
352
  c=${c}
353
+ style="stroke-dasharray: ${c}"
347
354
  ></circle>
348
355
  <circle
349
356
  class="circular-progress-bar-value"
@@ -351,7 +358,7 @@ export class JhProgress extends LitElement {
351
358
  cx=${axis}
352
359
  cy=${axis}
353
360
  c=${c}
354
- style="stroke-dashoffset:${percentOfCircleFilled}px"
361
+ style="stroke-dasharray: ${c}; stroke-dashoffset: ${percentOfCircleFilled}"
355
362
  ></circle>
356
363
  </svg>
357
364
  `;
@@ -388,7 +395,6 @@ export class JhProgress extends LitElement {
388
395
  indicator = this.#getLinearIndicator(percentage);
389
396
  } else if (this.type === 'circular') {
390
397
  indicator = this.#getCircularIndicator(
391
- this.size,
392
398
  this.indeterminate,
393
399
  percentage
394
400
  );
@@ -414,4 +420,4 @@ export class JhProgress extends LitElement {
414
420
  }
415
421
  }
416
422
 
417
- customElements.define('jh-progress', JhProgress);
423
+ JhProgress.register('jh-progress', JhProgress);
@@ -2,9 +2,8 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import { LitElement, css, html } from 'lit';
6
-
7
- let id = 0;
5
+ import { css, html } from 'lit';
6
+ import { JhElement } from '../element/element.js';
8
7
 
9
8
  /**
10
9
  * @cssprop --jh-radio-opacity-disabled - The radio opacity when disabled. Defaults to `--jh-opacity-disabled`.
@@ -64,12 +63,7 @@ let id = 0;
64
63
  *
65
64
  * @customElement jh-radio */
66
65
 
67
- export class JhRadio extends LitElement {
68
-
69
- /** @type {?Number} */
70
- #id;
71
- /** @type {ElementInternals} */
72
- #internals;
66
+ export class JhRadio extends JhElement {
73
67
 
74
68
  static get styles() {
75
69
  return css`
@@ -321,8 +315,7 @@ export class JhRadio extends LitElement {
321
315
 
322
316
  constructor() {
323
317
  super();
324
- this.#internals = this.attachInternals();
325
- this.#internals.role = 'radio';
318
+ this.internals.role = 'radio';
326
319
  /** @type {?string} */
327
320
  this.accessibleLabel = null;
328
321
  /** @type {?boolean} */
@@ -342,14 +335,13 @@ export class JhRadio extends LitElement {
342
335
 
343
336
  connectedCallback() {
344
337
  super.connectedCallback();
345
- this.#id = id++;
346
338
  this.setAttribute('tabindex', '0');
347
339
  this.setAttribute('aria-checked', `${this.checked}`);
348
340
  if (this.accessibleLabel) {
349
341
  this.setAttribute('aria-label', `${this.accessibleLabel}`);
350
342
  }
351
343
  if (this.helperText) {
352
- this.setAttribute('aria-describedby', `radio-helper-text-${this.#id}`);
344
+ this.setAttribute('aria-describedby', `radio-helper-text-${this.uniqueId}`);
353
345
  }
354
346
 
355
347
  let observer = new MutationObserver(this.#updateArias.bind(this));
@@ -378,12 +370,7 @@ export class JhRadio extends LitElement {
378
370
  if (this.checked) return;
379
371
 
380
372
  this.checked = true;
381
- const options = {
382
- bubbles: true,
383
- composed: true,
384
- cancelable: true,
385
- };
386
- this.dispatchEvent(new CustomEvent('jh-change', options));
373
+ this.dispatchCustomEvent('jh-change');
387
374
  }
388
375
 
389
376
  #handleKeydown(e) {
@@ -398,7 +385,7 @@ export class JhRadio extends LitElement {
398
385
 
399
386
  if (this.helperText) {
400
387
  helperText = html`
401
- <p class="helper-text" id="radio-helper-text-${this.#id}">
388
+ <p class="helper-text" id="radio-helper-text-${this.uniqueId}">
402
389
  ${this.helperText}
403
390
  </p>
404
391
  `;
@@ -419,4 +406,4 @@ export class JhRadio extends LitElement {
419
406
  `;
420
407
  }
421
408
  }
422
- customElements.define('jh-radio', JhRadio);
409
+ JhRadio.register('jh-radio', JhRadio);
@@ -2,10 +2,10 @@
2
2
  //
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
 
5
- import { LitElement, css, html } from 'lit';
5
+ import { css, html } from 'lit';
6
6
  import { ifDefined } from 'lit/directives/if-defined.js';
7
+ import { JhElement } from '../element/element.js';
7
8
 
8
- let id = 0;
9
9
  /**
10
10
  *
11
11
  * @cssprop --jh-radio-group-label-color-text - The label text color. Defaults to `--jh-color-content-primary-enabled`.
@@ -21,19 +21,15 @@ let id = 0;
21
21
  *
22
22
  * @slot default - Use to insert `<jh-radio>` components(s).
23
23
  *
24
- * @event jh-change - Dispatched when the value of the radio group has changed.
24
+ * @event jh-change - Dispatched when the value of the radio group has changed. Event payload includes the `value` and can be accessed via `e.detail.state.value`.
25
25
  *
26
26
  * @customElement jh-radio-group
27
27
  */
28
- export class JhRadioGroup extends LitElement {
28
+ export class JhRadioGroup extends JhElement {
29
29
  static get formAssociated() {
30
30
  return true;
31
31
  }
32
32
  #checked;
33
- /** @type {?Number} */
34
- #id;
35
- /** @type {ElementInternals} */
36
- #internals;
37
33
  /** @type {?string} */
38
34
  #value;
39
35
 
@@ -208,7 +204,6 @@ export class JhRadioGroup extends LitElement {
208
204
  }
209
205
  constructor() {
210
206
  super();
211
- this.#internals = this.attachInternals();
212
207
  /** @type {?string} */
213
208
  this.accessibleLabel = null;
214
209
  /** @type {boolean} */
@@ -237,11 +232,6 @@ export class JhRadioGroup extends LitElement {
237
232
  this.addEventListener('focusout', this.#handleFocusOut);
238
233
  }
239
234
 
240
- connectedCallback() {
241
- super.connectedCallback();
242
- this.#id = id++;
243
- }
244
-
245
235
  firstUpdated() {
246
236
  this._syncDisabledToChildren();
247
237
  }
@@ -257,12 +247,12 @@ export class JhRadioGroup extends LitElement {
257
247
  * @type {?HTMLFormElement}
258
248
  */
259
249
  get form() {
260
- return this.#internals.form;
250
+ return this.internals.form;
261
251
  }
262
252
 
263
253
  /** @ignore */
264
254
  get validity() {
265
- return this.#internals.validity;
255
+ return this.internals.validity;
266
256
  }
267
257
 
268
258
  /** @type {?string} */
@@ -274,7 +264,7 @@ export class JhRadioGroup extends LitElement {
274
264
  const oldValue = this.#value;
275
265
  if (newValue !== oldValue) {
276
266
  this.#value = newValue;
277
- this.#internals.setFormValue(newValue);
267
+ this.internals.setFormValue(newValue);
278
268
  }
279
269
  this.requestUpdate('value', oldValue);
280
270
  }
@@ -322,12 +312,7 @@ export class JhRadioGroup extends LitElement {
322
312
  this.#checked = e.target;
323
313
  this.#updateChecked();
324
314
 
325
- const options = {
326
- bubbles: true,
327
- composed: true,
328
- cancelable: true,
329
- };
330
- this.dispatchEvent(new CustomEvent('jh-change', options));
315
+ this.dispatchCustomEvent('jh-change');
331
316
  }
332
317
  }
333
318
 
@@ -390,11 +375,11 @@ export class JhRadioGroup extends LitElement {
390
375
 
391
376
  #getAriaDescribedBy() {
392
377
  if (this.errorText && this.invalid && this.helperText && this.label) {
393
- return `radio-group-error-${this.#id} radio-group-helper-${this.#id}`;
378
+ return `radio-group-error-${this.uniqueId} radio-group-helper-${this.uniqueId}`;
394
379
  } else if (this.errorText && this.invalid) {
395
- return `radio-group-error-${this.#id}`;
380
+ return `radio-group-error-${this.uniqueId}`;
396
381
  } else if (this.helperText && this.label) {
397
- return `radio-group-helper-${this.#id}`;
382
+ return `radio-group-helper-${this.uniqueId}`;
398
383
  }
399
384
  }
400
385
 
@@ -415,28 +400,28 @@ export class JhRadioGroup extends LitElement {
415
400
  if (this.helperText) {
416
401
  helperText = html`<p
417
402
  class="helper-text"
418
- id="radio-group-helper-${this.#id}"
403
+ id="radio-group-helper-${this.uniqueId}"
419
404
  >
420
405
  ${this.helperText}
421
406
  </p>`;
422
407
  }
423
408
 
424
409
  if (this.label) {
425
- label = html`<legend class="label" for="radio-group-label-${this.#id}">
410
+ label = html`<legend class="label" for="radio-group-label-${this.uniqueId}">
426
411
  ${this.label}${indicator}
427
412
  </legend>
428
413
  ${helperText}`;
429
414
  }
430
415
 
431
416
  if (this.invalid && this.errorText) {
432
- errorText = html`<p class="error-text" id="radio-group-error-${this.#id}">
417
+ errorText = html`<p class="error-text" id="radio-group-error-${this.uniqueId}">
433
418
  ${this.errorText}
434
419
  </p>`;
435
420
  }
436
421
  return html`
437
422
  <fieldset
438
423
  role="radiogroup"
439
- id=${ifDefined(this.label ? `radio-group-label-${this.#id}` : null)}
424
+ id=${ifDefined(this.label ? `radio-group-label-${this.uniqueId}` : null)}
440
425
  aria-describedby=${ifDefined(this.#getAriaDescribedBy())}
441
426
  aria-disabled=${ifDefined(this.disabled ? 'true' : null)}
442
427
  aria-required=${ifDefined(this.required ? 'true' : 'false')}
@@ -452,4 +437,4 @@ export class JhRadioGroup extends LitElement {
452
437
  `;
453
438
  }
454
439
  }
455
- customElements.define('jh-radio-group', JhRadioGroup);
440
+ JhRadioGroup.register('jh-radio-group', JhRadioGroup);
@@ -60,7 +60,7 @@ import { JhFilter } from './filtering.js';
60
60
  * @slot jh-select-trigger-open - Use to replace the default chevron icon displayed when the select menu is open.
61
61
  * @slot jh-select-trigger-closed - Use to replace the default chevron icon displayed when the select menu is closed.
62
62
  *
63
- * @event jh-change - Dispatched when the selected value changes.
63
+ * @event jh-change - Dispatched when the selected value changes. Event payload includes the `value` and can be accessed via `e.detail.state.value`.
64
64
  */
65
65
  export class JhSelect extends JhInput {
66
66
  /** @type {?string} */
@@ -243,17 +243,37 @@ export class JhSelect extends JhInput {
243
243
  ...item,
244
244
  };
245
245
  });
246
- // These will be the same until we add search functionality, at which point #flatOptions will be the filtered list and #allOptions will remain the source of truth.
247
246
  this.#flatOptions = this.#allOptions;
248
247
  this.#activeIndex = null;
249
248
 
250
- // Set initial value from the selected flag in the data array
251
- const selectedOption = this.#flatOptions.find((opt) => opt.selected);
252
- if (selectedOption && !this.value) {
253
- this.value = String(selectedOption.value);
254
- this.#displayValue = selectedOption.label;
249
+ // If no value is set yet, use the selected flag as the initial default
250
+ if (this.value == null) {
251
+ const selectedOption = this.#flatOptions.find((opt) => opt.selected);
252
+ if (selectedOption) {
253
+ this.value = String(selectedOption.value);
254
+ this.#displayValue = selectedOption.label;
255
+ }
256
+ } else {
257
+ // Value already set — resolve display label from new options
258
+ this.#syncDisplayValue();
255
259
  }
256
260
  }
261
+
262
+ // Handle external value changes (e.g. .value = 'x' or value="x" attribute)
263
+ if (changedProperties.has('value') && !changedProperties.has('options')) {
264
+ this.#syncDisplayValue();
265
+ }
266
+ }
267
+
268
+ #syncDisplayValue() {
269
+ if (this.value == null) {
270
+ this.#displayValue = null;
271
+ return;
272
+ }
273
+ const match = this.#flatOptions.find(
274
+ (opt) => String(opt.value) === String(this.value)
275
+ );
276
+ this.#displayValue = match ? (match.label ?? String(match.value)) : null;
257
277
  }
258
278
 
259
279
  #getIndexFromId(elementId) {
@@ -299,7 +319,7 @@ export class JhSelect extends JhInput {
299
319
  }
300
320
  }
301
321
 
302
- #handleOpenSelect() {
322
+ #handleOpenSelect({ keyboard = false } = {}) {
303
323
  if (this.disabled || this.readonly || !this.#flatOptions.length) return;
304
324
  if (!this.#inputWrapper || !this.#menuContainer) return;
305
325
 
@@ -310,8 +330,8 @@ export class JhSelect extends JhInput {
310
330
  requestAnimationFrame(() => {
311
331
  document.addEventListener('scroll', this.#boundDocumentScroll, true);
312
332
  });
313
- // Set initial active to selected item or first item
314
- if (this.#activeIndex === null) {
333
+ // Only set active item on keyboard open to avoid showing focus ring on mouse open
334
+ if (keyboard && this.#activeIndex === null) {
315
335
  const selectedIdx = this.#flatOptions.findIndex(
316
336
  (opt) => String(opt.value) === String(this.value));
317
337
  this.#setActiveItem(selectedIdx !== -1 ? selectedIdx : 0);
@@ -360,22 +380,26 @@ export class JhSelect extends JhInput {
360
380
  if (e.ctrlKey || e.metaKey) return;
361
381
 
362
382
  switch (e.key) {
363
- case 'ArrowDown':
364
- e.preventDefault();
365
- if (!this.#open) {
366
- this.#handleOpenSelect();
367
- } else {
368
- this.#setActiveItem(this.#activeIndex === null ? 0 : this.#activeIndex + 1);
369
- }
370
- return;
383
+ case 'ArrowDown':
384
+ e.preventDefault();
385
+ if (!this.#open) {
386
+ this.#handleOpenSelect({ keyboard: true });
387
+ } else {
388
+ const selectedIdx = this.#flatOptions.findIndex(
389
+ (opt) => String(opt.value) === String(this.value));
390
+ this.#setActiveItem((this.#activeIndex ?? selectedIdx) + 1);
391
+ }
392
+ return;
371
393
 
372
394
  case 'ArrowUp':
373
395
  e.preventDefault();
374
396
  if (!this.#open) {
375
- this.#handleOpenSelect();
397
+ this.#handleOpenSelect({ keyboard: true });
376
398
  } else {
399
+ const selectedIdx = this.#flatOptions.findIndex(
400
+ (opt) => String(opt.value) === String(this.value));
377
401
  this.#setActiveItem(
378
- this.#activeIndex === null ? this.#flatOptions.length - 1 : this.#activeIndex - 1);
402
+ (this.#activeIndex ?? (selectedIdx !== -1 ? selectedIdx : this.#flatOptions.length)) - 1);
379
403
  }
380
404
  return;
381
405
 
@@ -383,11 +407,13 @@ export class JhSelect extends JhInput {
383
407
  case ' ':
384
408
  e.preventDefault();
385
409
  if (!this.#open) {
386
- this.#handleOpenSelect();
387
- } else if (this.#activeIndex !== null) {
410
+ this.#handleOpenSelect({ keyboard: true });
411
+ } else if (this.#activeIndex !== null) {
388
412
  this.#handleSelection(this.#activeIndex);
389
413
  this.#handleCloseSelect();
390
- }
414
+ } else {
415
+ this.#handleCloseSelect();
416
+ }
391
417
  return;
392
418
 
393
419
  case 'Escape':
@@ -454,12 +480,7 @@ export class JhSelect extends JhInput {
454
480
  this.#scrollToActiveItem();
455
481
 
456
482
  //dispatch a jh-change event when the selected value changes.
457
- const options = {
458
- bubbles: true,
459
- composed: true,
460
- cancelable: true,
461
- };
462
- this.dispatchEvent(new CustomEvent('jh-change', options));
483
+ this.dispatchCustomEvent('jh-change');
463
484
  }
464
485
  }
465
486
 
@@ -657,4 +678,4 @@ export class JhSelect extends JhInput {
657
678
  }
658
679
  }
659
680
 
660
- customElements.define('jh-select', JhSelect);
681
+ JhSelect.register('jh-select', JhSelect);