@kubex/zinc 1.1.115 → 1.1.116

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,4 +1,5 @@
1
1
  import {classMap} from 'lit/directives/class-map.js';
2
+ import {defaultValue} from '../../internal/default-value';
2
3
  import {FormControlController, validValidityState} from '../../internal/form';
3
4
  import {HasSlotController} from '../../internal/slot';
4
5
  import {html, nothing, unsafeCSS} from 'lit';
@@ -7,37 +8,76 @@ import {keyed} from 'lit/directives/keyed.js';
7
8
  import {live} from 'lit/directives/live.js';
8
9
  import {parseSlashItems, type SlashMenuItem} from '../slash-menu';
9
10
  import {property, state} from 'lit/decorators.js';
10
- import {ResizeController} from '@lit-labs/observers/resize-controller.js';
11
11
  import ZincElement from '../../internal/zinc-element';
12
- import ZnButton from '../button';
13
- import ZnButtonGroup from '../button-group';
14
- import ZnDropdown from '../dropdown';
12
+ import ZnChip from '../chip';
15
13
  import ZnInlineEdit from '../inline-edit';
16
14
  import ZnInput from '../input';
17
- import ZnMenu from '../menu';
15
+ import ZnOption from '../option';
16
+ import ZnSelect from '../select';
17
+ import ZnTextarea from '../textarea';
18
18
  import type {PropertyValues} from 'lit';
19
19
  import type {ZincFormControl} from '../../internal/zinc-element';
20
- import type {ZnMenuSelectEvent} from '../../events/zn-menu-select';
21
20
 
22
21
  import formControlStyles from '../../form-control.scss';
23
22
  import styles from './translations.scss';
24
23
 
24
+ /**
25
+ * @summary Collects one piece of text in several languages, one language at a time.
26
+ * @documentation https://zinc.style/components/translations
27
+ * @status experimental
28
+ * @since 1.0
29
+ *
30
+ * A select above the field chooses the language being edited. Closed, it carries how many languages are translated —
31
+ * `1/5`; open, every language it offers carries a chip saying whether it has a translation of its own or falls back
32
+ * to English. Blank languages fall back to English at render
33
+ * time, so a blank field shows the English text as its placeholder rather than looking empty.
34
+ *
35
+ * The value submitted is a JSON object keyed by language code. A language stays out of it until it is typed into, so
36
+ * browsing the languages does not pad the payload with empty translations.
37
+ *
38
+ * Put several of these in a `zn-translation-group` to have one select drive all of them.
39
+ *
40
+ * @dependency zn-chip
41
+ * @dependency zn-inline-edit
42
+ * @dependency zn-input
43
+ * @dependency zn-option
44
+ * @dependency zn-select
45
+ * @dependency zn-textarea
46
+ *
47
+ * @slot label - The field's label. Alternatively, use the `label` attribute.
48
+ * @slot help-text - Text describing how to fill the field in, shown below it and shared by every language.
49
+ * Alternatively, use the `help-text` attribute.
50
+ *
51
+ * @event zn-change - Emitted when a translation's value changes.
52
+ * @event zn-input - Emitted when a translation receives input.
53
+ *
54
+ * @csspart form-control - The form control that wraps the label, the language select, the field and the help text.
55
+ * @csspart form-control-label - The label's wrapper.
56
+ * @csspart form-control-input - The wrapper around the field being edited.
57
+ * @csspart form-control-help-text - The help text's wrapper.
58
+ * @csspart language-select - The select that chooses the language being edited.
59
+ */
25
60
  export default class ZnTranslations extends ZincElement implements ZincFormControl {
26
61
  static styles = [unsafeCSS(formControlStyles), unsafeCSS(styles)];
27
62
  static dependencies = {
28
- 'zn-button': ZnButton,
29
- 'zn-button-group': ZnButtonGroup,
30
- 'zn-dropdown': ZnDropdown,
63
+ 'zn-chip': ZnChip,
31
64
  'zn-inline-edit': ZnInlineEdit,
32
65
  'zn-input': ZnInput,
33
- 'zn-menu': ZnMenu
66
+ 'zn-option': ZnOption,
67
+ 'zn-select': ZnSelect,
68
+ 'zn-textarea': ZnTextarea
34
69
  };
35
70
 
36
71
  private readonly formControlController: FormControlController = new FormControlController(this);
37
- private readonly hasSlotController = new HasSlotController(this, 'label', 'expand', 'help-text');
72
+ private readonly hasSlotController = new HasSlotController(this, 'label', 'help-text');
38
73
 
74
+ /** The name submitted with the form. */
39
75
  @property() name = '';
76
+
77
+ /** The translations as a JSON object keyed by language code. The mirror of `values` in attribute form. */
40
78
  @property() value = '{"en":""}';
79
+
80
+ /** The label shown above the field. If you need HTML, use the `label` slot instead. */
41
81
  @property() label: string = '';
42
82
 
43
83
  /**
@@ -45,12 +85,28 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
45
85
  * `help-text` slot instead.
46
86
  */
47
87
  @property({attribute: 'help-text'}) helpText: string = '';
88
+
89
+ /** Disables editing in every language. */
48
90
  @property({type: Boolean, reflect: true}) disabled = false;
91
+
92
+ /** Marks the label required. Validity is not enforced per language. */
49
93
  @property({type: Boolean, reflect: true}) required = false;
94
+
95
+ /** Removes the component's own padding. */
50
96
  @property({type: Boolean, reflect: true}) flush = false;
51
- @property({attribute: "input-type"}) inputType: 'select' | 'text' | 'number' | 'textarea' = 'text';
97
+
98
+ /** The control each translation is edited through. */
99
+ @property({attribute: "input-type"}) inputType: 'text' | 'number' | 'textarea' = 'text';
100
+
101
+ /** Rows of the textarea, when `input-type` is `textarea`. */
52
102
  @property({attribute: "textarea-rows", type: Number}) textareaRows: number | undefined;
53
103
 
104
+ /**
105
+ * Edits the translation through a `zn-inline-edit` — the value reads as text until it is clicked — rather than a
106
+ * plain input or textarea.
107
+ */
108
+ @property({type: Boolean, reflect: true, attribute: 'inline-edit'}) inlineEdit = false;
109
+
54
110
  /**
55
111
  * Quick insertions offered by the slash menu on `text` and `textarea` inputs. Accepts a JSON array of items, or
56
112
  * the shorthand `Brand name={{BRAND_NAME}}, Support email={{SUPPORT_EMAIL}}`. Every language shares the list.
@@ -82,36 +138,27 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
82
138
  /** Resolves additional slash menu items each time the menu opens. JavaScript only. */
83
139
  @property({attribute: false}) slashItemsProvider?: (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
84
140
 
85
- /** When true, hides the individual language navbar and defers language control to a parent zn-translation-group. */
141
+ /**
142
+ * Hides this component's own language select and defers the choice to a parent zn-translation-group. The group sets
143
+ * this on its children itself.
144
+ */
86
145
  @property({type: Boolean, reflect: true}) grouped = false;
87
146
 
147
+ /**
148
+ * The languages on offer, as language code to display name — `{"en": "English", "fr": "French"}`. Writing the code
149
+ * as the name (`{"en": "EN"}`) is also accepted. `en` is the language every other one falls back to.
150
+ */
88
151
  @property({type: Object}) languages: Record<string, string> = {
89
152
  'en': 'EN'
90
153
  };
154
+
155
+ /** The translations as an object keyed by language code. The mirror of `value` in property form. */
91
156
  @property({type: Object}) values: Record<string, string> = {};
92
157
 
158
+ /** The serialized translations a form reset restores. Taken from the `value` attribute where there is one. */
159
+ @defaultValue() defaultValue = '{"en":""}';
160
+
93
161
  @state() private _activeLanguage = 'en';
94
- @state() private _overflowIndex = -1;
95
-
96
- private _lastObservedWidth = 0;
97
- private _measureRafId = 0;
98
-
99
- constructor() {
100
- super();
101
- // eslint-disable-next-line no-new
102
- new ResizeController(this, {
103
- callback: entries => {
104
- const width = entries[0]?.contentRect.width ?? 0;
105
- if (Math.abs(width - this._lastObservedWidth) < 1) return;
106
- this._lastObservedWidth = width;
107
- if (this._overflowIndex !== -1) {
108
- this._overflowIndex = -1;
109
- } else {
110
- this._scheduleLangOverflow();
111
- }
112
- },
113
- });
114
- }
115
162
 
116
163
  get validity(): ValidityState {
117
164
  return validValidityState;
@@ -137,9 +184,11 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
137
184
  // no-op
138
185
  }
139
186
 
140
- /** Sets the active language externally. Used by zn-translation-group. */
187
+ /**
188
+ * Sets the active language externally. Used by zn-translation-group. Browsing to a language does not create a key
189
+ * for it — an untouched language stays absent from `values` so it is not submitted as an empty translation.
190
+ */
141
191
  public setActiveLanguage(language: string) {
142
- this.addLanguageKey(language);
143
192
  this._activeLanguage = language;
144
193
  this.requestUpdate();
145
194
  }
@@ -163,6 +212,27 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
163
212
  return Object.keys(this.pendingValues());
164
213
  }
165
214
 
215
+ /** Whether the language carries a translation of its own, rather than falling back to English. */
216
+ public hasTranslation(language: string): boolean {
217
+ return (this.pendingValues()[language] ?? '').trim() !== '';
218
+ }
219
+
220
+ /** The chip shown against a language, in the select's value and against each of its options. */
221
+ private languageState(language: string): { type: 'success' | 'error'; label: string } {
222
+ if (this.hasTranslation(language)) return {type: 'success', label: 'Translated'};
223
+ return {type: 'error', label: language === 'en' ? 'Empty' : 'English'};
224
+ }
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
+ */
230
+ 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})`;
234
+ }
235
+
166
236
  /**
167
237
  * `values`, falling back to the `value` attribute it is built from while that is still pending. A parent
168
238
  * zn-translation-group syncs its children from its own first update, which runs before theirs, so reading
@@ -177,66 +247,13 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
177
247
  }
178
248
  }
179
249
 
180
- disconnectedCallback() {
181
- super.disconnectedCallback();
182
- if (this._measureRafId) {
183
- cancelAnimationFrame(this._measureRafId);
184
- this._measureRafId = 0;
185
- }
186
- }
187
-
188
250
  protected firstUpdated() {
189
- this.formControlController.updateValidity();
190
- }
191
-
192
- protected updated(changedProperties: PropertyValues) {
193
- super.updated(changedProperties);
194
- this._scheduleLangOverflow();
195
- }
196
-
197
- private _scheduleLangOverflow() {
198
- if (this._measureRafId) return;
199
- this._measureRafId = requestAnimationFrame(() => {
200
- this._measureRafId = 0;
201
- this._computeLangOverflow();
202
- });
203
- }
204
-
205
- private _computeLangOverflow() {
206
- const group = this.shadowRoot?.querySelector<HTMLElement>('.translations__language-group');
207
- if (!group) return;
208
-
209
- const buttons = Array.from(group.querySelectorAll<HTMLElement>('zn-button[data-lang-btn]'));
210
- if (buttons.length < 2) {
211
- if (this._overflowIndex !== -1) this._overflowIndex = -1;
212
- return;
213
- }
214
-
215
- const firstTop = buttons[0].getBoundingClientRect().top;
216
- let wrapAt = -1;
217
- for (let i = 1; i < buttons.length; i++) {
218
- if (buttons[i].getBoundingClientRect().top > firstTop + 1) {
219
- wrapAt = i;
220
- break;
221
- }
222
- }
223
-
224
- if (wrapAt === -1) {
225
- const trailing = group.querySelector<HTMLElement>('[data-lang-overflow], [data-lang-add]');
226
- if (trailing && trailing.getBoundingClientRect().top > firstTop + 1) {
227
- wrapAt = buttons.length;
228
- }
229
- }
230
-
231
- if (wrapAt === -1) return;
232
-
233
- const newIndex = this._overflowIndex === -1
234
- ? wrapAt
235
- : Math.max(1, Math.min(wrapAt, this._overflowIndex - 1));
236
-
237
- if (newIndex !== this._overflowIndex) {
238
- this._overflowIndex = newIndex;
251
+ // The decorator only follows the `value` attribute; a component given its translations through `values`, or
252
+ // through either property, would otherwise reset to an empty English translation.
253
+ if (!this.hasAttribute('value')) {
254
+ this.defaultValue = this.value;
239
255
  }
256
+ this.formControlController.updateValidity();
240
257
  }
241
258
 
242
259
  willUpdate(changedProperties: PropertyValues) {
@@ -276,7 +293,9 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
276
293
  // In grouped mode the parent zn-translation-group manages the active language,
277
294
  // so we must not override what it set via setActiveLanguage().
278
295
  if (!this.grouped) {
279
- if (!this._activeLanguage || (!Object.prototype.hasOwnProperty.call(this.values, this._activeLanguage) && this._activeLanguage !== 'en')) {
296
+ const isKnown = Object.prototype.hasOwnProperty.call(this.values, this._activeLanguage)
297
+ || Object.prototype.hasOwnProperty.call(this.languages, this._activeLanguage);
298
+ if (!this._activeLanguage || (!isKnown && this._activeLanguage !== 'en')) {
280
299
  const keys = Object.keys(this.values);
281
300
  if (keys.length > 0) {
282
301
  this._activeLanguage = keys[0];
@@ -288,34 +307,30 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
288
307
  }
289
308
  }
290
309
 
291
- private handleLanguageAdd = (e: ZnMenuSelectEvent) => {
310
+ /**
311
+ * The select's own change and input events are stopped here: they describe the language being browsed, not the
312
+ * translation being edited, and a consumer listening on zn-translations reads either as a value change.
313
+ */
314
+ private handleLanguageSelect = (e: Event) => {
292
315
  e.stopPropagation();
293
- const element = e.detail.element as HTMLElement;
294
- const languageCode = element.getAttribute('data-path');
295
- if (languageCode) {
296
- // Add new language with empty string
297
- this.values = {...this.values, [languageCode]: ''};
298
- this._activeLanguage = languageCode;
299
- this.updateValue();
316
+ const language = (e.target as ZnSelect).value;
317
+ if (typeof language === 'string' && language && language !== this._activeLanguage) {
318
+ this.switchLanguage(language);
300
319
  }
301
320
  };
302
321
 
303
- private handleOverflowSelect = (e: ZnMenuSelectEvent) => {
322
+ private handleLanguageInput = (e: Event) => {
304
323
  e.stopPropagation();
305
- const element = e.detail.element as HTMLElement;
306
- const languageCode = element.getAttribute('data-path');
307
- if (languageCode) {
308
- this.switchLanguage(languageCode);
309
- }
310
324
  };
311
325
 
326
+ /** The language shown in the field, without touching `values`. */
312
327
  private switchLanguage = (lang: string) => {
313
328
  this._activeLanguage = lang;
314
329
  this.requestUpdate();
315
330
  };
316
331
 
317
332
  private handleValueUpdate = (e: CustomEvent) => {
318
- const target = e.target as (ZnInput | ZnInlineEdit);
333
+ const target = e.target as (ZnInput | ZnInlineEdit | ZnTextarea);
319
334
  if (this._activeLanguage) {
320
335
  const newValue: string = target.value as string;
321
336
  if (newValue !== this.values[this._activeLanguage]) {
@@ -333,7 +348,8 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
333
348
 
334
349
  private handleKeyDown = (event: KeyboardEvent) => {
335
350
  if (event.key === 'Enter') {
336
- if (event.target instanceof ZnInlineEdit) {
351
+ // An inline edit commits on Enter itself, and a textarea needs it for the newline.
352
+ if (event.target instanceof ZnInlineEdit || event.target instanceof ZnTextarea) {
337
353
  return;
338
354
  }
339
355
 
@@ -349,45 +365,113 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
349
365
  this.formControlController.submit();
350
366
  };
351
367
 
368
+ /** The control the active language's translation is edited through. */
369
+ private renderField(value: string, placeholder: string, isRTL: boolean) {
370
+ const dir = isRTL ? 'rtl' : 'ltr';
371
+
372
+ if (this.inlineEdit) {
373
+ return html`
374
+ <zn-inline-edit
375
+ input-type=${this.inputType}
376
+ textarea-rows=${ifDefined(this.textareaRows)}
377
+ .value=${live(value)}
378
+ name="${this.name}"
379
+ placeholder="${placeholder}"
380
+ dir="${dir}"
381
+ ?disabled="${this.disabled}"
382
+ slash-trigger="${this.slashTrigger}"
383
+ slash-heading="${this.slashHeading}"
384
+ slash-preset="${this.slashPreset}"
385
+ slash-recent-key="${this.slashRecentKey}"
386
+ ?slash-hide-keys="${this.slashHideKeys}"
387
+ .slashItems="${this.slashItems}"
388
+ .slashItemsProvider="${this.slashItemsProvider}"
389
+ @zn-change="${this.handleValueUpdate}"
390
+ @zn-input="${this.handleValueUpdate}"
391
+ @zn-submit="${this.handleSubmit}"
392
+ ></zn-inline-edit>`;
393
+ }
394
+
395
+ if (this.inputType === 'textarea') {
396
+ return html`
397
+ <zn-textarea
398
+ rows=${ifDefined(this.textareaRows)}
399
+ resize="auto"
400
+ .value=${live(value)}
401
+ name="${this.name}"
402
+ placeholder="${placeholder}"
403
+ dir="${dir}"
404
+ ?disabled="${this.disabled}"
405
+ slash-trigger="${this.slashTrigger}"
406
+ slash-heading="${this.slashHeading}"
407
+ slash-preset="${this.slashPreset}"
408
+ slash-recent-key="${this.slashRecentKey}"
409
+ ?slash-hide-keys="${this.slashHideKeys}"
410
+ .slashItems="${this.slashItems}"
411
+ .slashItemsProvider="${this.slashItemsProvider}"
412
+ @zn-change="${this.handleValueUpdate}"
413
+ @zn-input="${this.handleValueUpdate}"
414
+ ></zn-textarea>`;
415
+ }
416
+
417
+ return html`
418
+ <zn-input
419
+ type="${this.inputType === 'number' ? 'number' : 'text'}"
420
+ clearable
421
+ .value=${live(value)}
422
+ name="${this.name}"
423
+ placeholder="${placeholder}"
424
+ dir="${dir}"
425
+ ?disabled="${this.disabled}"
426
+ slash-trigger="${this.slashTrigger}"
427
+ slash-heading="${this.slashHeading}"
428
+ slash-preset="${this.slashPreset}"
429
+ slash-recent-key="${this.slashRecentKey}"
430
+ ?slash-hide-keys="${this.slashHideKeys}"
431
+ .slashItems="${this.slashItems}"
432
+ .slashItemsProvider="${this.slashItemsProvider}"
433
+ @zn-change="${this.handleValueUpdate}"
434
+ @zn-input="${this.handleValueUpdate}"
435
+ ></zn-input>`;
436
+ }
437
+
438
+ /** Arabic and Hebrew read right to left, so the field's `dir` follows the language being edited. */
352
439
  private isRTLLanguage(languageCode: string): boolean {
353
- // Check if language is Arabic or Hebrew (including variants)
354
440
  return languageCode.startsWith('ar') || languageCode === 'he' || languageCode === 'iw';
355
441
  }
356
442
 
357
443
  render() {
358
- const availableLanguages = Object.entries(this.languages)
359
- .filter(([code]) => code !== 'en' && !Object.prototype.hasOwnProperty.call(this.values, code))
360
- .map(([code, name]) => ({
361
- title: name,
362
- type: 'dropdown',
363
- path: code
364
- }));
365
-
366
- const visibleTabs = Object.keys(this.values);
367
- if (!visibleTabs.includes('en')) {
368
- visibleTabs.unshift('en');
369
- }
370
-
371
- const overflowCutoff = this._overflowIndex === -1 ? visibleTabs.length : this._overflowIndex;
372
- const visibleLangTabs = visibleTabs.slice(0, overflowCutoff);
373
- const overflowLangTabs = visibleTabs.slice(overflowCutoff);
374
- const overflowActions = overflowLangTabs.map(code => ({
375
- title: code.toUpperCase(),
376
- type: 'dropdown',
377
- path: code,
378
- icon: code === this._activeLanguage ? 'check' : ''
379
- }));
444
+ // A value can carry a language `languages` does not list — server-rendered content outliving a config change.
445
+ // Offer those too, or the translation is stranded in the value with no way to reach it.
446
+ const values = this.pendingValues();
447
+ const languageCodes = [
448
+ ...Object.keys(this.languages),
449
+ ...Object.keys(values).filter(code => !Object.prototype.hasOwnProperty.call(this.languages, code))
450
+ ];
451
+ // Closed, the select answers "how much is left to do" rather than the state of the one language on show — that
452
+ // is what the options are for.
453
+ const targets = languageCodes.filter(code => code !== 'en');
454
+ const translated = targets.filter(code => this.hasTranslation(code)).length;
455
+ const summary = {
456
+ label: `${translated}/${targets.length}`,
457
+ type: translated === targets.length ? 'success' : translated > 0 ? 'warning' : 'error'
458
+ };
380
459
 
381
460
  const currentTranslation = this.values[this._activeLanguage] ?? '';
382
461
  const isRTL = this.isRTLLanguage(this._activeLanguage);
383
462
 
463
+ // A blank translation falls back to English at render time, so show that English text as the placeholder rather
464
+ // than leaving the field looking like it has nothing behind it.
465
+ const englishValue = this.pendingValues().en ?? '';
466
+ const placeholder = !currentTranslation && this._activeLanguage !== 'en' && englishValue
467
+ ? englishValue
468
+ : 'Enter translation...';
469
+
384
470
  const hasLabelSlot = this.hasSlotController.test('label');
385
471
  const hasLabel = this.label ? true : hasLabelSlot;
386
- const hasExpandSlot = this.hasSlotController.test('expand');
387
472
  const hasHelpTextSlot = this.hasSlotController.test('help-text');
388
473
  const hasHelpText = this.helpText ? true : hasHelpTextSlot;
389
- const hasMultipleLanguages = Object.keys(this.languages).length > 1;
390
- const showActions = !this.grouped && (hasMultipleLanguages || hasExpandSlot);
474
+ const showLanguageSelect = !this.grouped && languageCodes.length > 1;
391
475
 
392
476
  return html`
393
477
  <div part="form-control"
@@ -401,80 +485,36 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
401
485
  'form-control--has-help-text': hasHelpText,
402
486
  })}"
403
487
  @keydown="${this.handleKeyDown}">
404
- <div class="translations__header">
405
- <label part="form-control-label" class="form-control__label" for="input"
406
- aria-hidden=${hasLabel ? 'false' : 'true'}>
407
- <slot name="label">${this.label}</slot>
408
- </label>
409
- ${showActions ? html`
410
- <div class="translations__actions">
411
- ${hasExpandSlot ? html`<slot name="expand"></slot>` : nothing}
412
- ${hasMultipleLanguages ? html`
413
- <zn-button-group class="translations__language-group">
414
- ${visibleLangTabs.map(code => html`
415
- <zn-button
416
- data-lang-btn
417
- color="default"
418
- ?outline="${code !== this._activeLanguage}"
419
- @click="${() => this.switchLanguage(code)}"
420
- >${code.toUpperCase()}
421
- </zn-button>
422
- `)}
423
- ${overflowActions.length > 0 ? html`
424
- <zn-dropdown placement="bottom-end" data-lang-overflow>
425
- <zn-button
426
- slot="trigger"
427
- color="default"
428
- icon="keyboard_arrow_down"
429
- ?outline="${!overflowLangTabs.includes(this._activeLanguage)}"
430
- ></zn-button>
431
- <zn-menu
432
- .actions=${overflowActions}
433
- @zn-menu-select="${this.handleOverflowSelect}"
434
- ></zn-menu>
435
- </zn-dropdown>
436
- ` : nothing}
437
- ${availableLanguages.length > 0 ? html`
438
- <zn-dropdown placement="bottom-end" data-lang-add>
439
- <zn-button
440
- slot="trigger"
441
- color="default"
442
- outline
443
- >+
444
- </zn-button>
445
- <zn-menu
446
- .actions=${availableLanguages}
447
- @zn-menu-select="${this.handleLanguageAdd}"
448
- ></zn-menu>
449
- </zn-dropdown>
450
- ` : nothing}
451
- </zn-button-group>
452
- ` : nothing}
453
- </div>
454
- ` : nothing}
455
- </div>
488
+ <label part="form-control-label" class="form-control__label" for="input"
489
+ aria-hidden=${hasLabel ? 'false' : 'true'}>
490
+ <slot name="label">${this.label}</slot>
491
+ </label>
492
+
493
+ ${showLanguageSelect ? html`
494
+ <div class="translations__language">
495
+ <zn-select
496
+ class="translations__language-select"
497
+ part="language-select"
498
+ hoist
499
+ ?disabled="${this.disabled}"
500
+ .value="${this._activeLanguage}"
501
+ @zn-change="${this.handleLanguageSelect}"
502
+ @zn-input="${this.handleLanguageInput}">
503
+ <zn-chip slot="suffix" type="${summary.type}">${summary.label}</zn-chip>
504
+ ${languageCodes.map(code => {
505
+ const optionState = this.languageState(code);
506
+ return html`
507
+ <zn-option value="${code}">
508
+ ${this.languageLabel(code)}
509
+ <zn-chip slot="suffix" type="${optionState.type}">${optionState.label}</zn-chip>
510
+ </zn-option>`;
511
+ })}
512
+ </zn-select>
513
+ </div>
514
+ ` : nothing}
456
515
 
457
516
  <div part="form-control-input" class="translations__body">
458
- ${keyed(this._activeLanguage, html`
459
- <zn-inline-edit
460
- input-type=${this.inputType}
461
- textarea-rows=${ifDefined(this.textareaRows)}
462
- .value=${live(currentTranslation)}
463
- name="${this.name}"
464
- placeholder="Enter translation..."
465
- dir="${isRTL ? 'rtl' : 'ltr'}"
466
- slash-trigger="${this.slashTrigger}"
467
- slash-heading="${this.slashHeading}"
468
- slash-preset="${this.slashPreset}"
469
- slash-recent-key="${this.slashRecentKey}"
470
- ?slash-hide-keys="${this.slashHideKeys}"
471
- .slashItems="${this.slashItems}"
472
- .slashItemsProvider="${this.slashItemsProvider}"
473
- @zn-change="${this.handleValueUpdate}"
474
- @zn-input="${this.handleValueUpdate}"
475
- @zn-submit="${this.handleSubmit}"
476
- ></zn-inline-edit>
477
- `)}
517
+ ${keyed(this._activeLanguage, this.renderField(currentTranslation, placeholder, isRTL))}
478
518
  </div>
479
519
 
480
520
  ${hasHelpText ? html`
@@ -7,38 +7,16 @@
7
7
  padding: var(--zn-spacing-small) var(--zn-spacing-medium);
8
8
  }
9
9
 
10
- .translations__header {
11
- display: flex;
12
- align-items: center;
13
- justify-content: space-between;
14
- gap: var(--zn-spacing-small);
15
- margin-bottom: var(--zn-spacing-2x-small);
16
- min-height: var(--zn-input-height-small);
17
- }
18
-
19
- .translations__actions {
20
- display: flex;
21
- align-items: center;
22
- gap: var(--zn-spacing-x-small);
23
- margin-left: auto;
10
+ // The select and the field below it are two stacked controls, so they take the same gap zn-form-group uses.
11
+ .translations__language {
12
+ margin-bottom: var(--zn-spacing-medium);
24
13
  }
25
14
 
26
15
  .translations__body {
27
16
  display: block;
28
17
  }
29
18
 
30
- .form-control__label {
31
- margin-bottom: 0 !important;
32
- white-space: nowrap;
33
- }
34
-
35
19
  .translations--flush,
36
20
  .translations--grouped {
37
21
  padding: 0;
38
22
  }
39
-
40
- .translations--grouped {
41
- .translations__header {
42
- margin-bottom: 0;
43
- }
44
- }