@kubex/zinc 1.1.116 → 1.1.118

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.
@@ -1,10 +1,9 @@
1
1
  import {classMap} from 'lit/directives/class-map.js';
2
2
  import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from 'lit';
3
3
  import {HasSlotController} from '../../internal/slot';
4
- import {ifDefined} from 'lit/directives/if-defined.js';
5
4
  import {property, state} from 'lit/decorators.js';
6
5
  import ZnChip from '../chip';
7
- import ZnHeader from '../header';
6
+ import ZnFormGroup from '../form-group';
8
7
  import ZnOption from '../option';
9
8
  import ZnPanel from '../panel/panel.component';
10
9
  import ZnSelect from '../select';
@@ -19,8 +18,12 @@ import styles from './translation-group.scss';
19
18
  * @status experimental
20
19
  * @since 1.0
21
20
  *
22
- * The select sits at the top right of the header, opposite the caption. Choosing a language switches every child at
23
- * once, and each child hides its own select while it is in a group — `grouped` is set on them here.
21
+ * The fields sit in a `zn-form-group`, so the caption, help text and the language select share its label column and
22
+ * the fields line up with every other form group around them. Choosing a language switches every child at once, and
23
+ * each child hides its own select while it is in a group — `grouped` is set on them here.
24
+ *
25
+ * The select is searchable. Each option holds its language code as its value, so typing `de` finds German without the
26
+ * code being on show.
24
27
  *
25
28
  * Closed, the select carries how many target languages are done — `1/5`. Its options each carry a chip aggregated
26
29
  * across the children:
@@ -35,11 +38,11 @@ import styles from './translation-group.scss';
35
38
  * The children own their values; this component only chooses which language is shown and reports on what they hold.
36
39
  * It reads them back on every child `zn-change`, so the chips and the count follow an edit as it is typed.
37
40
  *
38
- * Extends `zn-panel`, so `caption`, `icon`, `flush`, `transparent` and the `footer` slot behave as they do there.
39
- * Nested inside another panel, add `inline` to drop the chrome and keep the fields aligned with the surrounding form.
41
+ * Extends `zn-panel`, so `caption`, `flush`, `transparent` and the `footer` slot behave as they do there. Nested
42
+ * inside another panel, add `inline` to drop the chrome and keep the fields aligned with the surrounding form.
40
43
  *
41
44
  * @dependency zn-chip
42
- * @dependency zn-header
45
+ * @dependency zn-form-group
43
46
  * @dependency zn-option
44
47
  * @dependency zn-select
45
48
  *
@@ -49,30 +52,33 @@ import styles from './translation-group.scss';
49
52
  * @slot actions - Buttons for the bottom of the panel, on the white body rather than the grey footer. They sit on
50
53
  * the right, as zinc's form action rows do; `align="start"` moves one to the left. Write them in the order they
51
54
  * should be read — the sides are set by CSS ordering, so markup order is what a keyboard follows.
52
- * @slot footer - Content displayed in the grey panel footer. The header belongs to the language select; nothing
53
- * else is slotted into it.
55
+ * @slot footer - Content displayed in the grey panel footer.
54
56
  *
55
57
  * @csspart base - The component's base wrapper.
58
+ * @csspart form-group - The form group holding the caption, the language select and the fields.
56
59
  * @csspart actions - The row of buttons at the bottom of the body.
57
- * @csspart language-field - The label and select that choose the language every child is editing.
60
+ * @csspart language-field - The select that chooses the language every child is editing, in the group's chip slot.
58
61
  * @csspart language-select - The select itself.
59
62
  */
60
63
  export default class ZnTranslationGroup extends ZnPanel {
61
64
  static styles: CSSResultGroup = [ZnPanel.styles, unsafeCSS(styles)];
62
65
  static dependencies = {
63
66
  'zn-chip': ZnChip,
64
- 'zn-header': ZnHeader,
67
+ 'zn-form-group': ZnFormGroup,
65
68
  'zn-option': ZnOption,
66
69
  'zn-select': ZnSelect
67
70
  };
68
71
 
69
72
  private readonly _slotController = new HasSlotController(this, 'actions', 'footer');
70
73
 
71
- /** The caption shown in the panel header. An alias for the inherited `caption`, which wins where both are set. */
74
+ /** The form group's label. An alias for the inherited `caption`, which wins where both are set. */
72
75
  @property() label = '';
73
76
 
77
+ /** Sits under the label, above the language select, as help text does in any other form group. */
78
+ @property({attribute: 'help-text'}) helpText = '';
79
+
74
80
  /**
75
- * Drops the panel chrome — border, background and padding — so the group reads as a section of the form around it
81
+ * Drops the panel chrome — border, background and padding — so the group reads as a section of the surrounding form
76
82
  * rather than a panel of its own. For groups nested inside another panel, where the fields would otherwise sit
77
83
  * indented behind a second border.
78
84
  */
@@ -149,11 +155,9 @@ export default class ZnTranslationGroup extends ZnPanel {
149
155
  return {type: 'error', label: language === 'en' ? 'Empty' : 'English'};
150
156
  }
151
157
 
152
- /** `English (EN)`, or the code alone where the configured name already is the code. */
158
+ /** The configured name, or the code where `languages` does not name the language. */
153
159
  private displayName(language: string): string {
154
- const name = this.languages[language] ?? language.toUpperCase();
155
- const code = language.toUpperCase();
156
- return name.toUpperCase() === code ? name : `${name} (${code})`;
160
+ return this.languages[language] ?? language.toUpperCase();
157
161
  }
158
162
 
159
163
  /** Children take their language list from the group, so a change to `languages` has to reach them. */
@@ -206,7 +210,7 @@ export default class ZnTranslationGroup extends ZnPanel {
206
210
  render() {
207
211
  const hasActionsSlot = this._slotController.test('actions');
208
212
  const hasFooterSlot = this._slotController.test('footer');
209
- const headerCaption = this.caption || this.label;
213
+ const caption = this.caption || this.label;
210
214
 
211
215
  // A child's value can carry a language `languages` does not list — server-rendered content outliving a config
212
216
  // change. Offer those too, or the translation is stranded in the value with no way to reach it.
@@ -216,7 +220,6 @@ export default class ZnTranslationGroup extends ZnPanel {
216
220
  .forEach(code => extra.add(code)));
217
221
  const languageCodes = [...Object.keys(this.languages), ...extra];
218
222
  const hasMultipleLanguages = languageCodes.length > 1;
219
- const hasHeader = Boolean(headerCaption) || hasMultipleLanguages;
220
223
 
221
224
  // English is the source every other language falls back to, so it is not itself one of the translations counted.
222
225
  const targets = languageCodes.filter(code => code !== 'en');
@@ -229,49 +232,50 @@ export default class ZnTranslationGroup extends ZnPanel {
229
232
  };
230
233
 
231
234
  return html`
232
- <div class="${classMap({
235
+ <div part="base" class="${classMap({
233
236
  panel: true,
234
237
  'panel--flush': this.flush || this.inline,
235
238
  'panel--transparent': this.transparent || this.inline,
236
239
  'translation-group--inline': this.inline,
237
- 'panel--has-header': hasHeader,
238
240
  'panel--has-actions': hasActionsSlot,
239
241
  'panel--has-footer': hasFooterSlot,
240
242
  })}">
241
243
 
242
244
  <div class="panel__inner">
243
- ${hasHeader ? html`
244
- <zn-header class="panel__header"
245
- caption="${ifDefined(headerCaption || undefined)}"
246
- transparent>
247
- ${hasMultipleLanguages ? html`
248
- <div slot="actions" class="translation-group__language-field" part="language-field">
249
- <zn-select
250
- label="${this.languageLabel}"
251
- class="translation-group__language-select"
252
- part="language-select"
253
- hoist
254
- .value="${this._activeLanguage}"
255
- @zn-change="${this.handleLanguageSelect}"
256
- @zn-input="${this.handleLanguageInput}">
257
- <zn-chip slot="suffix" type="${summary.type}">${summary.label}</zn-chip>
258
- ${languageCodes.map(code => {
259
- const optionState = this.languageState(code);
260
- return html`
261
- <zn-option value="${code}">
262
- ${this.displayName(code)}
263
- <zn-chip slot="suffix" type="${optionState.type}">${optionState.label}</zn-chip>
264
- </zn-option>`;
265
- })}
266
- </zn-select>
267
- </div>` : nothing}
268
- </zn-header>` : null}
269
-
270
245
  <div class="panel__content">
271
246
  <div class="panel__body">
272
- <slot
273
- @slotchange="${this.handleSlotChange}"
274
- @zn-change="${this.handleChildChange}"></slot>
247
+ <zn-form-group
248
+ part="form-group"
249
+ label="${caption}"
250
+ help-text="${this.helpText}">
251
+
252
+ ${hasMultipleLanguages ? html`
253
+ <div slot="chip" class="translation-group__language-field" part="language-field">
254
+ <zn-select
255
+ label="${this.languageLabel}"
256
+ class="translation-group__language-select"
257
+ part="language-select"
258
+ hoist
259
+ search
260
+ .value="${this._activeLanguage}"
261
+ @zn-change="${this.handleLanguageSelect}"
262
+ @zn-input="${this.handleLanguageInput}">
263
+ <zn-chip slot="suffix" type="${summary.type}">${summary.label}</zn-chip>
264
+ ${languageCodes.map(code => {
265
+ const optionState = this.languageState(code);
266
+ return html`
267
+ <zn-option value="${code}">
268
+ ${this.displayName(code)}
269
+ <zn-chip slot="suffix" type="${optionState.type}">${optionState.label}</zn-chip>
270
+ </zn-option>`;
271
+ })}
272
+ </zn-select>
273
+ </div>` : nothing}
274
+
275
+ <slot
276
+ @slotchange="${this.handleSlotChange}"
277
+ @zn-change="${this.handleChildChange}"></slot>
278
+ </zn-form-group>
275
279
 
276
280
  ${hasActionsSlot ? html`
277
281
  <div class="translation-group__actions" part="actions">
@@ -2,81 +2,15 @@
2
2
  display: block;
3
3
  }
4
4
 
5
- // The body stacks the fields the select drives, so they take the row gap zn-form-group sets between stacked
6
- // controls. The slotted children are `display: contents` through the slot, putting them in this same flex flow.
5
+ // The body holds the form group and the actions row; the gap is what separates them.
7
6
  .panel__body {
8
7
  gap: var(--zn-spacing-medium);
9
8
  }
10
9
 
11
- // The header sets its own gap down to the first field; the panel's body padding on top of it would make that one gap
12
- // the odd one out.
13
- .panel--has-header .panel__body {
14
- padding-top: 0;
15
- }
16
-
17
- .panel__header {
18
- padding-bottom: var(--zn-spacing-medium);
19
- }
20
-
21
- .translation-group--inline .panel__header {
22
- padding: 0 0 var(--zn-spacing-medium);
23
- }
24
-
25
- // zn-header holds its content row at 36px so a caption sits level with action buttons. The select is taller than
26
- // that and sets the height on its own, so the row hugs whichever of the two is bigger.
27
- .panel__header::part(content) {
28
- min-height: 0;
29
- }
30
-
31
- // The actions container is a plain block that clips horizontally: its content would sit at the top rather than level
32
- // with the caption, and a select flush against its right edge would lose the focus ring.
33
- .panel__header::part(header-right) {
34
- display: flex;
35
- align-items: center;
36
- justify-content: flex-end;
37
- overflow: visible;
38
- }
39
-
40
- .translation-group__language-field {
41
- display: flex;
42
- align-items: center;
43
- }
44
-
45
- // The trigger shows one language, so it sizes to that.
46
- .translation-group__language-select {
47
- width: max-content;
48
- max-width: 100%;
49
- }
50
-
51
- // Left alone the display input hands the trigger its default 20-character intrinsic width, however short the language
52
- // name is. Where field-sizing is unsupported that width is what is left, which only makes the trigger a little wider.
53
- .translation-group__language-select::part(display-input) {
54
- width: auto;
55
- min-width: 0;
56
- field-sizing: content;
57
- }
58
-
59
- // The popup syncs its width to the trigger, which is now sized to the current language rather than the longest one in
60
- // the list. zn-select runs the popup with auto-size="vertical", so --auto-size-available-width is never set and the
61
- // listbox's own max-width is inert — this is the only thing keeping a long list on screen.
62
- .translation-group__language-select::part(listbox) {
63
- min-width: 320px;
64
- max-width: calc(100vw - var(--zn-spacing-large));
65
- }
66
-
67
- // The select is chrome in the panel header rather than a field to fill in, so it drops the input's fill and shadow.
68
- // The border stays, marking it out as something to open.
69
- .translation-group__language-select::part(combobox) {
70
- background-color: transparent;
71
- box-shadow: none;
72
- }
73
-
74
- .translation-group__language-select:hover::part(combobox) {
75
- background-color: var(--zn-input-background-color-hover);
76
- }
77
-
78
- .translation-group__language-select:focus-within::part(combobox) {
79
- box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
10
+ // The fields land in the form group's 6-column input grid, but they are assigned to this slot rather than the
11
+ // group's, so its span rules cannot reach them — each takes the full row from here.
12
+ ::slotted(:not([slot])) {
13
+ grid-column: 1 / -1;
80
14
  }
81
15
 
82
16
  // The select's label is its accessible name only; the caption names the section on screen.
@@ -92,11 +26,13 @@
92
26
  border: 0;
93
27
  }
94
28
 
95
- // Matches zinc's form action rows (.form-actions, zn-form-actions): right-aligned, 16px apart.
29
+ // Matches zinc's form action rows (.form-actions, zn-form-actions): right-aligned, 16px apart. The cap holds the
30
+ // buttons level with the fields' right edge, which zn-form-group sizes to the same container.
96
31
  .translation-group__actions {
97
32
  display: flex;
98
33
  align-items: center;
99
34
  gap: var(--zn-spacing-small);
35
+ max-width: var(--zn-container-lg);
100
36
  }
101
37
 
102
38
  // The spacer, not an auto margin, is what splits the two sides: an auto margin on every right-hand button would open
@@ -114,8 +50,8 @@
114
50
  order: 0;
115
51
  }
116
52
 
117
- // The panel draws a border between any two body children, and the actions row is the first real element to follow
118
- // the fields' slot. Matched on .panel--has-actions to out-specify that rule.
53
+ // The panel draws a border between any two body children, and the actions row follows the form group. Matched on
54
+ // .panel--has-actions to out-specify that rule.
119
55
  .panel--has-actions .panel__body > .translation-group__actions {
120
56
  border: 0;
121
57
  }
@@ -1,6 +1,8 @@
1
1
  import '../../../dist/zn.min.js';
2
2
  import { expect, fixture, html } from '@open-wc/testing';
3
+ import type ZnFormGroup from '../form-group/form-group.component';
3
4
  import type ZnInput from '../input/input.component';
5
+ import type ZnOption from '../option/option.component';
4
6
  import type ZnSelect from '../select/select.component';
5
7
  import type ZnTranslationGroup from './translation-group.component';
6
8
  import type ZnTranslations from '../translations/translations.component';
@@ -110,24 +112,30 @@ describe('<zn-translation-group>', () => {
110
112
 
111
113
  const body = group.shadowRoot!.querySelector<HTMLElement>('.panel__body')!;
112
114
  expect(getComputedStyle(body).paddingLeft).to.equal('0px');
113
- expect(getComputedStyle(group.shadowRoot!.querySelector<HTMLElement>('.panel__header')!).paddingLeft)
114
- .to.equal('0px');
115
+
116
+ const formGroup = group.shadowRoot!.querySelector<ZnFormGroup>('zn-form-group')!;
117
+ expect(formGroup.getBoundingClientRect().left, 'flush with the form around it')
118
+ .to.equal(group.getBoundingClientRect().left);
115
119
  });
116
120
 
117
- it('leaves the same gap under the caption as between the fields', async () => {
121
+ it('leaves the same gap between the fields as between the group and its actions', async () => {
118
122
  const group = await fixture<ZnTranslationGroup>(html`
119
123
  <zn-translation-group inline label="Content" .languages=${{en: 'English', fr: 'French'}}>
120
124
  <zn-translations name="heading"></zn-translations>
121
125
  </zn-translation-group>`);
122
- // The spacing tokens live in the theme stylesheet, which the bundle under test does not carry.
126
+ // The spacing tokens live in the theme stylesheet, which the bundle under test does not carry. The form
127
+ // group's gap is one shorthand across both axes, so an unset column gap would void the row gap with it.
123
128
  group.style.setProperty('--zn-spacing-medium', '24px');
129
+ group.style.setProperty('--zn-spacing-small', '16px');
124
130
  await group.updateComplete;
125
131
 
126
- const header = group.shadowRoot!.querySelector<HTMLElement>('.panel__header')!;
132
+ const formGroup = group.shadowRoot!.querySelector<ZnFormGroup>('zn-form-group')!;
133
+ await formGroup.updateComplete;
134
+ const fields = formGroup.shadowRoot!.querySelector<HTMLElement>('.form-control-input')!;
127
135
  const body = group.shadowRoot!.querySelector<HTMLElement>('.panel__body')!;
128
- // zn-form-group's row gap between stacked controls. The header heads the same stack, so its gap matches.
129
- expect(getComputedStyle(body).rowGap).to.equal('24px');
130
- expect(getComputedStyle(header).paddingBottom).to.equal('24px');
136
+
137
+ expect(getComputedStyle(fields).rowGap, "the form group's gap between fields").to.equal('24px');
138
+ expect(getComputedStyle(body).rowGap, 'and down to the actions row').to.equal('24px');
131
139
  });
132
140
 
133
141
 
@@ -150,7 +158,7 @@ describe('<zn-translation-group>', () => {
150
158
  return group.shadowRoot!.querySelector<ZnSelect>('zn-select')!;
151
159
  }
152
160
 
153
- it('sits in the header, opposite the caption', async () => {
161
+ it("sits under the caption, in the form group's chip slot", async () => {
154
162
  const group = await fixture<ZnTranslationGroup>(html`
155
163
  <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French'}}>
156
164
  <zn-translations name="heading"></zn-translations>
@@ -158,66 +166,92 @@ describe('<zn-translation-group>', () => {
158
166
  await group.updateComplete;
159
167
 
160
168
  const field = group.shadowRoot!.querySelector<HTMLElement>('.translation-group__language-field')!;
161
- expect(field.getAttribute('slot')).to.equal('actions');
162
- expect(field.closest('zn-header')).to.exist;
163
- expect(group.shadowRoot!.querySelector('.panel__body zn-select')).to.be.null;
169
+ expect(field.assignedSlot?.name, 'assigned to the chip slot').to.equal('chip');
170
+
171
+ const formGroup = group.shadowRoot!.querySelector<ZnFormGroup>('zn-form-group')!;
172
+ await formGroup.updateComplete;
173
+ const label = formGroup.shadowRoot!.querySelector<HTMLElement>('[part="form-control-label"]')!;
174
+ expect(field.getBoundingClientRect().top, 'below the caption')
175
+ .to.be.greaterThan(label.getBoundingClientRect().bottom - 1);
164
176
  });
165
177
 
166
- it('leaves the caption to the label alone, with the count on the chip', async () => {
178
+ it('leaves the caption to the form group label, with the count on the chip', async () => {
167
179
  const group = await fixture<ZnTranslationGroup>(html`
168
180
  <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French'}}>
169
181
  <zn-translations name="heading" value='{"en":"Hi","fr":"Salut"}'></zn-translations>
170
182
  </zn-translation-group>`);
171
183
  await group.updateComplete;
172
184
 
173
- const header = group.shadowRoot!.querySelector('.panel__header')!;
174
- expect(header.getAttribute('caption')).to.equal('Content');
175
- expect(header.querySelector('[slot="caption"]'), 'nothing else rides the caption').to.be.null;
176
- expect(group.shadowRoot!.querySelector('.translation-group__language-label')).to.be.null;
185
+ const formGroup = group.shadowRoot!.querySelector<ZnFormGroup>('zn-form-group')!;
186
+ await formGroup.updateComplete;
187
+ const label = formGroup.shadowRoot!.querySelector<HTMLElement>('[part="form-control-label"]')!;
188
+ expect(label.textContent?.trim(), 'nothing else rides the caption').to.equal('Content');
189
+
190
+ const summary = selectOf(group).querySelector('zn-chip[slot="suffix"]')!;
191
+ expect(summary.textContent?.trim()).to.equal('1/1');
177
192
  });
178
193
 
179
- it('drops the fill and shadow but keeps the border, hover and focus', async () => {
194
+ it('wears the same chrome as any other select', async () => {
180
195
  const group = await fixture<ZnTranslationGroup>(html`
181
196
  <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French'}}>
182
197
  <zn-translations name="heading"></zn-translations>
183
198
  </zn-translation-group>`);
184
199
  // The input tokens live in the theme stylesheet, which the bundle under test does not carry.
200
+ group.style.setProperty('--zn-input-background-color', 'rgb(1, 2, 3)');
201
+ group.style.setProperty('--zn-shadow-x-small', '0 1px 2px rgb(7, 8, 9)');
185
202
  group.style.setProperty('--zn-input-border-width', '1px');
186
203
  group.style.setProperty('--zn-input-border-color', 'rgb(9, 9, 9)');
187
- group.style.setProperty('--zn-focus-ring-width', '3px');
188
- group.style.setProperty('--zn-input-focus-ring-color', 'rgb(4, 5, 6)');
189
204
  await group.updateComplete;
190
205
 
191
206
  const select = selectOf(group);
192
207
  await select.updateComplete;
193
- const combobox = select.shadowRoot!.querySelector<HTMLElement>('[part~="combobox"]')!;
208
+ const styles = getComputedStyle(select.shadowRoot!.querySelector<HTMLElement>('[part~="combobox"]')!);
194
209
 
195
- const styles = getComputedStyle(combobox);
196
- expect(styles.backgroundColor).to.equal('rgba(0, 0, 0, 0)');
197
- expect(styles.boxShadow).to.equal('none');
198
- expect(styles.borderTopWidth, 'the border stays').to.equal('1px');
199
- expect(styles.borderTopColor, 'the border stays').to.equal('rgb(9, 9, 9)');
210
+ expect(styles.backgroundColor, 'the fill').to.equal('rgb(1, 2, 3)');
211
+ expect(styles.boxShadow, 'the shadow').to.include('rgb(7, 8, 9)');
212
+ expect(styles.borderTopWidth, 'the border').to.equal('1px');
213
+ expect(styles.borderTopColor, 'the border').to.equal('rgb(9, 9, 9)');
214
+ });
200
215
 
201
- select.focus();
216
+ it('fills the width of the label column', async () => {
217
+ const el = await fixture<HTMLElement>(html`
218
+ <div style="width: 900px">
219
+ <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French'}}>
220
+ <zn-translations name="heading"></zn-translations>
221
+ </zn-translation-group>
222
+ </div>`);
223
+ const group = el.querySelector<ZnTranslationGroup>('zn-translation-group')!;
224
+ await group.updateComplete;
225
+
226
+ const formGroup = group.shadowRoot!.querySelector<ZnFormGroup>('zn-form-group')!;
227
+ await formGroup.updateComplete;
228
+ const column = formGroup.shadowRoot!.querySelector<HTMLElement>('[part="form-control-chip"]')!;
229
+
230
+ const select = selectOf(group);
202
231
  await select.updateComplete;
203
- expect(getComputedStyle(combobox).boxShadow, 'focus stays visible').to.not.equal('none');
232
+ expect(select.getBoundingClientRect().width).to.be.closeTo(column.getBoundingClientRect().width, 1);
204
233
  });
205
234
 
206
- it('sizes the trigger to its language and holds the listbox open wider', async () => {
235
+ it('filters the list on the name or the code, which stays off the option', async () => {
207
236
  const group = await fixture<ZnTranslationGroup>(html`
208
- <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French'}}>
237
+ <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French', de: 'German'}}>
209
238
  <zn-translations name="heading"></zn-translations>
210
239
  </zn-translation-group>`);
211
240
  await group.updateComplete;
212
241
 
213
242
  const select = selectOf(group);
214
243
  await select.updateComplete;
244
+ const option = (code: string) => select.querySelector<ZnOption>(`zn-option[value="${code}"]`)!;
245
+ expect(option('de').getTextLabel().trim(), 'the code is not on show').to.equal('German');
215
246
 
216
- // The whole point: the trigger is no longer pinned to the width the options need.
217
- expect(select.getBoundingClientRect().width).to.be.lessThan(320);
247
+ await select.show();
248
+ const input = select.shadowRoot!.querySelector<HTMLInputElement>('[part~="display-input"]')!;
249
+ input.value = 'de';
250
+ input.dispatchEvent(new InputEvent('input', {bubbles: true, composed: true}));
251
+ await select.updateComplete;
218
252
 
219
- const listbox = select.shadowRoot!.querySelector<HTMLElement>('[part~="listbox"]')!;
220
- expect(getComputedStyle(listbox).minWidth).to.equal('320px');
253
+ expect(option('de').hidden, 'matched on its code').to.be.false;
254
+ expect(option('fr').hidden, 'and nothing else').to.be.true;
221
255
  });
222
256
 
223
257
  it('names the select for a screen reader without showing the label', async () => {
@@ -234,18 +268,22 @@ describe('<zn-translation-group>', () => {
234
268
  expect(label.getBoundingClientRect().width).to.be.lessThan(2);
235
269
  });
236
270
 
237
- it('renders a header for the select even with no caption', async () => {
271
+ it('stands the select on its own with no caption', async () => {
238
272
  const group = await fixture<ZnTranslationGroup>(html`
239
273
  <zn-translation-group .languages=${{en: 'English', fr: 'French'}}>
240
274
  <zn-translations name="heading"></zn-translations>
241
275
  </zn-translation-group>`);
242
276
  await group.updateComplete;
243
277
 
244
- expect(group.shadowRoot!.querySelector('.panel__header')).to.exist;
278
+ const formGroup = group.shadowRoot!.querySelector<ZnFormGroup>('zn-form-group')!;
279
+ await formGroup.updateComplete;
280
+
245
281
  expect(selectOf(group)).to.exist;
282
+ expect(formGroup.shadowRoot!.querySelector('[part="form-control-label"]'), 'no empty caption').to.be.null;
283
+ expect(formGroup.shadowRoot!.querySelector('[part="form-control-chip"]'), 'the select keeps its column').to.exist;
246
284
  });
247
285
 
248
- it('keeps the header to itself, with slotted actions going to the bottom', async () => {
286
+ it('keeps the form group to the fields, with slotted actions going to the bottom', async () => {
249
287
  const group = await fixture<ZnTranslationGroup>(html`
250
288
  <zn-translation-group label="Content" .languages=${{en: 'English', fr: 'French'}}>
251
289
  <zn-button slot="actions">Auto translate</zn-button>
@@ -254,7 +292,7 @@ describe('<zn-translation-group>', () => {
254
292
  await group.updateComplete;
255
293
 
256
294
  const actionsSlot = group.shadowRoot!.querySelector('slot[name="actions"]')!;
257
- expect(actionsSlot.closest('zn-header'), 'the header takes no actions').to.be.null;
295
+ expect(actionsSlot.closest('zn-form-group'), 'the form group takes no actions').to.be.null;
258
296
  expect(actionsSlot.closest('.panel__body'), 'they go to the body').to.exist;
259
297
  });
260
298
 
@@ -223,14 +223,9 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
223
223
  return {type: 'error', label: language === 'en' ? 'Empty' : 'English'};
224
224
  }
225
225
 
226
- /**
227
- * `English (EN)` — the configured name plus its code, unless the name already is the code, in which case the code
228
- * alone. `languages` is written both ways: `{"en": "English"}` and `{"en": "EN"}`.
229
- */
226
+ /** The configured name, or the code where `languages` does not name the language. */
230
227
  private languageLabel(language: string): string {
231
- const name = this.languages[language] ?? language.toUpperCase();
232
- const code = language.toUpperCase();
233
- return name.toUpperCase() === code ? name : `${name} (${code})`;
228
+ return this.languages[language] ?? language.toUpperCase();
234
229
  }
235
230
 
236
231
  /**
@@ -496,6 +491,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
496
491
  class="translations__language-select"
497
492
  part="language-select"
498
493
  hoist
494
+ search
499
495
  ?disabled="${this.disabled}"
500
496
  .value="${this._activeLanguage}"
501
497
  @zn-change="${this.handleLanguageSelect}"