@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.
- package/dist/custom-elements.json +411 -175
- package/dist/vscode.html-custom-data.json +71 -26
- package/dist/web-types.json +171 -51
- package/dist/zn.d.ts +182 -48
- package/dist/zn.min.js +581 -588
- package/docs/pages/components/dialog.md +45 -16
- package/docs/pages/components/translation-group.md +102 -93
- package/docs/pages/components/translations.md +64 -114
- package/package.json +1 -1
- package/src/components/dialog/dialog.component.ts +2 -1
- package/src/components/dialog/dialog.scss +8 -0
- package/src/components/header/header.component.ts +2 -1
- package/src/components/select/select.component.ts +5 -0
- package/src/components/select/select.scss +11 -0
- package/src/components/translation-group/translation-group.component.ts +171 -198
- package/src/components/translation-group/translation-group.scss +124 -1
- package/src/components/translation-group/translation-group.test.ts +371 -2
- package/src/components/translations/translations.component.ts +249 -209
- package/src/components/translations/translations.scss +3 -25
- package/src/components/translations/translations.test.ts +214 -10
|
@@ -3,86 +3,111 @@ import {type CSSResultGroup, html, nothing, type PropertyValues, unsafeCSS} from
|
|
|
3
3
|
import {HasSlotController} from '../../internal/slot';
|
|
4
4
|
import {ifDefined} from 'lit/directives/if-defined.js';
|
|
5
5
|
import {property, state} from 'lit/decorators.js';
|
|
6
|
-
import
|
|
7
|
-
import ZnButton from '../button';
|
|
8
|
-
import ZnButtonGroup from '../button-group';
|
|
9
|
-
import ZnDropdown from '../dropdown';
|
|
6
|
+
import ZnChip from '../chip';
|
|
10
7
|
import ZnHeader from '../header';
|
|
11
|
-
import
|
|
8
|
+
import ZnOption from '../option';
|
|
12
9
|
import ZnPanel from '../panel/panel.component';
|
|
13
|
-
import
|
|
10
|
+
import ZnSelect from '../select';
|
|
14
11
|
import type ZnTranslations from '../translations/translations.component';
|
|
15
12
|
|
|
16
13
|
import styles from './translation-group.scss';
|
|
17
14
|
|
|
18
15
|
/**
|
|
19
|
-
* @summary
|
|
16
|
+
* @summary Puts several zn-translations fields behind one language select, so a whole form's worth of copy is
|
|
17
|
+
* translated a language at a time.
|
|
18
|
+
* @documentation https://zinc.style/components/translation-group
|
|
19
|
+
* @status experimental
|
|
20
|
+
* @since 1.0
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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.
|
|
24
|
+
*
|
|
25
|
+
* Closed, the select carries how many target languages are done — `1/5`. Its options each carry a chip aggregated
|
|
26
|
+
* across the children:
|
|
27
|
+
*
|
|
28
|
+
* - `Translated` — every child has a value for it
|
|
29
|
+
* - `Partial` — only some children do
|
|
30
|
+
* - `English` — none do, so all of them fall back to the English text
|
|
31
|
+
*
|
|
32
|
+
* `Empty` replaces the last of those for English itself, which has nothing to fall back to. English is the source
|
|
33
|
+
* rather than a translation, so it is also left out of the `n of m translated` count beside the label.
|
|
34
|
+
*
|
|
35
|
+
* The children own their values; this component only chooses which language is shown and reports on what they hold.
|
|
36
|
+
* It reads them back on every child `zn-change`, so the chips and the count follow an edit as it is typed.
|
|
37
|
+
*
|
|
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.
|
|
40
|
+
*
|
|
41
|
+
* @dependency zn-chip
|
|
42
|
+
* @dependency zn-header
|
|
43
|
+
* @dependency zn-option
|
|
44
|
+
* @dependency zn-select
|
|
25
45
|
*
|
|
26
46
|
* @event zn-language-change - Emitted when the active language changes. Detail: `{ language: string }`.
|
|
27
47
|
*
|
|
28
|
-
* @slot -
|
|
29
|
-
* @slot actions -
|
|
48
|
+
* @slot - The `zn-translations` fields the select drives.
|
|
49
|
+
* @slot actions - Buttons for the bottom of the panel, on the white body rather than the grey footer. They sit on
|
|
50
|
+
* the right, as zinc's form action rows do; `align="start"` moves one to the left. Write them in the order they
|
|
51
|
+
* 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.
|
|
30
54
|
*
|
|
31
55
|
* @csspart base - The component's base wrapper.
|
|
56
|
+
* @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.
|
|
58
|
+
* @csspart language-select - The select itself.
|
|
32
59
|
*/
|
|
33
60
|
export default class ZnTranslationGroup extends ZnPanel {
|
|
34
61
|
static styles: CSSResultGroup = [ZnPanel.styles, unsafeCSS(styles)];
|
|
35
62
|
static dependencies = {
|
|
36
|
-
'zn-
|
|
37
|
-
'zn-button-group': ZnButtonGroup,
|
|
38
|
-
'zn-dropdown': ZnDropdown,
|
|
63
|
+
'zn-chip': ZnChip,
|
|
39
64
|
'zn-header': ZnHeader,
|
|
40
|
-
'zn-
|
|
65
|
+
'zn-option': ZnOption,
|
|
66
|
+
'zn-select': ZnSelect
|
|
41
67
|
};
|
|
42
68
|
|
|
43
69
|
private readonly _slotController = new HasSlotController(this, 'actions', 'footer');
|
|
44
70
|
|
|
45
|
-
/** The
|
|
71
|
+
/** The caption shown in the panel header. An alias for the inherited `caption`, which wins where both are set. */
|
|
46
72
|
@property() label = '';
|
|
47
73
|
|
|
48
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Drops the panel chrome — border, background and padding — so the group reads as a section of the form around it
|
|
76
|
+
* rather than a panel of its own. For groups nested inside another panel, where the fields would otherwise sit
|
|
77
|
+
* indented behind a second border.
|
|
78
|
+
*/
|
|
79
|
+
@property({type: Boolean, reflect: true}) inline = false;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The select's accessible name. Not shown — the caption is what names the section on screen — but read out by a
|
|
83
|
+
* screen reader, which has nothing else to go on once the visible label is gone.
|
|
84
|
+
*/
|
|
85
|
+
@property({attribute: 'language-label'}) languageLabel = 'Edit Languages';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The languages on offer, as language code to display name — `{"en": "English", "fr": "French"}`. Writing the code
|
|
89
|
+
* as the name (`{"en": "EN"}`) is also accepted. `en` is the language every other one falls back to. Set on every
|
|
90
|
+
* child, so they do not need their own copy.
|
|
91
|
+
*/
|
|
49
92
|
@property({type: Object}) languages: Record<string, string> = {
|
|
50
93
|
'en': 'EN'
|
|
51
94
|
};
|
|
52
95
|
|
|
96
|
+
/** The language every child is currently editing. */
|
|
53
97
|
@state() private _activeLanguage = 'en';
|
|
54
98
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
private _measureRafId = 0;
|
|
62
|
-
|
|
63
|
-
constructor() {
|
|
64
|
-
super();
|
|
65
|
-
// eslint-disable-next-line no-new
|
|
66
|
-
new ResizeController(this, {
|
|
67
|
-
callback: entries => {
|
|
68
|
-
const width = entries[0]?.contentRect.width ?? 0;
|
|
69
|
-
if (Math.abs(width - this._lastObservedWidth) < 1) return;
|
|
70
|
-
this._lastObservedWidth = width;
|
|
71
|
-
if (this._overflowIndex !== -1) {
|
|
72
|
-
this._overflowIndex = -1;
|
|
73
|
-
} else {
|
|
74
|
-
this._scheduleLangOverflow();
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
});
|
|
99
|
+
private _form: HTMLFormElement | null = null;
|
|
100
|
+
|
|
101
|
+
connectedCallback() {
|
|
102
|
+
super.connectedCallback();
|
|
103
|
+
this._form = this.closest('form');
|
|
104
|
+
this._form?.addEventListener('reset', this.handleFormReset);
|
|
78
105
|
}
|
|
79
106
|
|
|
80
107
|
disconnectedCallback() {
|
|
81
108
|
super.disconnectedCallback();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this._measureRafId = 0;
|
|
85
|
-
}
|
|
109
|
+
this._form?.removeEventListener('reset', this.handleFormReset);
|
|
110
|
+
this._form = null;
|
|
86
111
|
}
|
|
87
112
|
|
|
88
113
|
protected firstUpdated(_changedProperties: PropertyValues) {
|
|
@@ -95,81 +120,43 @@ export default class ZnTranslationGroup extends ZnPanel {
|
|
|
95
120
|
if (changedProperties.has('languages')) {
|
|
96
121
|
this.syncChildLanguages();
|
|
97
122
|
}
|
|
98
|
-
this._scheduleLangOverflow();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private _scheduleLangOverflow() {
|
|
102
|
-
if (this._measureRafId) return;
|
|
103
|
-
this._measureRafId = requestAnimationFrame(() => {
|
|
104
|
-
this._measureRafId = 0;
|
|
105
|
-
this._computeLangOverflow();
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private _computeLangOverflow() {
|
|
110
|
-
const group = this.shadowRoot?.querySelector<HTMLElement>('.translation-group__languages zn-button-group');
|
|
111
|
-
if (!group) return;
|
|
112
|
-
|
|
113
|
-
const buttons = Array.from(group.querySelectorAll<HTMLElement>('zn-button[data-lang-btn]'));
|
|
114
|
-
if (buttons.length < 2) {
|
|
115
|
-
if (this._overflowIndex !== -1) this._overflowIndex = -1;
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const firstTop = buttons[0].getBoundingClientRect().top;
|
|
120
|
-
let wrapAt = -1;
|
|
121
|
-
for (let i = 1; i < buttons.length; i++) {
|
|
122
|
-
if (buttons[i].getBoundingClientRect().top > firstTop + 1) {
|
|
123
|
-
wrapAt = i;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (wrapAt === -1) {
|
|
129
|
-
const trailing = group.querySelector<HTMLElement>('[data-lang-overflow], [data-lang-add]');
|
|
130
|
-
if (trailing && trailing.getBoundingClientRect().top > firstTop + 1) {
|
|
131
|
-
wrapAt = buttons.length;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (wrapAt === -1) return;
|
|
136
|
-
|
|
137
|
-
const newIndex = this._overflowIndex === -1
|
|
138
|
-
? wrapAt
|
|
139
|
-
: Math.max(1, Math.min(wrapAt, this._overflowIndex - 1));
|
|
140
|
-
|
|
141
|
-
if (newIndex !== this._overflowIndex) {
|
|
142
|
-
this._overflowIndex = newIndex;
|
|
143
|
-
}
|
|
144
123
|
}
|
|
145
124
|
|
|
125
|
+
/** The children the select drives. Read live rather than cached, so markup added later is picked up. */
|
|
146
126
|
private getAllTranslations(): ZnTranslations[] {
|
|
147
127
|
return [...this.querySelectorAll<ZnTranslations>('zn-translations')];
|
|
148
128
|
}
|
|
149
129
|
|
|
150
130
|
/** Sync grouped state, languages, and active language to all children. */
|
|
151
131
|
private syncChildren() {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
// Collect all language codes from children's existing values
|
|
155
|
-
const allLanguageCodes = new Set<string>(this._activatedLanguages);
|
|
156
|
-
children.forEach(child => {
|
|
157
|
-
child.getValueLanguages().forEach(code => allLanguageCodes.add(code));
|
|
158
|
-
});
|
|
159
|
-
this._activatedLanguages = [...allLanguageCodes];
|
|
160
|
-
|
|
161
|
-
children.forEach(child => {
|
|
132
|
+
this.getAllTranslations().forEach(child => {
|
|
162
133
|
child.grouped = true;
|
|
163
134
|
child.languages = this.languages;
|
|
164
|
-
// Ensure every activated language exists in each child's values.
|
|
165
|
-
// This handles the case where one child has a language that another doesn't.
|
|
166
|
-
for (const code of this._activatedLanguages) {
|
|
167
|
-
child.addLanguageKey(code);
|
|
168
|
-
}
|
|
169
135
|
child.setActiveLanguage(this._activeLanguage);
|
|
170
136
|
});
|
|
171
137
|
}
|
|
172
138
|
|
|
139
|
+
/**
|
|
140
|
+
* A language is translated once every child carries a value for it, partial while only some do. The chips and the
|
|
141
|
+
* count are read off the children, so a child's edit has to bring the group back round.
|
|
142
|
+
*/
|
|
143
|
+
private languageState(language: string): { type: 'success' | 'warning' | 'error'; label: string } {
|
|
144
|
+
const children = this.getAllTranslations();
|
|
145
|
+
const translated = children.filter(child => child.hasTranslation(language)).length;
|
|
146
|
+
|
|
147
|
+
if (children.length > 0 && translated === children.length) return {type: 'success', label: 'Translated'};
|
|
148
|
+
if (translated > 0) return {type: 'warning', label: 'Partial'};
|
|
149
|
+
return {type: 'error', label: language === 'en' ? 'Empty' : 'English'};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `English (EN)`, or the code alone where the configured name already is the code. */
|
|
153
|
+
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})`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Children take their language list from the group, so a change to `languages` has to reach them. */
|
|
173
160
|
private syncChildLanguages() {
|
|
174
161
|
this.getAllTranslations().forEach(child => {
|
|
175
162
|
child.languages = this.languages;
|
|
@@ -180,74 +167,75 @@ export default class ZnTranslationGroup extends ZnPanel {
|
|
|
180
167
|
this.syncChildren();
|
|
181
168
|
};
|
|
182
169
|
|
|
170
|
+
/** Moves every child onto `lang` and announces it. Does not touch their values. */
|
|
183
171
|
private switchLanguage(lang: string) {
|
|
184
172
|
this._activeLanguage = lang;
|
|
185
173
|
this.getAllTranslations().forEach(child => child.setActiveLanguage(lang));
|
|
186
174
|
this.emit('zn-language-change', {detail: {language: lang}});
|
|
187
175
|
}
|
|
188
176
|
|
|
189
|
-
|
|
177
|
+
/**
|
|
178
|
+
* The select's own change and input events describe the language being browsed, not a translation being edited, so
|
|
179
|
+
* they are stopped rather than allowed to reach a consumer listening for a child's value change.
|
|
180
|
+
*/
|
|
181
|
+
private handleLanguageSelect = (e: Event) => {
|
|
190
182
|
e.stopPropagation();
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!this._activatedLanguages.includes(languageCode)) {
|
|
195
|
-
this._activatedLanguages = [...this._activatedLanguages, languageCode];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Add language key to all children first so the key exists in values
|
|
199
|
-
this.getAllTranslations().forEach(child => child.addLanguageKey(languageCode));
|
|
200
|
-
this.switchLanguage(languageCode);
|
|
183
|
+
const language = (e.target as ZnSelect).value;
|
|
184
|
+
if (typeof language === 'string' && language && language !== this._activeLanguage) {
|
|
185
|
+
this.switchLanguage(language);
|
|
201
186
|
}
|
|
202
187
|
};
|
|
203
188
|
|
|
204
|
-
private
|
|
189
|
+
private handleLanguageInput = (e: Event) => {
|
|
205
190
|
e.stopPropagation();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** A child's edit changes which chips the select shows, and the translated count above it. */
|
|
194
|
+
private handleChildChange = () => {
|
|
195
|
+
this.requestUpdate();
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The children restore their own values on the form's reset event without announcing it, and the chips and the
|
|
200
|
+
* count are read off them — so re-read once every listener on that event has run.
|
|
201
|
+
*/
|
|
202
|
+
private handleFormReset = () => {
|
|
203
|
+
requestAnimationFrame(() => this.requestUpdate());
|
|
211
204
|
};
|
|
212
205
|
|
|
213
206
|
render() {
|
|
214
|
-
const
|
|
207
|
+
const hasActionsSlot = this._slotController.test('actions');
|
|
215
208
|
const hasFooterSlot = this._slotController.test('footer');
|
|
216
209
|
const headerCaption = this.caption || this.label;
|
|
217
210
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
path: code,
|
|
238
|
-
icon: code === this._activeLanguage ? 'check' : ''
|
|
239
|
-
}));
|
|
240
|
-
|
|
241
|
-
const hasMultipleLanguages = Object.keys(this.languages).length > 1;
|
|
242
|
-
const hasHeader = headerCaption || hasMultipleLanguages || hasActionSlot;
|
|
211
|
+
// A child's value can carry a language `languages` does not list — server-rendered content outliving a config
|
|
212
|
+
// change. Offer those too, or the translation is stranded in the value with no way to reach it.
|
|
213
|
+
const extra = new Set<string>();
|
|
214
|
+
this.getAllTranslations().forEach(child => child.getValueLanguages()
|
|
215
|
+
.filter(code => !Object.prototype.hasOwnProperty.call(this.languages, code))
|
|
216
|
+
.forEach(code => extra.add(code)));
|
|
217
|
+
const languageCodes = [...Object.keys(this.languages), ...extra];
|
|
218
|
+
const hasMultipleLanguages = languageCodes.length > 1;
|
|
219
|
+
const hasHeader = Boolean(headerCaption) || hasMultipleLanguages;
|
|
220
|
+
|
|
221
|
+
// English is the source every other language falls back to, so it is not itself one of the translations counted.
|
|
222
|
+
const targets = languageCodes.filter(code => code !== 'en');
|
|
223
|
+
const translated = targets.filter(code => this.languageState(code).type === 'success').length;
|
|
224
|
+
// Closed, the select answers "how much is left to do" rather than the state of the one language on show — that
|
|
225
|
+
// is what the options are for.
|
|
226
|
+
const summary = {
|
|
227
|
+
label: `${translated}/${targets.length}`,
|
|
228
|
+
type: translated === targets.length ? 'success' : translated > 0 ? 'warning' : 'error'
|
|
229
|
+
};
|
|
243
230
|
|
|
244
231
|
return html`
|
|
245
232
|
<div class="${classMap({
|
|
246
233
|
panel: true,
|
|
247
|
-
'panel--flush': this.flush,
|
|
248
|
-
'panel--transparent': this.transparent,
|
|
234
|
+
'panel--flush': this.flush || this.inline,
|
|
235
|
+
'panel--transparent': this.transparent || this.inline,
|
|
236
|
+
'translation-group--inline': this.inline,
|
|
249
237
|
'panel--has-header': hasHeader,
|
|
250
|
-
'panel--has-actions':
|
|
238
|
+
'panel--has-actions': hasActionsSlot,
|
|
251
239
|
'panel--has-footer': hasFooterSlot,
|
|
252
240
|
})}">
|
|
253
241
|
|
|
@@ -257,55 +245,40 @@ export default class ZnTranslationGroup extends ZnPanel {
|
|
|
257
245
|
caption="${ifDefined(headerCaption || undefined)}"
|
|
258
246
|
transparent>
|
|
259
247
|
${hasMultipleLanguages ? html`
|
|
260
|
-
<div slot="actions" class="translation-
|
|
261
|
-
<zn-
|
|
262
|
-
${
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
<zn-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
.actions=${overflowActions}
|
|
281
|
-
@zn-menu-select="${this.handleOverflowSelect}"
|
|
282
|
-
></zn-menu>
|
|
283
|
-
</zn-dropdown>
|
|
284
|
-
` : nothing}
|
|
285
|
-
${availableLanguages.length > 0 ? html`
|
|
286
|
-
<zn-dropdown placement="bottom-end" data-lang-add>
|
|
287
|
-
<zn-button
|
|
288
|
-
slot="trigger"
|
|
289
|
-
color="default"
|
|
290
|
-
outline
|
|
291
|
-
>+
|
|
292
|
-
</zn-button>
|
|
293
|
-
<zn-menu
|
|
294
|
-
.actions=${availableLanguages}
|
|
295
|
-
@zn-menu-select="${this.handleLanguageAdd}"
|
|
296
|
-
></zn-menu>
|
|
297
|
-
</zn-dropdown>
|
|
298
|
-
` : nothing}
|
|
299
|
-
</zn-button-group>
|
|
300
|
-
</div>
|
|
301
|
-
` : nothing}
|
|
302
|
-
${hasActionSlot ? html`
|
|
303
|
-
<slot name="actions" slot="actions"></slot>` : null}
|
|
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}
|
|
304
268
|
</zn-header>` : null}
|
|
305
269
|
|
|
306
270
|
<div class="panel__content">
|
|
307
271
|
<div class="panel__body">
|
|
308
|
-
<slot
|
|
272
|
+
<slot
|
|
273
|
+
@slotchange="${this.handleSlotChange}"
|
|
274
|
+
@zn-change="${this.handleChildChange}"></slot>
|
|
275
|
+
|
|
276
|
+
${hasActionsSlot ? html`
|
|
277
|
+
<div class="translation-group__actions" part="actions">
|
|
278
|
+
<slot name="actions"></slot>
|
|
279
|
+
<span class="translation-group__actions-spacer"></span>
|
|
280
|
+
</div>` : nothing}
|
|
281
|
+
|
|
309
282
|
</div>
|
|
310
283
|
</div>
|
|
311
284
|
|
|
@@ -2,7 +2,130 @@
|
|
|
2
2
|
display: block;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
7
|
+
.panel__body {
|
|
8
|
+
gap: var(--zn-spacing-medium);
|
|
9
|
+
}
|
|
10
|
+
|
|
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);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The select's label is its accessible name only; the caption names the section on screen.
|
|
83
|
+
.translation-group__language-select::part(form-control-label) {
|
|
84
|
+
position: absolute;
|
|
85
|
+
width: 1px;
|
|
86
|
+
height: 1px;
|
|
87
|
+
padding: 0;
|
|
88
|
+
margin: -1px;
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
clip-path: inset(50%);
|
|
91
|
+
white-space: nowrap;
|
|
92
|
+
border: 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Matches zinc's form action rows (.form-actions, zn-form-actions): right-aligned, 16px apart.
|
|
96
|
+
.translation-group__actions {
|
|
6
97
|
display: flex;
|
|
7
98
|
align-items: center;
|
|
99
|
+
gap: var(--zn-spacing-small);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The spacer, not an auto margin, is what splits the two sides: an auto margin on every right-hand button would open
|
|
103
|
+
// a gap between each of them, where one flexing element in the middle holds any number on either side together.
|
|
104
|
+
.translation-group__actions-spacer {
|
|
105
|
+
order: 1;
|
|
106
|
+
flex: 1 1 auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
::slotted([slot="actions"]) {
|
|
110
|
+
order: 2;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
::slotted([slot="actions"][align="start"]) {
|
|
114
|
+
order: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
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.
|
|
119
|
+
.panel--has-actions .panel__body > .translation-group__actions {
|
|
120
|
+
border: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The panel clips its content to its rounded corners, and the body scrolls. Neither applies with the chrome gone, and
|
|
124
|
+
// both would cut the focus ring off a field now that no padding stands between it and the body's edge.
|
|
125
|
+
.translation-group--inline {
|
|
126
|
+
.panel__inner,
|
|
127
|
+
.panel__content,
|
|
128
|
+
.panel__body {
|
|
129
|
+
overflow: visible;
|
|
130
|
+
}
|
|
8
131
|
}
|