@kubex/zinc 1.0.19 → 1.0.20

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.
@@ -146,6 +146,22 @@ ul.navbar {
146
146
 
147
147
  }
148
148
 
149
+ .navbar__container--stacked {
150
+ display: inline;
151
+ }
152
+
153
+ ul.navbar.navbar--stacked.navbar--icon-bar {
154
+ flex-direction: column;
155
+ padding: 0;
156
+ gap: 0;
157
+
158
+ li {
159
+ border-radius: 0;
160
+ align-items: flex-start;
161
+ padding: 8px;
162
+ }
163
+ }
164
+
149
165
 
150
166
  :host(:not([stacked]):not([icon-bar])) {
151
167
  ul.navbar {
@@ -236,7 +252,7 @@ ul.navbar {
236
252
  height: 2px;
237
253
  }
238
254
 
239
- &:hover:before {
255
+ &:hover:before:not(#dropdown-item) {
240
256
  // TODO Hover Before colors
241
257
  left: -2px;
242
258
  right: -2px;
@@ -254,7 +254,12 @@ export default class ZnTabs extends ZincElement {
254
254
  // ts-ignore
255
255
  const target = (event.relatedTarget ?? event.target) as HTMLElement;
256
256
  if (target) {
257
- this.clickTab(target, event.altKey);
257
+ if ('startViewTransition' in document) {
258
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
+ (document as any).startViewTransition(() => this.clickTab(target, event.altKey));
260
+ } else {
261
+ this.clickTab(target, event.altKey)
262
+ }
258
263
  }
259
264
  }
260
265
 
@@ -333,8 +338,9 @@ export default class ZnTabs extends ZincElement {
333
338
  return false;
334
339
  }
335
340
 
341
+ const sameTab = this._current === tabName;
336
342
  // Multi click on an active tab will refresh the tab
337
- if (this._current === tabName) {
343
+ if (sameTab) {
338
344
  this._activeClicks++;
339
345
  if (this._activeClicks > 2) {
340
346
  refresh = true;
@@ -345,32 +351,35 @@ export default class ZnTabs extends ZincElement {
345
351
  }
346
352
 
347
353
  let inSlot = true;
348
- this._panels.forEach((elements, key) => {
349
- const isActive = key === tabName;
350
- elements.forEach((element) => {
351
- if (isActive && element.parentNode !== this) {
352
- inSlot = false;
353
- }
354
- element.toggleAttribute('selected', isActive);
355
- if (isActive && refresh) {
356
- let uri = "";
357
- let gaid = "";
358
- if (this._activeTab && this._activeTab.hasAttribute('tab-uri')) {
359
- uri = this._activeTab.getAttribute('tab-uri')!;
360
- gaid = this._activeTab.getAttribute('gaid')!;
354
+ const updateTabs = () => {
355
+ this._panels.forEach((elements, key) => {
356
+ const isActive = key === tabName;
357
+ elements.forEach((element) => {
358
+ if (isActive && element.parentNode !== this) {
359
+ inSlot = false;
361
360
  }
362
- document.dispatchEvent(new CustomEvent('zn-refresh-element', {
363
- detail: {element: element, uri: uri, gaid: gaid}
364
- }));
365
- }
361
+ element.toggleAttribute('selected', isActive);
362
+ if (isActive && refresh) {
363
+ let uri = "";
364
+ let gaid = "";
365
+ if (this._activeTab && this._activeTab.hasAttribute('tab-uri')) {
366
+ uri = this._activeTab.getAttribute('tab-uri')!;
367
+ gaid = this._activeTab.getAttribute('gaid')!;
368
+ }
369
+ document.dispatchEvent(new CustomEvent('zn-refresh-element', {
370
+ detail: {element: element, uri: uri, gaid: gaid}
371
+ }));
372
+ }
373
+ });
366
374
  });
367
- });
368
375
 
369
- if (this._panel) {
370
- this._panel.classList.toggle('contents-slot', !inSlot);
371
- }
376
+ if (this._panel) {
377
+ this._panel.classList.toggle('contents-slot', !inSlot);
378
+ }
379
+ this._current = tabName;
380
+ };
372
381
 
373
- this._current = tabName;
382
+ updateTabs();
374
383
 
375
384
  return true;
376
385
  }
@@ -0,0 +1,6 @@
1
+ import ZnTranslations from './translations.component';
2
+
3
+ export * from './translations.component';
4
+ export default ZnTranslations;
5
+
6
+ ZnTranslations.define('zn-translations');
@@ -0,0 +1,214 @@
1
+ import {classMap} from 'lit/directives/class-map.js';
2
+ import {FormControlController, validValidityState} from '../../internal/form';
3
+ import {HasSlotController} from '../../internal/slot';
4
+ import {html, unsafeCSS} from 'lit';
5
+ import {property, state} from 'lit/decorators.js';
6
+ import ZincElement from '../../internal/zinc-element';
7
+ import ZnInlineEdit from '../inline-edit';
8
+ import ZnInput from '../input';
9
+ import ZnNavbar from '../navbar';
10
+ import type {PropertyValues} from 'lit';
11
+ import type {ZincFormControl} from '../../internal/zinc-element';
12
+ import type {ZnMenuSelectEvent} from '../../events/zn-menu-select';
13
+
14
+ import styles from './translations.scss';
15
+
16
+ export default class ZnTranslations extends ZincElement implements ZincFormControl {
17
+ static styles = unsafeCSS(styles);
18
+ static dependencies = {
19
+ 'zn-navbar': ZnNavbar,
20
+ 'zn-input': ZnInput,
21
+ 'zn-inline-edit': ZnInlineEdit
22
+ };
23
+
24
+ private readonly formControlController: FormControlController = new FormControlController(this);
25
+ private readonly hasSlotController = new HasSlotController(this, 'label');
26
+
27
+ @property() name = '';
28
+ @property() value = '{"en":""}';
29
+ @property() label: string = '';
30
+ @property({type: Boolean, reflect: true}) disabled = false;
31
+ @property({type: Boolean, reflect: true}) required = false;
32
+
33
+ @property({type: Object}) languages: Record<string, string> = {
34
+ 'en': 'English'
35
+ };
36
+ @property({type: Object}) values: Record<string, string> = {};
37
+
38
+ @state() private _activeLanguage = '';
39
+
40
+ get validity(): ValidityState {
41
+ return validValidityState;
42
+ }
43
+
44
+ get validationMessage() {
45
+ return '';
46
+ }
47
+
48
+ checkValidity() {
49
+ return true;
50
+ }
51
+
52
+ getForm() {
53
+ return this.formControlController.getForm();
54
+ }
55
+
56
+ reportValidity() {
57
+ return true;
58
+ }
59
+
60
+ setCustomValidity() {
61
+ // no-op
62
+ }
63
+
64
+ protected firstUpdated() {
65
+ this.formControlController.updateValidity();
66
+ }
67
+
68
+ willUpdate(changedProperties: PropertyValues) {
69
+ let processValue = changedProperties.has('value');
70
+ let processValues = changedProperties.has('values');
71
+
72
+ if (processValue && processValues) {
73
+ const isValueDefault = this.value === '{"en":""}';
74
+ const isValuesEmpty = Object.keys(this.values).length === 0;
75
+
76
+ if (isValueDefault && !isValuesEmpty) {
77
+ processValue = false;
78
+ } else if (!isValueDefault && isValuesEmpty) {
79
+ processValues = false;
80
+ } else if (this.hasAttribute('values') && !this.hasAttribute('value')) {
81
+ processValue = false;
82
+ } else if (this.hasAttribute('value') && !this.hasAttribute('values')) {
83
+ processValues = false;
84
+ }
85
+ }
86
+
87
+ if (processValue) {
88
+ try {
89
+ const newValues = JSON.parse(this.value || '{}') as Record<string, string>;
90
+ if (JSON.stringify(newValues) !== JSON.stringify(this.values)) {
91
+ this.values = newValues;
92
+ }
93
+ } catch (e) {
94
+ // no-op
95
+ }
96
+ }
97
+
98
+ if (processValues) {
99
+ this.value = JSON.stringify(this.values);
100
+
101
+ // Ensure active language is valid
102
+ if (!this._activeLanguage || !Object.prototype.hasOwnProperty.call(this.values, this._activeLanguage)) {
103
+ const keys = Object.keys(this.values);
104
+ if (keys.length > 0) {
105
+ this._activeLanguage = keys[0];
106
+ } else {
107
+ this._activeLanguage = '';
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ private handleLanguageAdd(e: ZnMenuSelectEvent) {
114
+ const element = e.detail.element as HTMLElement;
115
+ const languageCode = element.getAttribute('data-path');
116
+ if (languageCode) {
117
+ // Add new language with empty string
118
+ this.values = {...this.values, [languageCode]: ''};
119
+ this._activeLanguage = languageCode;
120
+ this.updateValue();
121
+ }
122
+ }
123
+
124
+ private handleNavbarClick(e: MouseEvent) {
125
+ const path = e.composedPath();
126
+ const li = path.find(el => el instanceof HTMLElement && el.tagName === 'LI' && el.hasAttribute('tab')) as HTMLElement;
127
+ if (li) {
128
+ const tab = li.getAttribute('tab');
129
+ if (tab) {
130
+ this._activeLanguage = tab;
131
+ this.requestUpdate();
132
+ }
133
+ }
134
+ }
135
+
136
+ private handleValueUpdate(e: CustomEvent) {
137
+ const target = e.target as (ZnInput | ZnInlineEdit);
138
+ if (this._activeLanguage) {
139
+ const newValue: string = target.value as string;
140
+ if (newValue !== this.values[this._activeLanguage]) {
141
+ this.values = {...this.values, [this._activeLanguage]: newValue};
142
+ this.updateValue();
143
+ }
144
+ }
145
+ }
146
+
147
+ private updateValue() {
148
+ this.value = JSON.stringify(this.values);
149
+ this.emit('zn-change');
150
+ this.emit('zn-input');
151
+ }
152
+
153
+ render() {
154
+ const availableLanguages = Object.entries(this.languages)
155
+ .filter(([code]) => !Object.prototype.hasOwnProperty.call(this.values, code))
156
+ .map(([code, name]) => ({
157
+ title: name,
158
+ type: 'dropdown',
159
+ path: code
160
+ }));
161
+
162
+ const navigation = Object.keys(this.values).map(code => ({
163
+ title: this.languages[code] || code,
164
+ active: code === this._activeLanguage,
165
+ tab: code
166
+ }));
167
+
168
+ const currentTranslation = this.values[this._activeLanguage];
169
+ const useInlineEdit = currentTranslation && currentTranslation.length > 0;
170
+
171
+ const hasLabelSlot = this.hasSlotController.test('label');
172
+ const hasLabel = this.label ? true : hasLabelSlot;
173
+
174
+ return html`
175
+ <div part="form-control"
176
+ class="${classMap({
177
+ 'translations': true,
178
+ 'form-control': true,
179
+ 'form-control--medium': true,
180
+ 'form-control--has-label': hasLabel,
181
+ })}">
182
+ <label part="form-control-label" class="form-control__label" for="input"
183
+ aria-hidden=${hasLabel ? 'false' : 'true'}>
184
+ <slot name="label">${this.label}</slot>
185
+ </label>
186
+
187
+ <zn-navbar
188
+ .navigation="${navigation}"
189
+ .dropdown="${availableLanguages}"
190
+ @click="${this.handleNavbarClick}"
191
+ @zn-menu-select="${this.handleLanguageAdd}"
192
+ manual-add-items
193
+ ></zn-navbar>
194
+ <div class="input-container">
195
+ ${this._activeLanguage ? html`
196
+ ${useInlineEdit ? html`
197
+ <zn-inline-edit
198
+ .value=${currentTranslation}
199
+ @zn-change="${this.handleValueUpdate}"
200
+ @zn-input="${this.handleValueUpdate}"
201
+ ></zn-inline-edit>
202
+ ` : html`
203
+ <zn-input
204
+ .value="${currentTranslation || ''}"
205
+ placeholder="Enter translation..."
206
+ @zn-change="${this.handleValueUpdate}"
207
+ ></zn-input>
208
+ `}
209
+ ` : ''}
210
+ </div>
211
+ </div>
212
+ `;
213
+ }
214
+ }
@@ -0,0 +1,15 @@
1
+ @use "../../form-control";
2
+
3
+ :host {
4
+ display: block;
5
+ }
6
+
7
+ .translations {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--zn-spacing-small);
11
+ }
12
+
13
+ .input-container {
14
+ padding: var(--zn-spacing-small) 0;
15
+ }
@@ -0,0 +1,39 @@
1
+ import '../../../dist/zn.min.js';
2
+ import { expect, fixture, html } from '@open-wc/testing';
3
+ import type ZnTranslations from './translations.component';
4
+
5
+ describe('<zn-translations>', () => {
6
+ it('should render a component', async () => {
7
+ const el = await fixture(html` <zn-translations></zn-translations> `);
8
+ expect(el).to.exist;
9
+ });
10
+
11
+ it('should have default language en', async () => {
12
+ const el = await fixture<ZnTranslations>(html` <zn-translations></zn-translations> `);
13
+ expect(el.languages).to.have.property('en');
14
+ // We expect values to default to en: '' if strictly following requirements,
15
+ // or at least the UI should show it.
16
+ // Based on my plan, I will implement auto-population of 'en'.
17
+ expect(el.values).to.have.property('en');
18
+ });
19
+
20
+ it('should update value when input changes', async () => {
21
+ const el = await fixture<ZnTranslations>(html` <zn-translations></zn-translations> `);
22
+ // Mock languages to ensure we have something to edit
23
+ el.languages = { 'en': 'English', 'fr': 'French' };
24
+ el.values = { 'en': 'Hello' };
25
+ await el.updateComplete;
26
+
27
+ const input = el.shadowRoot!.querySelector('zn-inline-edit');
28
+ expect(input).to.exist;
29
+ await expect(input!.getAttribute('value')).to.equal('Hello');
30
+
31
+ // Simulate change
32
+ input!.value = 'Hello World';
33
+ input!.dispatchEvent(new CustomEvent('zn-change'));
34
+
35
+ await el.updateComplete;
36
+ await expect(el.values['en']).to.equal('Hello World');
37
+ await expect(JSON.parse(el.value)).to.deep.equal({'en': 'Hello World'});
38
+ });
39
+ });
package/src/zinc.ts CHANGED
@@ -57,6 +57,7 @@ export {default as Textarea} from './components/textarea';
57
57
  export {default as Checkbox} from './components/checkbox';
58
58
  export {default as Datepicker} from './components/datepicker';
59
59
  export {default as FormGroup} from './components/form-group';
60
+ export {default as InputGroup} from './components/input-group';
60
61
  export {default as LinkedSelect} from './components/linked-select';
61
62
  export {default as Radio} from './components/radio';
62
63
  export {default as Rating} from './components/rating';
@@ -83,6 +84,7 @@ export {default as SettingsContainer} from './components/settings-container';
83
84
  export { default as FilterContainer } from './components/filter-container';
84
85
  export { default as Reveal } from './components/reveal';
85
86
  export { default as AudioSelect } from './components/audio-select';
87
+ export { default as Translations } from './components/translations';
86
88
  /* plop:component */
87
89
 
88
90
  // Base Component