@kubex/zinc 1.0.19 → 1.0.21

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,5 +1,6 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
3
+ import {ifDefined} from "lit/directives/if-defined.js";
3
4
  import {property} from 'lit/decorators.js';
4
5
  import ZincElement from '../../internal/zinc-element';
5
6
  import ZnDropdown from "../dropdown";
@@ -40,6 +41,8 @@ export default class ZnNavbar extends ZincElement {
40
41
  @property({type: Boolean}) stacked: boolean;
41
42
  @property({type: Array}) dropdown = [];
42
43
  @property({attribute: "no-pad", type: Boolean}) noPad: false
44
+ @property({attribute: 'manual-add-items', type: Boolean}) manualAddItems = false;
45
+ @property({type: Boolean}) isolated = false;
43
46
 
44
47
  private _preItems: NodeListOf<Element>;
45
48
  private _postItems: NodeListOf<Element>;
@@ -126,16 +129,21 @@ export default class ZnNavbar extends ZincElement {
126
129
  this._navItems?.classList.toggle('has-hidden', hasHidden && hideRemaining)
127
130
  }
128
131
 
129
- public addItem(item: any) {
130
- if (this._openedTabs.includes(item.getAttribute('tab-uri'))) {
132
+ public addItem(item: Element): void {
133
+ const tabUri = item.getAttribute('tab-uri');
134
+ if (typeof tabUri !== 'string' || this._openedTabs.includes(tabUri)) {
131
135
  return;
132
136
  }
133
- this._openedTabs.push(item.getAttribute('tab-uri'));
137
+ this._openedTabs.push(tabUri);
134
138
  const ul = this.shadowRoot?.querySelector('ul');
135
139
  const dropdown = this.shadowRoot?.querySelector('[id="dropdown-item"]');
136
140
  // @ts-expect-error
137
141
  dropdown.querySelector('zn-dropdown').hide();
138
- return dropdown ? ul?.insertBefore(item, dropdown) : ul?.appendChild(item);
142
+ if (dropdown) {
143
+ ul?.insertBefore(item, dropdown);
144
+ } else {
145
+ ul?.appendChild(item);
146
+ }
139
147
  }
140
148
 
141
149
  protected firstUpdated(_changedProperties: PropertyValues) {
@@ -166,6 +174,9 @@ export default class ZnNavbar extends ZincElement {
166
174
  const menu = this.shadowRoot?.querySelector('zn-menu');
167
175
  if (menu) {
168
176
  menu.addEventListener('zn-menu-select', (e: ZnMenuSelectEvent) => {
177
+ if (this.manualAddItems) {
178
+ return;
179
+ }
169
180
  const element = e.detail.element;
170
181
  if (element.hasAttribute('data-path')) {
171
182
  const li = document.createElement('li');
@@ -185,6 +196,17 @@ export default class ZnNavbar extends ZincElement {
185
196
  //console.log("Showing More")
186
197
  }
187
198
 
199
+ private handleClick = (e: MouseEvent) => {
200
+ const path = e.composedPath();
201
+ const tabAttr = this.isolated ? 'data-tab' : 'tab';
202
+ const li = path.find(el => el instanceof HTMLElement && el.tagName === 'LI' && (el.hasAttribute(tabAttr) || el.hasAttribute('tab-uri'))) as HTMLElement;
203
+
204
+ if (li) {
205
+ e.stopPropagation();
206
+ this.emit('zn-select', {detail: {item: li}});
207
+ }
208
+ };
209
+
188
210
  render() {
189
211
  if (!this.navigation) {
190
212
  this.navigation = [];
@@ -197,13 +219,19 @@ export default class ZnNavbar extends ZincElement {
197
219
  }
198
220
 
199
221
  return html`
200
- <div class="navbar__container">
201
- <ul class="${classMap({
222
+ <div class="${classMap({
223
+ 'navbar__container': true,
224
+ 'navbar__container--stacked': this.stacked,
225
+ 'navbar__container--icon-bar': this.iconBar
226
+ })}">
227
+ <ul @click="${this.handleClick}" class="${classMap({
202
228
  'navbar': true,
203
229
  'navbar--slim': this.slim,
204
230
  'navbar--border': this.border,
205
231
  'navbar--flush': this.flush,
206
- 'navbar--no-pad': this.noPad
232
+ 'navbar--no-pad': this.noPad,
233
+ 'navbar--stacked': this.stacked,
234
+ 'navbar--icon-bar': this.iconBar
207
235
  })}">
208
236
  ${this._preItems}
209
237
  ${this.navigation.map((item: any) => {
@@ -217,7 +245,11 @@ export default class ZnNavbar extends ZincElement {
217
245
  <li class="${classMap({'active': item.active})}" tab-uri="${item.path}">${content}</li>`;
218
246
  }
219
247
  return html`
220
- <li class="${classMap({'active': item.active})}" tab="${item.tab}">${content}</li>`;
248
+ <li class="${classMap({'active': item.active})}"
249
+ tab="${ifDefined(!this.isolated ? item.tab : undefined)}"
250
+ data-tab="${ifDefined(this.isolated ? item.tab : undefined)}">
251
+ ${content}
252
+ </li>`;
221
253
  })}
222
254
  <li class="more">
223
255
  <zn-dropdown placement="bottom-end" id="extended-dropdown">
@@ -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,239 @@
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
+ import type {ZnSelectEvent} from "../../events/zn-select";
14
+
15
+ import styles from './translations.scss';
16
+
17
+ export default class ZnTranslations extends ZincElement implements ZincFormControl {
18
+ static styles = unsafeCSS(styles);
19
+ static dependencies = {
20
+ 'zn-navbar': ZnNavbar,
21
+ 'zn-input': ZnInput,
22
+ 'zn-inline-edit': ZnInlineEdit
23
+ };
24
+
25
+ private readonly formControlController: FormControlController = new FormControlController(this);
26
+ private readonly hasSlotController = new HasSlotController(this, 'label');
27
+
28
+ @property() name = '';
29
+ @property() value = '{"en":""}';
30
+ @property() label: string = '';
31
+ @property({type: Boolean, reflect: true}) disabled = false;
32
+ @property({type: Boolean, reflect: true}) required = false;
33
+
34
+ @property({type: Object}) languages: Record<string, string> = {
35
+ 'en': 'English'
36
+ };
37
+ @property({type: Object}) values: Record<string, string> = {};
38
+
39
+ @state() private _activeLanguage = '';
40
+
41
+ get validity(): ValidityState {
42
+ return validValidityState;
43
+ }
44
+
45
+ get validationMessage() {
46
+ return '';
47
+ }
48
+
49
+ checkValidity() {
50
+ return true;
51
+ }
52
+
53
+ getForm() {
54
+ return this.formControlController.getForm();
55
+ }
56
+
57
+ reportValidity() {
58
+ return true;
59
+ }
60
+
61
+ setCustomValidity() {
62
+ // no-op
63
+ }
64
+
65
+ protected firstUpdated() {
66
+ this.formControlController.updateValidity();
67
+ }
68
+
69
+ willUpdate(changedProperties: PropertyValues) {
70
+ let processValue = changedProperties.has('value');
71
+ let processValues = changedProperties.has('values');
72
+
73
+ if (processValue && processValues) {
74
+ const isValueDefault = this.value === '{"en":""}';
75
+ const isValuesEmpty = Object.keys(this.values).length === 0;
76
+
77
+ if (isValueDefault && !isValuesEmpty) {
78
+ processValue = false;
79
+ } else if (!isValueDefault && isValuesEmpty) {
80
+ processValues = false;
81
+ } else if (this.hasAttribute('values') && !this.hasAttribute('value')) {
82
+ processValue = false;
83
+ } else if (this.hasAttribute('value') && !this.hasAttribute('values')) {
84
+ processValues = false;
85
+ }
86
+ }
87
+
88
+ if (processValue) {
89
+ try {
90
+ const newValues = JSON.parse(this.value || '{}') as Record<string, string>;
91
+ if (JSON.stringify(newValues) !== JSON.stringify(this.values)) {
92
+ this.values = newValues;
93
+ }
94
+ } catch (e) {
95
+ // no-op
96
+ }
97
+ }
98
+
99
+ if (processValues) {
100
+ this.value = JSON.stringify(this.values);
101
+
102
+ // Ensure active language is valid
103
+ if (!this._activeLanguage || !Object.prototype.hasOwnProperty.call(this.values, this._activeLanguage)) {
104
+ const keys = Object.keys(this.values);
105
+ if (keys.length > 0) {
106
+ this._activeLanguage = keys[0];
107
+ } else {
108
+ this._activeLanguage = '';
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ private handleLanguageAdd = (e: ZnMenuSelectEvent) => {
115
+ const element = e.detail.element as HTMLElement;
116
+ const languageCode = element.getAttribute('data-path');
117
+ if (languageCode) {
118
+ // Add new language with empty string
119
+ this.values = {...this.values, [languageCode]: ''};
120
+ this._activeLanguage = languageCode;
121
+ this.updateValue();
122
+ }
123
+ };
124
+
125
+ private handleNavbarClick = (e: ZnSelectEvent) => {
126
+ e.stopPropagation();
127
+ const li = e.detail.item as HTMLElement;
128
+ if (li) {
129
+ const tab = li.getAttribute('tab') || li.getAttribute('data-tab');
130
+ if (tab) {
131
+ this._activeLanguage = tab;
132
+ this.requestUpdate();
133
+ }
134
+ }
135
+ };
136
+
137
+ private handleValueUpdate = (e: CustomEvent) => {
138
+ const target = e.target as (ZnInput | ZnInlineEdit);
139
+ if (this._activeLanguage) {
140
+ const newValue: string = target.value as string;
141
+ if (newValue !== this.values[this._activeLanguage]) {
142
+ this.values = {...this.values, [this._activeLanguage]: newValue};
143
+ this.updateValue();
144
+ }
145
+ }
146
+ };
147
+
148
+ private updateValue() {
149
+ this.value = JSON.stringify(this.values);
150
+ this.emit('zn-change');
151
+ this.emit('zn-input');
152
+ }
153
+
154
+ private handleKeyDown = (event: KeyboardEvent) => {
155
+ if (event.key === 'Enter') {
156
+ if (event.target instanceof ZnInlineEdit) {
157
+ return;
158
+ }
159
+
160
+ const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
161
+
162
+ if (!hasModifier && !event.defaultPrevented && !event.isComposing) {
163
+ this.formControlController.submit();
164
+ }
165
+ }
166
+ };
167
+
168
+ private handleSubmit = () => {
169
+ this.formControlController.submit();
170
+ };
171
+
172
+ render() {
173
+ const availableLanguages = Object.entries(this.languages)
174
+ .filter(([code]) => !Object.prototype.hasOwnProperty.call(this.values, code))
175
+ .map(([code, name]) => ({
176
+ title: name,
177
+ type: 'dropdown',
178
+ path: code
179
+ }));
180
+
181
+ const navigation = Object.keys(this.values).map(code => ({
182
+ title: this.languages[code] || code,
183
+ active: code === this._activeLanguage,
184
+ tab: code
185
+ }));
186
+
187
+ const currentTranslation = this.values[this._activeLanguage];
188
+ const useInlineEdit = currentTranslation && currentTranslation.length > 0;
189
+
190
+ const hasLabelSlot = this.hasSlotController.test('label');
191
+ const hasLabel = this.label ? true : hasLabelSlot;
192
+
193
+ return html`
194
+ <div part="form-control"
195
+ class="${classMap({
196
+ 'translations': true,
197
+ 'form-control': true,
198
+ 'form-control--medium': true,
199
+ 'form-control--has-label': hasLabel,
200
+ })}"
201
+ @keydown="${this.handleKeyDown}">
202
+ <label part="form-control-label" class="form-control__label" for="input"
203
+ aria-hidden=${hasLabel ? 'false' : 'true'}>
204
+ <slot name="label">${this.label}</slot>
205
+ </label>
206
+
207
+ <zn-navbar
208
+ .navigation="${navigation}"
209
+ .dropdown="${availableLanguages}"
210
+ name="${this.name}-translations-navbar"
211
+ isolated
212
+ @zn-select="${this.handleNavbarClick}"
213
+ @zn-menu-select="${this.handleLanguageAdd}"
214
+ manual-add-items
215
+ ></zn-navbar>
216
+ <div class="input-container">
217
+ ${this._activeLanguage ? html`
218
+ ${useInlineEdit ? html`
219
+ <zn-inline-edit
220
+ .value=${currentTranslation}
221
+ name="${this.name}"
222
+ @zn-change="${this.handleValueUpdate}"
223
+ @zn-input="${this.handleValueUpdate}"
224
+ @zn-submit="${this.handleSubmit}"
225
+ ></zn-inline-edit>
226
+ ` : html`
227
+ <zn-input
228
+ .value="${currentTranslation || ''}"
229
+ name="${this.name}"
230
+ placeholder="Enter translation..."
231
+ @zn-change="${this.handleValueUpdate}"
232
+ ></zn-input>
233
+ `}
234
+ ` : ''}
235
+ </div>
236
+ </div>
237
+ `;
238
+ }
239
+ }
@@ -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
+ });
@@ -1,6 +1,6 @@
1
1
  import type ZnMenuItem from "../components/menu-item";
2
2
 
3
- export type ZnSelectEvent = CustomEvent<{ item: ZnMenuItem }>;
3
+ export type ZnSelectEvent = CustomEvent<{ item: ZnMenuItem | HTMLElement }>;
4
4
 
5
5
  declare global {
6
6
  interface GlobalEventHandlersEventMap {
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