tyrell-components 1.0.0-TC46 → 1.0.0-TC48

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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/css/tyrell-theme.css +167 -178
  3. package/css/tyrell.css +19 -0
  4. package/dist/tyrell-theme.css +167 -178
  5. package/dist/tyrell.css +19 -0
  6. package/dist/tyrell.js +1 -1
  7. package/lib/base/ty-component.d.ts +8 -3
  8. package/lib/base/ty-component.js +13 -5
  9. package/lib/components/date-picker.d.ts +8 -0
  10. package/lib/components/date-picker.js +13 -9
  11. package/lib/components/file-upload.d.ts +6 -0
  12. package/lib/components/file-upload.js +19 -4
  13. package/lib/components/input.js +1 -0
  14. package/lib/components/modal.d.ts +8 -0
  15. package/lib/components/modal.js +29 -6
  16. package/lib/components/popup.js +6 -10
  17. package/lib/components/radio.js +10 -2
  18. package/lib/components/resize-observer.d.ts +1 -0
  19. package/lib/components/resize-observer.js +12 -1
  20. package/lib/components/select.d.ts +11 -3
  21. package/lib/components/select.js +26 -9
  22. package/lib/components/selected-tags.js +1 -1
  23. package/lib/components/tabs.d.ts +2 -0
  24. package/lib/components/tabs.js +18 -1
  25. package/lib/components/tooltip.js +24 -6
  26. package/lib/components/wizard.d.ts +2 -0
  27. package/lib/components/wizard.js +15 -0
  28. package/lib/styles/button.js +9 -2
  29. package/lib/styles/calendar-month.js +1 -1
  30. package/lib/styles/calendar-navigation.d.ts +1 -1
  31. package/lib/styles/calendar-navigation.js +1 -1
  32. package/lib/styles/calendar.d.ts +1 -1
  33. package/lib/styles/calendar.js +1 -1
  34. package/lib/styles/date-picker.js +30 -4
  35. package/lib/styles/file-upload.d.ts +1 -1
  36. package/lib/styles/file-upload.js +1 -1
  37. package/lib/styles/icon.d.ts +1 -1
  38. package/lib/styles/icon.js +12 -1
  39. package/lib/styles/input.js +2 -1
  40. package/lib/styles/modal.d.ts +1 -1
  41. package/lib/styles/modal.js +18 -0
  42. package/lib/styles/option.d.ts +1 -1
  43. package/lib/styles/option.js +16 -1
  44. package/lib/styles/popup.d.ts +1 -1
  45. package/lib/styles/popup.js +22 -5
  46. package/lib/styles/radio.js +1 -1
  47. package/lib/styles/select-base.js +59 -9
  48. package/lib/styles/select.d.ts +1 -1
  49. package/lib/styles/select.js +5 -1
  50. package/lib/styles/tabs.d.ts +1 -1
  51. package/lib/styles/tabs.js +6 -1
  52. package/lib/styles/textarea.d.ts +1 -1
  53. package/lib/styles/textarea.js +2 -1
  54. package/lib/styles/wizard.d.ts +1 -1
  55. package/lib/styles/wizard.js +3 -3
  56. package/lib/utils/number-format.d.ts +6 -1
  57. package/lib/utils/number-format.js +9 -2
  58. package/lib/utils/positioning.d.ts +50 -5
  59. package/lib/utils/positioning.js +52 -8
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. package/package.json +1 -1
@@ -132,9 +132,14 @@ export declare abstract class TyComponent<T = any> extends HTMLElement {
132
132
  * two genuinely separate clicks are always in different tasks.
133
133
  */
134
134
  protected isDuplicateActivationClick(): boolean;
135
- /**
136
- * Form reset callback - called when form.reset() is invoked
137
- * Resets component to its default value
135
+ private _resetDefaults;
136
+ /**
137
+ * Form reset callback - called when form.reset() is invoked.
138
+ * Restores every formValue-bearing property to its attribute-declared
139
+ * default captured at first connect (native defaultValue/defaultChecked
140
+ * semantics). Non-form state (e.g. a checkbox's submission `value`, a
141
+ * ty-radio's option `value`) is deliberately left alone — the old
142
+ * blanket 'value' reset corrupted radio groups by wiping child values.
138
143
  */
139
144
  formResetCallback(): void;
140
145
  /**
@@ -5,6 +5,7 @@ export class TyComponent extends HTMLElement {
5
5
  this._connected = false;
6
6
  this._rendered = false;
7
7
  this._handlingActivationClick = false;
8
+ this._resetDefaults = null;
8
9
  const ctor = this.constructor;
9
10
  this.props = new PropertyManager(ctor.properties);
10
11
  this._internals = this.attachInternals();
@@ -79,6 +80,16 @@ export class TyComponent extends HTMLElement {
79
80
  }
80
81
  connectedCallback() {
81
82
  this._connected = true;
83
+ if (!this._resetDefaults) {
84
+ this._resetDefaults = new Map();
85
+ const ctor = this.constructor;
86
+ for (const [name, config] of Object.entries(ctor.properties)) {
87
+ if (!config.formValue)
88
+ continue;
89
+ const attr = this.getAttribute(this._toKebabCase(name));
90
+ this._resetDefaults.set(name, attr ?? config.default ?? null);
91
+ }
92
+ }
82
93
  const preSetProps = this._capturePreSetProperties();
83
94
  if (preSetProps.length > 0) {
84
95
  this._handlePropertyChanges(preSetProps);
@@ -178,11 +189,8 @@ export class TyComponent extends HTMLElement {
178
189
  return false;
179
190
  }
180
191
  formResetCallback() {
181
- const ctor = this.constructor;
182
- const valueConfig = ctor.properties['value'];
183
- if (valueConfig) {
184
- const defaultValue = valueConfig.default ?? '';
185
- this.setProperty('value', defaultValue);
192
+ for (const [name, def] of this._resetDefaults ?? []) {
193
+ this.setProperty(name, def);
186
194
  }
187
195
  this.onFormReset();
188
196
  }
@@ -3,6 +3,7 @@
3
3
  * (modal <dialog>), optional time input, form participation via
4
4
  * ElementInternals. Value is UTC; display is local.
5
5
  */
6
+ import { type Placement } from '../utils/positioning.js';
6
7
  import { TyComponent } from '../base/ty-component.js';
7
8
  import type { Flavor } from '../types/common.js';
8
9
  import type { PropertyChange } from '../utils/property-manager.js';
@@ -41,6 +42,11 @@ export interface DatePickerChangeDetail {
41
42
  }
42
43
  export declare class TyDatePicker extends TyComponent<DatePickerState> {
43
44
  protected static properties: {
45
+ placement: {
46
+ type: "string";
47
+ visual: boolean;
48
+ default: string;
49
+ };
44
50
  size: {
45
51
  type: "string";
46
52
  visual: boolean;
@@ -169,6 +175,8 @@ export declare class TyDatePicker extends TyComponent<DatePickerState> {
169
175
  private emitChangeEvent;
170
176
  get size(): DatePickerSize;
171
177
  set size(v: DatePickerSize);
178
+ get placement(): Placement | '';
179
+ set placement(v: Placement | '');
172
180
  get flavor(): DatePickerFlavor;
173
181
  set flavor(v: DatePickerFlavor);
174
182
  get label(): string;
@@ -2,7 +2,7 @@ import { ensureStyles } from '../utils/styles.js';
2
2
  import { syncCustomFlavorSheet } from '../utils/flavor-sheet.js';
3
3
  import { datePickerStyles, datePickerCustomFlavorCss } from '../styles/date-picker.js';
4
4
  import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
5
- import { computeAnchoredPosition } from '../utils/positioning.js';
5
+ import { computeAnchoredPosition, placementToAnchored } from '../utils/positioning.js';
6
6
  import { getEffectiveLocale, observeLocaleChanges } from '../utils/locale.js';
7
7
  import { isMobileTouch } from '../utils/mobile.js';
8
8
  import { TyComponent } from '../base/ty-component.js';
@@ -506,6 +506,8 @@ export class TyDatePicker extends TyComponent {
506
506
  }
507
507
  get size() { return this.getProperty('size'); }
508
508
  set size(v) { this.setProperty('size', v); }
509
+ get placement() { return this.getProperty('placement'); }
510
+ set placement(v) { this.setProperty('placement', v); }
509
511
  get flavor() { return this.getProperty('flavor'); }
510
512
  set flavor(v) { this.setProperty('flavor', v); }
511
513
  get label() { return this.getProperty('label'); }
@@ -755,11 +757,14 @@ export class TyDatePicker extends TyComponent {
755
757
  const estimatedHeight = dialogRect.height || 400;
756
758
  const popupWidth = dialogRect.width || 320;
757
759
  const wrapPadding = 8;
760
+ const anchored = placementToAnchored(this.getProperty('placement'));
758
761
  const pos = computeAnchoredPosition({
759
762
  anchorRect: stubRect,
760
763
  popupWidth,
761
764
  popupHeight: estimatedHeight,
762
765
  gap: 0,
766
+ align: anchored.align,
767
+ side: anchored.side,
763
768
  });
764
769
  const positionBelow = pos.below;
765
770
  const x = pos.x - wrapPadding;
@@ -975,10 +980,7 @@ export class TyDatePicker extends TyComponent {
975
980
  if (isMobileTouch())
976
981
  return;
977
982
  this.updateState({ open: true });
978
- this.dispatchEvent(new CustomEvent('open', {
979
- bubbles: true,
980
- composed: true
981
- }));
983
+ this.dispatchEvent(new CustomEvent('open'));
982
984
  this.render();
983
985
  requestAnimationFrame(() => {
984
986
  if (!this.shadowRoot)
@@ -1017,10 +1019,7 @@ export class TyDatePicker extends TyComponent {
1017
1019
  if (stubEl)
1018
1020
  stubEl.setAttribute('aria-expanded', 'false');
1019
1021
  }
1020
- this.dispatchEvent(new CustomEvent('close', {
1021
- bubbles: true,
1022
- composed: true
1023
- }));
1022
+ this.dispatchEvent(new CustomEvent('close'));
1024
1023
  this.render();
1025
1024
  }
1026
1025
  cleanup() {
@@ -1047,6 +1046,11 @@ export class TyDatePicker extends TyComponent {
1047
1046
  }
1048
1047
  }
1049
1048
  TyDatePicker.properties = {
1049
+ placement: {
1050
+ type: 'string',
1051
+ visual: true,
1052
+ default: '',
1053
+ },
1050
1054
  size: {
1051
1055
  type: 'string',
1052
1056
  visual: true,
@@ -80,6 +80,12 @@ export declare class TyFileUpload extends TyComponent<FileUploadState> implement
80
80
  protected onPropertiesChanged(_changes: PropertyChange[]): void;
81
81
  protected onFormReset(): void;
82
82
  protected getFormValue(): FormData | string | null;
83
+ /**
84
+ * Match a file against the `accept` attribute the way native inputs do:
85
+ * comma-separated list of `.ext`, `type/subtype` or `type/*` entries.
86
+ * No/blank accept matches everything.
87
+ */
88
+ private matchesAccept;
83
89
  get files(): File[];
84
90
  get multiple(): boolean;
85
91
  set multiple(v: boolean);
@@ -44,15 +44,30 @@ export class TyFileUpload extends TyComponent {
44
44
  fileInput.value = "";
45
45
  }
46
46
  getFormValue() {
47
- if (this._files.length === 0)
47
+ if (this._files.length === 0 || !this.name)
48
48
  return null;
49
49
  const fd = new FormData();
50
- const fieldName = this.name || "files";
51
50
  for (const file of this._files) {
52
- fd.append(fieldName, file);
51
+ fd.append(this.name, file);
53
52
  }
54
53
  return fd;
55
54
  }
55
+ matchesAccept(file) {
56
+ const accept = this.accept?.trim();
57
+ if (!accept)
58
+ return true;
59
+ return accept.split(",").some((raw) => {
60
+ const entry = raw.trim().toLowerCase();
61
+ if (!entry)
62
+ return false;
63
+ if (entry.startsWith("."))
64
+ return file.name.toLowerCase().endsWith(entry);
65
+ const type = file.type.toLowerCase();
66
+ if (entry.endsWith("/*"))
67
+ return type.startsWith(entry.slice(0, -1));
68
+ return type === entry;
69
+ });
70
+ }
56
71
  get files() {
57
72
  return [...this._files];
58
73
  }
@@ -178,7 +193,7 @@ export class TyFileUpload extends TyComponent {
178
193
  e.stopPropagation();
179
194
  this._isDragging = false;
180
195
  dropZone.classList.remove("drag-active");
181
- const files = Array.from(e.dataTransfer?.files ?? []);
196
+ const files = Array.from(e.dataTransfer?.files ?? []).filter((f) => this.matchesAccept(f));
182
197
  if (files.length > 0)
183
198
  this.setFiles(files);
184
199
  };
@@ -81,6 +81,7 @@ export class TyInput extends TyComponent {
81
81
  }
82
82
  onFormReset() {
83
83
  this._shadowValue = null;
84
+ this.initializeShadowValue();
84
85
  this._isFocused = false;
85
86
  if (this._inputDebounceTimer !== null) {
86
87
  clearTimeout(this._inputDebounceTimer);
@@ -48,6 +48,14 @@ export declare class TyModal extends HTMLElement {
48
48
  force?: boolean;
49
49
  }) => void;
50
50
  static get observedAttributes(): string[];
51
+ get open(): boolean;
52
+ set open(v: boolean);
53
+ get backdrop(): boolean;
54
+ set backdrop(v: boolean);
55
+ get closeOnOutsideClick(): boolean;
56
+ set closeOnOutsideClick(v: boolean);
57
+ get closeOnEscape(): boolean;
58
+ set closeOnEscape(v: boolean);
51
59
  constructor();
52
60
  connectedCallback(): void;
53
61
  disconnectedCallback(): void;
@@ -7,6 +7,7 @@ const closeButtonHandlers = new WeakMap();
7
7
  const hoverEnterHandlers = new WeakMap();
8
8
  const hoverLeaveHandlers = new WeakMap();
9
9
  const modalIds = new WeakMap();
10
+ const pendingCloseReasons = new WeakMap();
10
11
  const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-icon lucide-x"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
11
12
  function parseBoolAttr(el, name) {
12
13
  if (!el.hasAttribute(name)) {
@@ -47,7 +48,6 @@ function isMobile() {
47
48
  function dispatchModalEvent(el, eventType, detail) {
48
49
  const event = new CustomEvent(eventType, {
49
50
  detail,
50
- bubbles: true,
51
51
  cancelable: true,
52
52
  });
53
53
  el.dispatchEvent(event);
@@ -80,14 +80,13 @@ function closeModal(el, reason = 'programmatic', opts) {
80
80
  if (!opts?.force) {
81
81
  const beforeClose = new CustomEvent('beforeclose', {
82
82
  detail: { reason },
83
- bubbles: true,
84
- composed: true,
85
83
  cancelable: true,
86
84
  });
87
85
  el.dispatchEvent(beforeClose);
88
86
  if (beforeClose.defaultPrevented)
89
87
  return;
90
88
  }
89
+ pendingCloseReasons.set(el, reason);
91
90
  el.removeAttribute('open');
92
91
  }
93
92
  function openModal(el) {
@@ -226,6 +225,11 @@ function render(el) {
226
225
  }
227
226
  setupBackdropClick(el, dialog, attributes.closeOnOutsideClick);
228
227
  setupEscapeKey(el, dialog, attributes.closeOnEscape);
228
+ dialog.oncancel = (event) => {
229
+ event.preventDefault();
230
+ if (getModalAttributes(el).closeOnEscape)
231
+ closeModal(el, 'escape');
232
+ };
229
233
  setupCloseButton(el, dialog, attributes.closeOnOutsideClick);
230
234
  setupCloseButtonHover(el, dialog);
231
235
  if (attributes.open) {
@@ -237,6 +241,7 @@ function render(el) {
237
241
  else {
238
242
  dialog.show();
239
243
  }
244
+ syncBackdropZoom();
240
245
  dispatchModalEvent(el, 'open', {});
241
246
  }
242
247
  }
@@ -244,9 +249,10 @@ function render(el) {
244
249
  if (dialog.open) {
245
250
  unlockScroll(modalId);
246
251
  dialog.close();
247
- dispatchModalEvent(el, 'close', {
248
- reason: 'programmatic'
249
- });
252
+ syncBackdropZoom();
253
+ const reason = pendingCloseReasons.get(el) ?? 'programmatic';
254
+ pendingCloseReasons.delete(el);
255
+ dispatchModalEvent(el, 'close', { reason });
250
256
  }
251
257
  }
252
258
  dialog.onclose = (event) => {
@@ -260,8 +266,16 @@ function render(el) {
260
266
  returnValue: dialog.returnValue || undefined
261
267
  });
262
268
  }
269
+ syncBackdropZoom();
263
270
  };
264
271
  }
272
+ function syncBackdropZoom() {
273
+ const anyOpen = Array.from(document.querySelectorAll('ty-modal[open][backdrop-zoom], ty-dialog[open][backdrop-zoom]')).some((m) => {
274
+ const v = m.getAttribute('backdrop-zoom').toLowerCase().trim();
275
+ return v !== 'false' && v !== '0';
276
+ });
277
+ document.documentElement.classList.toggle('ty-modal-zoom', anyOpen);
278
+ }
265
279
  function cleanup(el) {
266
280
  const modalId = modalIds.get(el);
267
281
  const shadowRoot = el.shadowRoot;
@@ -270,6 +284,7 @@ function cleanup(el) {
270
284
  unlockScroll(modalId);
271
285
  modalIds.delete(el);
272
286
  }
287
+ syncBackdropZoom();
273
288
  if (!dialog)
274
289
  return;
275
290
  const backdropHandler = backdropClickHandlers.get(el);
@@ -308,6 +323,14 @@ export class TyModal extends HTMLElement {
308
323
  static get observedAttributes() {
309
324
  return ['open', 'backdrop', 'close-on-outside-click', 'close-on-escape', 'label'];
310
325
  }
326
+ get open() { return parseBoolAttr(this, 'open'); }
327
+ set open(v) { v ? this.setAttribute('open', 'true') : this.removeAttribute('open'); }
328
+ get backdrop() { return getModalAttributes(this).backdrop; }
329
+ set backdrop(v) { this.setAttribute('backdrop', String(!!v)); }
330
+ get closeOnOutsideClick() { return getModalAttributes(this).closeOnOutsideClick; }
331
+ set closeOnOutsideClick(v) { this.setAttribute('close-on-outside-click', String(!!v)); }
332
+ get closeOnEscape() { return getModalAttributes(this).closeOnEscape; }
333
+ set closeOnEscape(v) { this.setAttribute('close-on-escape', String(!!v)); }
311
334
  constructor() {
312
335
  super();
313
336
  this.attachShadow({ mode: 'open' });
@@ -1,4 +1,4 @@
1
- import { findBestPosition } from '../utils/positioning.js';
1
+ import { findBestPosition, preferenceChain } from '../utils/positioning.js';
2
2
  import { lockScroll, unlockScroll } from '../utils/scroll-lock.js';
3
3
  import { ensureStyles } from '../utils/styles.js';
4
4
  import { popupStyles } from '../styles/popup.js';
@@ -47,11 +47,7 @@ function updatePosition(el) {
47
47
  const dialog = shadowRoot ? getPopupDialog(shadowRoot) : null;
48
48
  if (!anchor || !dialog)
49
49
  return;
50
- const preferences = placement === 'top' ? ['top', 'bottom', 'left', 'right'] :
51
- placement === 'bottom' ? ['bottom', 'top', 'left', 'right'] :
52
- placement === 'left' ? ['left', 'right', 'top', 'bottom'] :
53
- placement === 'right' ? ['right', 'left', 'top', 'bottom'] :
54
- ['bottom', 'top', 'right', 'left'];
50
+ const preferences = preferenceChain(placement);
55
51
  const positionData = findBestPosition({
56
52
  targetEl: anchor,
57
53
  floatingEl: dialog,
@@ -62,6 +58,7 @@ function updatePosition(el) {
62
58
  });
63
59
  el.style.setProperty('--popup-x', `${positionData.x}px`);
64
60
  el.style.setProperty('--popup-y', `${positionData.y}px`);
61
+ dialog.dataset.side = positionData.placement.split('-')[0];
65
62
  dialog.classList.add('position-calculated');
66
63
  }
67
64
  function closePopup(el, force = false) {
@@ -80,8 +77,7 @@ function closePopup(el, force = false) {
80
77
  setTimeout(() => {
81
78
  dialog.classList.remove('position-calculated');
82
79
  dialog.close();
83
- }, 150);
84
- el.dispatchEvent(new CustomEvent('close', { bubbles: true }));
80
+ }, 200);
85
81
  }
86
82
  function openPopup(el) {
87
83
  const popupId = getPopupId(el);
@@ -99,7 +95,7 @@ function openPopup(el) {
99
95
  dialog.classList.add('preparing-animation');
100
96
  requestAnimationFrame(() => {
101
97
  dialog.classList.add('open');
102
- el.dispatchEvent(new CustomEvent('open', { bubbles: true }));
98
+ el.dispatchEvent(new CustomEvent('open'));
103
99
  });
104
100
  });
105
101
  }
@@ -215,7 +211,7 @@ function render(el) {
215
211
  unlockScroll(popupId);
216
212
  dialog.classList.remove('open');
217
213
  dialog.classList.remove('preparing-animation');
218
- el.dispatchEvent(new CustomEvent('close', { bubbles: true }));
214
+ el.dispatchEvent(new CustomEvent('close'));
219
215
  });
220
216
  }
221
217
  el.closePopup = () => closePopup(el, true);
@@ -176,8 +176,16 @@ export class TyRadioGroup extends TyComponent {
176
176
  const radios = this.getRadios();
177
177
  for (const radio of radios) {
178
178
  radio.checked = radio.value === this.value;
179
- if (this.disabled)
180
- radio.disabled = true;
179
+ if (this.disabled) {
180
+ if (!radio.disabled) {
181
+ radio.disabled = true;
182
+ radio.dataset.tyGroupDisabled = "";
183
+ }
184
+ }
185
+ else if ("tyGroupDisabled" in radio.dataset) {
186
+ radio.disabled = false;
187
+ delete radio.dataset.tyGroupDisabled;
188
+ }
181
189
  radio.size = this.size;
182
190
  radio.flavor = this.flavor;
183
191
  }
@@ -13,6 +13,7 @@ export declare class TyResizeObserver extends HTMLElement {
13
13
  set debounce(value: number);
14
14
  constructor();
15
15
  connectedCallback(): void;
16
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
16
17
  disconnectedCallback(): void;
17
18
  private setupObserver;
18
19
  private handleResize;
@@ -22,6 +22,15 @@ export class TyResizeObserver extends HTMLElement {
22
22
  connectedCallback() {
23
23
  this.setupObserver();
24
24
  }
25
+ attributeChangedCallback(name, oldValue, newValue) {
26
+ if (name === 'id' && oldValue && oldValue !== newValue) {
27
+ removeSize(oldValue);
28
+ if (newValue && this.isConnected) {
29
+ const { width, height } = this.getBoundingClientRect();
30
+ updateSize(newValue, width, height);
31
+ }
32
+ }
33
+ }
25
34
  disconnectedCallback() {
26
35
  this.cleanup();
27
36
  }
@@ -60,4 +69,6 @@ export class TyResizeObserver extends HTMLElement {
60
69
  }
61
70
  }
62
71
  }
63
- customElements.define('ty-resize-observer', TyResizeObserver);
72
+ if (!customElements.get('ty-resize-observer')) {
73
+ customElements.define('ty-resize-observer', TyResizeObserver);
74
+ }
@@ -5,6 +5,7 @@
5
5
  * Skins: default form field, `compact` content-hugging trigger, slot="trigger" custom.
6
6
  */
7
7
  import type { Flavor, Size } from "../types/common.js";
8
+ import { type Placement } from "../utils/positioning.js";
8
9
  import { TyComponent } from "../base/ty-component.js";
9
10
  import type { PropertyChange } from "../utils/property-manager.js";
10
11
  interface TagData {
@@ -116,7 +117,12 @@ export declare class TySelect extends TyComponent<SelectState> {
116
117
  visual: boolean;
117
118
  default: string;
118
119
  validate: (v: any) => boolean;
119
- coerce: (v: any) => "start" | "end";
120
+ coerce: (v: any) => "start" | "end" | "center";
121
+ };
122
+ placement: {
123
+ type: "string";
124
+ visual: boolean;
125
+ default: string;
120
126
  };
121
127
  size: {
122
128
  type: "string";
@@ -402,8 +408,10 @@ export declare class TySelect extends TyComponent<SelectState> {
402
408
  set debounce(value: number | string);
403
409
  get size(): Size;
404
410
  set size(value: Size);
405
- get align(): "start" | "end";
406
- set align(value: "start" | "end");
411
+ get align(): "start" | "center" | "end";
412
+ set align(value: "start" | "center" | "end");
413
+ get placement(): Placement | "";
414
+ set placement(value: Placement | "");
407
415
  get flavor(): Flavor;
408
416
  set flavor(value: Flavor);
409
417
  get form(): HTMLFormElement | null;
@@ -4,7 +4,7 @@ import { selectBaseStyles, selectCustomFlavorCss } from "../styles/select-base.j
4
4
  import { selectStyles } from "../styles/select.js";
5
5
  import { getLoaderSvg } from "../utils/loader-registry.js";
6
6
  import { lockScroll, unlockScroll } from "../utils/scroll-lock.js";
7
- import { computeAnchoredPosition } from "../utils/positioning.js";
7
+ import { computeAnchoredPosition, placementToAnchored } from "../utils/positioning.js";
8
8
  import { isMobileTouch } from "../utils/mobile.js";
9
9
  import { TyComponent } from "../base/ty-component.js";
10
10
  import { CustomScrollbar, isCustomScrollbarEnabled, } from "../utils/custom-scrollbar.js";
@@ -366,11 +366,20 @@ export class TySelect extends TyComponent {
366
366
  const popupWidth = !isNaN(cssWidth)
367
367
  ? cssWidth
368
368
  : Math.max(stubRect.width, 320);
369
+ const requested = this.getProperty("placement");
370
+ const anchored = requested
371
+ ? placementToAnchored(requested)
372
+ : {
373
+ side: "auto",
374
+ align: this.getProperty("align"),
375
+ };
369
376
  const pos = computeAnchoredPosition({
370
377
  anchorRect: stubRect,
371
378
  popupWidth,
372
379
  popupHeight: estimatedHeight,
373
- align: this.getProperty("align"),
380
+ align: anchored.align,
381
+ side: anchored.side,
382
+ gap: 8,
374
383
  });
375
384
  const positionBelow = pos.below;
376
385
  const x = pos.x - wrapPadding;
@@ -428,8 +437,8 @@ export class TySelect extends TyComponent {
428
437
  this._scrollLockId = lockId;
429
438
  lockScroll(lockId);
430
439
  dialog.showModal();
431
- dialog.classList.add("open");
432
440
  this.calculatePosition();
441
+ dialog.classList.add("open");
433
442
  this._state.open = true;
434
443
  const chevron = shadow.querySelector(".dropdown-chevron");
435
444
  if (chevron)
@@ -508,6 +517,7 @@ export class TySelect extends TyComponent {
508
517
  this._scrollLockId = lockId;
509
518
  lockScroll(lockId);
510
519
  dialog.showModal();
520
+ dialog.getBoundingClientRect();
511
521
  dialog.classList.add("open");
512
522
  this._state.open = true;
513
523
  const tags = this.getTagElements().map((el) => this.getTagData(el));
@@ -837,8 +847,6 @@ export class TySelect extends TyComponent {
837
847
  fireOpenEvent() {
838
848
  this.dispatchEvent(new CustomEvent("open", {
839
849
  detail: { mode: this._state.mode, element: this },
840
- bubbles: true,
841
- composed: true,
842
850
  }));
843
851
  if (this._externalSearch) {
844
852
  this.fireSearchEvent("");
@@ -847,8 +855,6 @@ export class TySelect extends TyComponent {
847
855
  fireCloseEvent() {
848
856
  this.dispatchEvent(new CustomEvent("close", {
849
857
  detail: { mode: this._state.mode, element: this },
850
- bubbles: true,
851
- composed: true,
852
858
  }));
853
859
  }
854
860
  dispatchChangeEvent(detail) {
@@ -1377,6 +1383,12 @@ export class TySelect extends TyComponent {
1377
1383
  set align(value) {
1378
1384
  this.setProperty("align", value);
1379
1385
  }
1386
+ get placement() {
1387
+ return this.getProperty("placement");
1388
+ }
1389
+ set placement(value) {
1390
+ this.setProperty("placement", value);
1391
+ }
1380
1392
  get flavor() {
1381
1393
  return this.getProperty("flavor");
1382
1394
  }
@@ -1484,8 +1496,13 @@ TySelect.properties = {
1484
1496
  type: "string",
1485
1497
  visual: true,
1486
1498
  default: "start",
1487
- validate: (v) => ["start", "end"].includes(v),
1488
- coerce: (v) => (v === "end" ? "end" : "start"),
1499
+ validate: (v) => ["start", "center", "end"].includes(v),
1500
+ coerce: (v) => v === "end" ? "end" : v === "center" ? "center" : "start",
1501
+ },
1502
+ placement: {
1503
+ type: "string",
1504
+ visual: true,
1505
+ default: "",
1489
1506
  },
1490
1507
  size: {
1491
1508
  type: "string",
@@ -105,7 +105,7 @@ export class TySelectedOptions extends HTMLElement {
105
105
  return ctx;
106
106
  }
107
107
  templateSource(template) {
108
- if (template.content.firstElementChild)
108
+ if (template.content.hasChildNodes())
109
109
  return template.content;
110
110
  const frag = document.createDocumentFragment();
111
111
  template.childNodes.forEach(n => frag.appendChild(n.cloneNode(true)));
@@ -27,6 +27,8 @@ export interface TabChangeDetail {
27
27
  export declare class TyTabs extends HTMLElement {
28
28
  static get observedAttributes(): string[];
29
29
  constructor();
30
+ private _childObserver;
31
+ private _childRenderQueued;
30
32
  connectedCallback(): void;
31
33
  disconnectedCallback(): void;
32
34
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
@@ -231,7 +231,7 @@ function setupResizeObserver(el) {
231
231
  const activeId = getActiveTabId(el, tabs);
232
232
  const activeIndex = activeId ? findTabIndex(tabs, activeId) : 0;
233
233
  if (width.includes('%')) {
234
- el.style.setProperty('--tabs-width', `${entry.contentRect.width}px`);
234
+ el.style.setProperty('--tabs-page-width', `${entry.contentRect.width}px`);
235
235
  }
236
236
  if (activeIndex !== undefined) {
237
237
  updateTransform(el, activeIndex);
@@ -370,6 +370,8 @@ function updateOverflow(el) {
370
370
  if (buttons.length === 0)
371
371
  return;
372
372
  const available = container.clientWidth;
373
+ if (available <= 0)
374
+ return;
373
375
  let total = 0;
374
376
  let cutoff = buttons.length;
375
377
  for (let i = 0; i < buttons.length; i++) {
@@ -513,12 +515,27 @@ export class TyTabs extends HTMLElement {
513
515
  }
514
516
  constructor() {
515
517
  super();
518
+ this._childObserver = null;
519
+ this._childRenderQueued = false;
516
520
  this.attachShadow({ mode: 'open' });
517
521
  }
518
522
  connectedCallback() {
519
523
  render(this);
524
+ this._childObserver = new MutationObserver(() => {
525
+ if (this._childRenderQueued)
526
+ return;
527
+ this._childRenderQueued = true;
528
+ queueMicrotask(() => {
529
+ this._childRenderQueued = false;
530
+ if (this.isConnected)
531
+ render(this);
532
+ });
533
+ });
534
+ this._childObserver.observe(this, { childList: true });
520
535
  }
521
536
  disconnectedCallback() {
537
+ this._childObserver?.disconnect();
538
+ this._childObserver = null;
522
539
  cleanup(this);
523
540
  }
524
541
  attributeChangedCallback(name, oldValue, newValue) {