@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.154 → 2.0.0-next.156

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 (95) hide show
  1. package/LICENSE-AGPL.txt +661 -0
  2. package/LICENSE-APACHE.txt +177 -0
  3. package/LICENSE.txt +35 -0
  4. package/bundle/openbridge-webcomponents.bundle.js +3629 -3281
  5. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  6. package/custom-elements.json +236 -37
  7. package/dist/automation/transmitter/transmitter-shared.d.ts +11 -0
  8. package/dist/automation/transmitter/transmitter-shared.d.ts.map +1 -0
  9. package/dist/automation/transmitter/transmitter-shared.js +16 -0
  10. package/dist/automation/transmitter/transmitter-shared.js.map +1 -0
  11. package/dist/automation/transmitter/transmitter.css.js +12 -1
  12. package/dist/automation/transmitter/transmitter.css.js.map +1 -1
  13. package/dist/automation/transmitter/transmitter.d.ts +9 -10
  14. package/dist/automation/transmitter/transmitter.d.ts.map +1 -1
  15. package/dist/automation/transmitter/transmitter.js +12 -15
  16. package/dist/automation/transmitter/transmitter.js.map +1 -1
  17. package/dist/automation/transmitter-button/transmitter-button.css.js +18 -42
  18. package/dist/automation/transmitter-button/transmitter-button.css.js.map +1 -1
  19. package/dist/automation/transmitter-button/transmitter-button.d.ts +31 -21
  20. package/dist/automation/transmitter-button/transmitter-button.d.ts.map +1 -1
  21. package/dist/automation/transmitter-button/transmitter-button.js +37 -39
  22. package/dist/automation/transmitter-button/transmitter-button.js.map +1 -1
  23. package/dist/automation/transmitter-stack/transmitter-stack.css.js +144 -0
  24. package/dist/automation/transmitter-stack/transmitter-stack.css.js.map +1 -0
  25. package/dist/automation/transmitter-stack/transmitter-stack.d.ts +93 -0
  26. package/dist/automation/transmitter-stack/transmitter-stack.d.ts.map +1 -0
  27. package/dist/automation/transmitter-stack/transmitter-stack.js +97 -0
  28. package/dist/automation/transmitter-stack/transmitter-stack.js.map +1 -0
  29. package/dist/building-blocks/readout-block/readout-block.d.ts +1 -1
  30. package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
  31. package/dist/components/textarea-field/textarea-field.css.js +0 -2
  32. package/dist/components/textarea-field/textarea-field.css.js.map +1 -1
  33. package/dist/components/textbox/textbox.d.ts +1 -1
  34. package/dist/components/textbox/textbox.js.map +1 -1
  35. package/dist/components/user-button/user-button.css.js +68 -1
  36. package/dist/components/user-button/user-button.css.js.map +1 -1
  37. package/dist/components/user-button/user-button.d.ts +8 -1
  38. package/dist/components/user-button/user-button.d.ts.map +1 -1
  39. package/dist/components/user-button/user-button.js +12 -3
  40. package/dist/components/user-button/user-button.js.map +1 -1
  41. package/dist/components/user-menu/user-menu.css.js +15 -0
  42. package/dist/components/user-menu/user-menu.css.js.map +1 -1
  43. package/dist/components/user-menu/user-menu.d.ts +22 -2
  44. package/dist/components/user-menu/user-menu.d.ts.map +1 -1
  45. package/dist/components/user-menu/user-menu.js +34 -2
  46. package/dist/components/user-menu/user-menu.js.map +1 -1
  47. package/dist/generated/locales/es-419.d.ts +1 -0
  48. package/dist/generated/locales/es-419.d.ts.map +1 -1
  49. package/dist/generated/locales/es-419.js +1 -0
  50. package/dist/generated/locales/es-419.js.map +1 -1
  51. package/dist/generated/locales/fi-FI.d.ts +1 -0
  52. package/dist/generated/locales/fi-FI.d.ts.map +1 -1
  53. package/dist/generated/locales/fi-FI.js +1 -0
  54. package/dist/generated/locales/fi-FI.js.map +1 -1
  55. package/dist/navigation-instruments/readout/readout.d.ts +1 -1
  56. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  57. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +1 -0
  58. package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js.map +1 -1
  59. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +1 -1
  60. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  61. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +3 -0
  62. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  63. package/package.json +6 -3
  64. package/script/prepare-full-bundle.ts +5 -0
  65. package/src/automation/transmitter/transmitter-shared.ts +16 -0
  66. package/src/automation/transmitter/transmitter.css +3 -71
  67. package/src/automation/transmitter/transmitter.stories.ts +13 -1
  68. package/src/automation/transmitter/transmitter.ts +19 -16
  69. package/src/automation/transmitter-button/transmitter-button.css +11 -36
  70. package/src/automation/transmitter-button/transmitter-button.stories.ts +3 -1
  71. package/src/automation/transmitter-button/transmitter-button.ts +62 -66
  72. package/src/automation/transmitter-stack/transmitter-stack.css +56 -0
  73. package/src/automation/transmitter-stack/transmitter-stack.spec.ts +210 -0
  74. package/src/automation/transmitter-stack/transmitter-stack.stories.ts +149 -0
  75. package/src/automation/transmitter-stack/transmitter-stack.ts +172 -0
  76. package/src/building-blocks/readout-block/readout-block.stories.ts +1 -1
  77. package/src/building-blocks/readout-block/readout-block.ts +1 -1
  78. package/src/components/textarea-field/textarea-field.css +1 -8
  79. package/src/components/textbox/textbox.stories.ts +1 -1
  80. package/src/components/textbox/textbox.ts +1 -1
  81. package/src/components/user-button/user-button.css +27 -1
  82. package/src/components/user-button/user-button.stories.ts +29 -0
  83. package/src/components/user-button/user-button.ts +22 -3
  84. package/src/components/user-menu/user-menu.css +8 -0
  85. package/src/components/user-menu/user-menu.stories.ts +53 -0
  86. package/src/components/user-menu/user-menu.ts +55 -4
  87. package/src/generated/locales/es-419.ts +1 -0
  88. package/src/generated/locales/fi-FI.ts +1 -0
  89. package/src/mixins/transmitter.css +90 -0
  90. package/src/mixins/visually-hidden.css +10 -0
  91. package/src/navigation-instruments/readout/readout.stories.ts +1 -1
  92. package/src/navigation-instruments/readout/readout.ts +1 -1
  93. package/src/navigation-instruments/readout-list-item/readout-list-item.css +1 -0
  94. package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +1 -1
  95. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +12 -2
@@ -11,7 +11,7 @@
11
11
  display: inline-flex;
12
12
  align-items: center;
13
13
  justify-content: center;
14
- width: fit-content;
14
+ width: 100%; /* follows the host: hugs by default, fills a stretched grid cell */
15
15
  padding: 0;
16
16
  appearance: none;
17
17
  background: none;
@@ -21,52 +21,23 @@
21
21
  @mixin style style=normal visibleWrapperClass=.visible-wrapper;
22
22
  }
23
23
 
24
+ /* Figma draws the 1 px stroke inside its 4 / 2 px padding, so the padding
25
+ gives the border back and the chip stays 24 px high. */
24
26
  .visible-wrapper {
25
27
  display: inline-flex;
26
28
  align-items: center;
27
- gap: 4px;
28
- padding: 2px 4px;
29
+ padding: 1px 3px;
29
30
  border: 1px solid var(--normal-enabled-border-color);
30
31
  border-radius: 4px;
31
32
  }
32
33
 
33
- .value-container {
34
- display: inline-flex;
35
- align-items: center;
36
- gap: 2px;
37
- padding: 0 2px;
38
- }
39
-
40
- .icon {
41
- display: inline-flex;
42
- align-items: center;
43
- justify-content: center;
44
- }
45
-
46
- .unit {
47
- @mixin font-instrument-unit;
48
- align-self: flex-end;
49
- color: var(--element-neutral-color);
50
- }
51
-
52
- .size-regular .icon ::slotted(*) {
53
- width: 16px;
54
- height: 16px;
55
- }
56
-
57
- .size-medium .icon ::slotted(*) {
58
- width: 20px;
59
- height: 20px;
60
- }
61
-
62
- .size-large .icon ::slotted(*) {
63
- width: 24px;
64
- height: 24px;
34
+ obc-readout-list-item {
35
+ width: auto; /* the list item fills a row by default; hugging lets a stretched chip centre the reading */
65
36
  }
66
37
 
67
38
  .size-medium .visible-wrapper:not(.tag),
68
39
  .size-large .visible-wrapper:not(.tag) {
69
- padding: 4px 8px;
40
+ padding: 3px 7px;
70
41
  border-radius: 6px;
71
42
  }
72
43
 
@@ -81,3 +52,7 @@
81
52
  @mixin font-instrument-value-regular-neutral;
82
53
  color: var(--element-neutral-color);
83
54
  }
55
+
56
+ .visually-hidden {
57
+ @mixin visually-hidden;
58
+ }
@@ -16,11 +16,12 @@ const meta: Meta<typeof ObcTransmitterButton> = {
16
16
  variant: TransmitterButtonVariant.value,
17
17
  size: TransmitterButtonSize.regular,
18
18
  value: 12.3,
19
- unit: '°C',
19
+ unit: 'C',
20
20
  fractionDigits: 1,
21
21
  maxDigits: 0,
22
22
  hintedZeros: false,
23
23
  hasSignSpacer: false,
24
+ hasDegree: true,
24
25
  hasIcon: false,
25
26
  hasAdvice: false,
26
27
  adviceValue: 123,
@@ -81,6 +82,7 @@ function renderComponent(args: ObcTransmitterButton) {
81
82
  .maxDigits=${args.maxDigits}
82
83
  .hintedZeros=${args.hintedZeros}
83
84
  .hasSignSpacer=${args.hasSignSpacer}
85
+ .hasDegree=${args.hasDegree}
84
86
  .hasIcon=${args.hasIcon}
85
87
  .hasAdvice=${args.hasAdvice}
86
88
  .adviceValue=${args.adviceValue}
@@ -3,11 +3,8 @@ import {property} from 'lit/decorators.js';
3
3
  import {classMap} from 'lit/directives/class-map.js';
4
4
  import componentStyle from './transmitter-button.css?inline';
5
5
  import {customElement} from '../../decorator.js';
6
- import '../../building-blocks/readout-block/readout-block.js';
7
- import {
8
- ReadoutBlockSize,
9
- ReadoutBlockVariant,
10
- } from '../../building-blocks/readout-block/readout-block.js';
6
+ import '../../navigation-instruments/readout-list-item/readout-list-item.js';
7
+ import {ReadoutListItemSize} from '../../navigation-instruments/readout-list-item/readout-list-item.js';
11
8
 
12
9
  export enum TransmitterButtonVariant {
13
10
  value = 'value',
@@ -20,31 +17,31 @@ export enum TransmitterButtonSize {
20
17
  large = 'large',
21
18
  }
22
19
 
23
- const readoutSizeBySize: Record<TransmitterButtonSize, ReadoutBlockSize> = {
24
- [TransmitterButtonSize.regular]: ReadoutBlockSize.small,
25
- [TransmitterButtonSize.medium]: ReadoutBlockSize.medium,
26
- [TransmitterButtonSize.large]: ReadoutBlockSize.large,
20
+ const readoutSizeBySize: Record<TransmitterButtonSize, ReadoutListItemSize> = {
21
+ [TransmitterButtonSize.regular]: ReadoutListItemSize.small,
22
+ [TransmitterButtonSize.medium]: ReadoutListItemSize.medium,
23
+ [TransmitterButtonSize.large]: ReadoutListItemSize.large,
27
24
  };
28
25
 
29
26
  /**
30
- * `<obc-transmitter-button>` – The pressable readout chip used as the core of a
31
- * transmitter on a process diagram.
27
+ * `<obc-transmitter-button>` – The pressable readout chip at the core of a
28
+ * transmitter: a bordered button around one reading.
32
29
  *
33
- * The value content (leading icon, optional advice and setpoint segments,
34
- * value, unit) is laid out inline. The value, advice and setpoint segments all
35
- * share one numeric format, so they support fixed-width values and muted
36
- * leading zeros identically. The leading icon is slotted so the consumer
37
- * provides the type-specific content.
30
+ * The reading is a label-less `<obc-readout-list-item>` (the Figma "automation
31
+ * value"): leading icon, optional advice and setpoint, value, optional degree
32
+ * and unit, all rendered by the readout family. The leading icon is slotted so
33
+ * the consumer provides the type-specific content.
38
34
  *
39
35
  * ### Features / Variants
40
- * - **`value`** – white, bordered box showing an icon, value and unit. Opt into a
41
- * leading advice segment with `hasAdvice`/`adviceValue` and a setpoint segment
42
- * with `hasSetPoint`/`setpointValue`.
36
+ * - **`value`** – bordered chip showing an icon, value and unit. Opt into a
37
+ * leading advice segment with `hasAdvice`/`adviceValue`, a setpoint segment
38
+ * with `hasSetPoint`/`setpointValue`, and a degree column before the unit
39
+ * with `hasDegree`.
43
40
  * - **`tag`** – a static rounded pill showing a short identifier (e.g. `TT`)
44
41
  * from the `label` property, with no live value.
45
- * - **`size`** – `regular`, `medium` or `large`, scaling the value text, the icon
46
- * glyph and the advice/setpoint segments. The unit stays at a fixed size across
47
- * all sizes.
42
+ * - **`size`** – `regular`, `medium` or `large`, the list item's `small`,
43
+ * `medium` and `large` tiers: the value text, the icon and the advice/setpoint
44
+ * segments scale, the unit stays extra small.
48
45
  * - **Formatting** – `fractionDigits` sets the decimal precision, `maxDigits`
49
46
  * reserves a number of integer digits, and `hintedZeros` renders the reserved
50
47
  * leading positions as muted zeros (e.g. `0012.3`). The sign never consumes
@@ -57,9 +54,15 @@ const readoutSizeBySize: Record<TransmitterButtonSize, ReadoutBlockSize> = {
57
54
  * dashes fill the whole reserved width (e.g. `---.-`).
58
55
  *
59
56
  * ### Usage Guidelines
60
- * Use as a building block for `<obc-transmitter>`; it is the part that carries
61
- * the measured value or the tag identifier. Slot in the type-specific icon; the
62
- * advice and setpoint segments are value-driven and read-only.
57
+ * Use as a building block for `<obc-transmitter>` and `<obc-transmitter-stack>`;
58
+ * it is the part that carries the measured value or the tag identifier. Slot in
59
+ * the type-specific icon; the advice and setpoint segments are value-driven and
60
+ * read-only.
61
+ *
62
+ * **TODO(designer):** the hit area is the visible chip, 24 px high at `regular`:
63
+ * at the WCAG 2.5.8 floor of 24 px, below the 48 px touch-target token. A
64
+ * 48 px target would overlap the id tag, the leader line and neighbouring
65
+ * `<obc-transmitter-stack>` segments, so the size needs a design decision.
63
66
  *
64
67
  * ### Slots
65
68
  * | Slot Name | Conditions | Purpose |
@@ -69,9 +72,12 @@ const readoutSizeBySize: Record<TransmitterButtonSize, ReadoutBlockSize> = {
69
72
  * @property maxDigits - Integer digits to reserve / hint (independent of `fractionDigits`).
70
73
  * @property hasSignSpacer - Reserve a minus-sign column on every segment, filled by the real sign
71
74
  * only while a value is negative, so the width does not change across zero.
75
+ * @property hasDegree - Show a degree column between the value and the unit (e.g. `12.3°` then `C`).
72
76
  * @property adviceValue - Advisory value shown in the leading advice segment when `hasAdvice`.
73
77
  * @property setpointValue - Target value shown in the setpoint segment when `hasSetPoint`.
74
78
  * @property label - Short tag identifier shown in the `tag` variant (e.g. `TT`).
79
+ * @property idTag - Identifier appended to the button's accessible name only (e.g. `#0001`); the
80
+ * parent transmitter draws the visible tag outside the button, where it cannot join the name.
75
81
  * @slot icon - Leading icon beside the value.
76
82
  *
77
83
  * @experimental
@@ -90,6 +96,7 @@ export class ObcTransmitterButton extends LitElement {
90
96
 
91
97
  @property({type: Boolean}) hintedZeros = false;
92
98
  @property({type: Boolean}) hasSignSpacer = false;
99
+ @property({type: Boolean}) hasDegree = false;
93
100
  @property({type: Boolean}) hasIcon = false;
94
101
  @property({type: Boolean}) hasAdvice = false;
95
102
 
@@ -101,53 +108,37 @@ export class ObcTransmitterButton extends LitElement {
101
108
 
102
109
  @property({type: String}) label = '';
103
110
 
111
+ @property({type: String}) idTag = '';
112
+
104
113
  private get isTag() {
105
114
  return this.variant === TransmitterButtonVariant.tag;
106
115
  }
107
116
 
108
- private renderBlock(
109
- variant: ReadoutBlockVariant,
110
- value: number | null | undefined
111
- ) {
112
- return html`<obc-readout-block
113
- .variant=${variant}
114
- .value=${value ?? null}
115
- .size=${readoutSizeBySize[this.size]}
116
- .fractionDigits=${this.fractionDigits}
117
- .maxDigits=${this.maxDigits}
118
- .hintedZeros=${this.hintedZeros}
119
- .hasSignSpacer=${this.hasSignSpacer}
120
- ></obc-readout-block>`;
121
- }
122
-
123
- private renderAdvice() {
124
- if (!this.hasAdvice) {
125
- return nothing;
126
- }
127
- return this.renderBlock(ReadoutBlockVariant.advice, this.adviceValue);
128
- }
129
-
130
- private renderSetpoint() {
131
- if (!this.hasSetPoint) {
132
- return nothing;
133
- }
134
- return this.renderBlock(ReadoutBlockVariant.setpoint, this.setpointValue);
135
- }
136
-
137
- private renderContent() {
138
- if (this.isTag) {
139
- return html`<span class="label">${this.label}</span>`;
140
- }
141
-
117
+ private renderReading() {
118
+ const format = {
119
+ hintedZeros: this.hintedZeros,
120
+ hasSignSpacer: this.hasSignSpacer,
121
+ };
142
122
  return html`
143
- ${this.renderAdvice()} ${this.renderSetpoint()}
144
- <div class="value-container">
123
+ <obc-readout-list-item
124
+ .size=${readoutSizeBySize[this.size]}
125
+ .value=${this.value ?? null}
126
+ .unit=${this.unit}
127
+ .hasDegree=${this.hasDegree}
128
+ .fractionDigits=${this.fractionDigits}
129
+ .maxDigits=${this.maxDigits}
130
+ .valueOptions=${{...format, hasIcon: this.hasIcon}}
131
+ .hasSetpoint=${this.hasSetPoint}
132
+ .setpoint=${this.setpointValue ?? undefined}
133
+ .setpointOptions=${format}
134
+ .hasAdvice=${this.hasAdvice}
135
+ .advice=${this.adviceValue ?? undefined}
136
+ .adviceOptions=${format}
137
+ >
145
138
  ${this.hasIcon
146
- ? html`<div class="icon"><slot name="icon"></slot></div>`
139
+ ? html`<slot name="icon" slot="value-icon"></slot>`
147
140
  : nothing}
148
- ${this.renderBlock(ReadoutBlockVariant.value, this.value)}
149
- ${this.unit ? html`<span class="unit">${this.unit}</span>` : nothing}
150
- </div>
141
+ </obc-readout-list-item>
151
142
  `;
152
143
  }
153
144
 
@@ -161,8 +152,13 @@ export class ObcTransmitterButton extends LitElement {
161
152
  tag: this.isTag,
162
153
  })}
163
154
  >
164
- ${this.renderContent()}
155
+ ${this.isTag
156
+ ? html`<span class="label">${this.label}</span>`
157
+ : this.renderReading()}
165
158
  </div>
159
+ ${this.idTag
160
+ ? html`<span class="visually-hidden">${this.idTag}</span>`
161
+ : nothing}
166
162
  </button>
167
163
  `;
168
164
  }
@@ -0,0 +1,56 @@
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ :host {
6
+ display: block;
7
+ }
8
+
9
+ .transmitter {
10
+ @mixin transmitter-leader;
11
+ }
12
+
13
+ .values {
14
+ display: grid;
15
+ grid-auto-flow: column;
16
+ grid-auto-columns: 1fr; /* equal segments, sized by the widest reading (Figma 25656:45218) */
17
+ }
18
+
19
+ .cell {
20
+ position: relative; /* containing block for the left/right id tag (transmitter-leader) */
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ gap: 2px;
25
+ }
26
+
27
+ .orientation-top .cell {
28
+ flex-direction: column-reverse;
29
+ }
30
+
31
+ .segment {
32
+ width: 100%;
33
+ }
34
+
35
+ .segment::part(button) {
36
+ flex: 1;
37
+ justify-content: center;
38
+ }
39
+
40
+ /* Neighbours share one border line; only the chip's outer corners round. */
41
+ .cell:not(:last-child) .segment::part(button) {
42
+ border-right: none;
43
+ border-top-right-radius: 0;
44
+ border-bottom-right-radius: 0;
45
+ }
46
+
47
+ .cell:not(:first-child) .segment::part(button) {
48
+ border-top-left-radius: 0;
49
+ border-bottom-left-radius: 0;
50
+ }
51
+
52
+ .id-tag {
53
+ @mixin transmitter-id-tag;
54
+ /* an empty tag keeps the chip row aligned */
55
+ min-height: var(--global-typography-instrument-label-line-height);
56
+ }
@@ -0,0 +1,210 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {render} from 'vitest-browser-lit';
3
+ import {page, userEvent} from '@vitest/browser/context';
4
+ import {html} from 'lit';
5
+ import '../../main.css';
6
+ import './transmitter-stack.js';
7
+ import type {
8
+ ObcTransmitterStack,
9
+ TransmitterStackValue,
10
+ TransmitterStackValueClickDetail,
11
+ } from './transmitter-stack.js';
12
+ import type {ObcTransmitterButton} from '../transmitter-button/transmitter-button.js';
13
+ import type {ObcReadoutListItem} from '../../navigation-instruments/readout-list-item/readout-list-item.js';
14
+
15
+ async function setup(values: TransmitterStackValue[]) {
16
+ const screen = render(
17
+ html`<obc-transmitter-stack .values=${values}>
18
+ <span slot="temperature" id="icon"></span>
19
+ </obc-transmitter-stack>`
20
+ );
21
+ const el = screen.container.querySelector(
22
+ 'obc-transmitter-stack'
23
+ ) as ObcTransmitterStack;
24
+ await el.updateComplete;
25
+ return el;
26
+ }
27
+
28
+ function segments(el: ObcTransmitterStack): ObcTransmitterButton[] {
29
+ return Array.from(el.shadowRoot!.querySelectorAll('obc-transmitter-button'));
30
+ }
31
+
32
+ function idTags(el: ObcTransmitterStack): string[] {
33
+ return Array.from(el.shadowRoot!.querySelectorAll('.id-tag')).map(
34
+ (tag) => tag.textContent?.trim() ?? ''
35
+ );
36
+ }
37
+
38
+ describe('obc-transmitter-stack', () => {
39
+ it('renders one segment per value, in order', async () => {
40
+ const el = await setup([
41
+ {value: 12.3, unit: '°C'},
42
+ {value: 1.23, unit: 'm', fractionDigits: 2},
43
+ ]);
44
+
45
+ expect(
46
+ segments(el).map((s) => [s.value, s.unit, s.fractionDigits])
47
+ ).toEqual([
48
+ [12.3, '°C', 1],
49
+ [1.23, 'm', 2],
50
+ ]);
51
+ });
52
+
53
+ it('renders each reading through a label-less readout list item', async () => {
54
+ const el = await setup([
55
+ {value: 12.3, unit: 'C', hasDegree: true},
56
+ {value: 1, unit: 'm'},
57
+ ]);
58
+ const items = await Promise.all(
59
+ segments(el).map(async (segment) => {
60
+ await segment.updateComplete;
61
+ return segment.shadowRoot!.querySelector(
62
+ 'obc-readout-list-item'
63
+ ) as ObcReadoutListItem;
64
+ })
65
+ );
66
+
67
+ expect(
68
+ items.map((item) => [item.value, item.unit, item.hasDegree, item.label])
69
+ ).toEqual([
70
+ [12.3, 'C', true, undefined],
71
+ [1, 'm', false, undefined],
72
+ ]);
73
+ });
74
+
75
+ it('defaults an omitted digit key but forwards one that never arrived', async () => {
76
+ const el = await setup([
77
+ {value: 1},
78
+ {value: 1, fractionDigits: undefined, maxDigits: NaN},
79
+ ]);
80
+ const [omitted, missing] = segments(el);
81
+
82
+ expect([omitted.fractionDigits, omitted.maxDigits]).toEqual([1, 0]);
83
+ expect(missing.fractionDigits).toBeNaN();
84
+ expect(missing.maxDigits).toBeNaN();
85
+ });
86
+
87
+ it('forwards the named icon slot into the segment', async () => {
88
+ const el = await setup([
89
+ {value: 1, iconSlotName: 'temperature'},
90
+ {value: 2},
91
+ ]);
92
+ const [withIcon, withoutIcon] = segments(el);
93
+ const forwarded = withIcon.querySelector('slot') as HTMLSlotElement;
94
+
95
+ expect(withIcon.hasIcon).toBe(true);
96
+ expect(withoutIcon.hasIcon).toBe(false);
97
+ expect(forwarded.name).toBe('temperature');
98
+ expect(forwarded.getAttribute('slot')).toBe('icon');
99
+ expect(forwarded.assignedElements()[0]?.id).toBe('icon');
100
+ });
101
+
102
+ it('fires value-click with the pressed segment', async () => {
103
+ const values = [{value: 1}, {value: 2}];
104
+ const el = await setup(values);
105
+ const details: TransmitterStackValueClickDetail[] = [];
106
+ el.addEventListener('value-click', (event) =>
107
+ details.push(
108
+ (event as CustomEvent<TransmitterStackValueClickDetail>).detail
109
+ )
110
+ );
111
+ const second = segments(el)[1];
112
+ await second.updateComplete;
113
+
114
+ second.shadowRoot!.querySelector('button')!.click();
115
+
116
+ expect(details).toEqual([{index: 1, value: values[1]}]);
117
+ });
118
+
119
+ it('reports the rendered value when values change before the next render', async () => {
120
+ const rendered = [{value: 1}, {value: 2}];
121
+ const el = await setup(rendered);
122
+ const details: TransmitterStackValueClickDetail[] = [];
123
+ el.addEventListener('value-click', (event) =>
124
+ details.push(
125
+ (event as CustomEvent<TransmitterStackValueClickDetail>).detail
126
+ )
127
+ );
128
+ const second = segments(el)[1];
129
+ await second.updateComplete;
130
+
131
+ el.values = [{value: 3}, {value: 4}];
132
+ second.shadowRoot!.querySelector('button')!.click();
133
+
134
+ expect(details).toEqual([{index: 1, value: rendered[1]}]);
135
+ });
136
+
137
+ it('reaches each segment with Tab and activates it with Enter and Space', async () => {
138
+ const el = await setup([{value: 1}, {value: 2}]);
139
+ const indexes: number[] = [];
140
+ el.addEventListener('value-click', (event) =>
141
+ indexes.push(
142
+ (event as CustomEvent<TransmitterStackValueClickDetail>).detail.index
143
+ )
144
+ );
145
+ await Promise.all(segments(el).map((segment) => segment.updateComplete));
146
+
147
+ await userEvent.tab();
148
+ await userEvent.keyboard('{Enter}');
149
+ await userEvent.tab();
150
+ await userEvent.keyboard(' ');
151
+
152
+ expect(indexes).toEqual([0, 1]);
153
+ });
154
+
155
+ it('names each segment by its reading and id tag, and hides the visible tag', async () => {
156
+ const el = await setup([
157
+ {value: 1, unit: 'm', idTag: '#0001'},
158
+ {value: 1, unit: 'm', idTag: '#0002'},
159
+ ]);
160
+ await Promise.all(segments(el).map((segment) => segment.updateComplete));
161
+
162
+ await expect
163
+ .element(page.getByRole('button', {name: /#0002/}))
164
+ .toBeInTheDocument();
165
+ expect(
166
+ Array.from(el.shadowRoot!.querySelectorAll('.id-tag')).map((tag) =>
167
+ tag.getAttribute('aria-hidden')
168
+ )
169
+ ).toEqual(['true', 'true']);
170
+ });
171
+
172
+ it('renders nothing, leader line included, without values', async () => {
173
+ const el = await setup([]);
174
+
175
+ expect(el.shadowRoot!.querySelector('.transmitter')).toBeNull();
176
+ });
177
+
178
+ it('gives every segment the width of the widest reading', async () => {
179
+ const el = await setup([
180
+ {value: 1, unit: 'm'},
181
+ {value: 12345.6, unit: 'mVs', maxDigits: 5},
182
+ ]);
183
+ await Promise.all(segments(el).map((segment) => segment.updateComplete));
184
+ const [narrow, wide] = segments(el).map(
185
+ (segment) => segment.getBoundingClientRect().width
186
+ );
187
+
188
+ expect(narrow).toBeCloseTo(wide, 0);
189
+ });
190
+
191
+ it('keeps each segment at or above the 24 px pointer-target floor', async () => {
192
+ const el = await setup([{value: 1, unit: 'm'}]);
193
+ const [segment] = segments(el);
194
+ await segment.updateComplete;
195
+ const target = segment
196
+ .shadowRoot!.querySelector('button')!
197
+ .getBoundingClientRect();
198
+
199
+ expect(target.height).toBeGreaterThanOrEqual(24);
200
+ expect(target.width).toBeGreaterThanOrEqual(24);
201
+ });
202
+
203
+ it('renders an id-tag row in every cell once any value has one', async () => {
204
+ expect(idTags(await setup([{value: 1, idTag: '#1'}, {value: 2}]))).toEqual([
205
+ '#1',
206
+ '',
207
+ ]);
208
+ expect(idTags(await setup([{value: 1}, {value: 2}]))).toEqual([]);
209
+ });
210
+ });