@design.estate/dees-catalog 7.0.0 → 7.0.1

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.
@@ -3,6 +3,7 @@ import {
3
3
  html,
4
4
  DeesElement,
5
5
  property,
6
+ state,
6
7
  type TemplateResult,
7
8
  cssManager,
8
9
  css,
@@ -93,18 +94,36 @@ export class DeesButton extends DeesElement {
93
94
  @property({ type: String, reflect: true })
94
95
  accessor iconPosition: 'left' | 'right' = 'left';
95
96
 
97
+ @state()
98
+ private accessor adoptedText = '';
99
+
100
+ @state()
101
+ private accessor adoptedIcon = '';
102
+
103
+ @state()
104
+ private accessor adoptedIconPosition: 'left' | 'right' = 'left';
105
+
106
+ private lightDomObserver: MutationObserver | undefined = undefined;
107
+
96
108
  constructor() {
97
109
  super();
98
110
  }
99
111
 
100
- public async connectedCallback() {
101
- await super.connectedCallback();
112
+ public async connectedCallback(): Promise<void> {
113
+ const superConnected = super.connectedCallback();
114
+ this.observeLightDom();
115
+ await superConnected;
102
116
  // Auto-detect if inside a form
103
117
  if (!this.insideForm && this.closest('dees-form')) {
104
118
  this.insideForm = true;
105
119
  }
106
120
  }
107
121
 
122
+ public async disconnectedCallback(): Promise<void> {
123
+ this.lightDomObserver?.disconnect();
124
+ await super.disconnectedCallback();
125
+ }
126
+
108
127
  public static styles = [
109
128
  themeDefaultStyles,
110
129
  cssManager.defaultStyles,
@@ -464,49 +483,76 @@ export class DeesButton extends DeesElement {
464
483
  `,
465
484
  ];
466
485
 
467
- /**
468
- * Extracts icon and text from light DOM and sets properties
469
- */
470
- private extractLightDom(): void {
471
- const iconElement = this.querySelector('dees-icon') as any;
472
-
473
- // Get all text content from light DOM
474
- const textContent = Array.from(this.childNodes)
475
- .filter(node => node.nodeType === Node.TEXT_NODE)
476
- .map(node => node.textContent?.trim())
477
- .filter(Boolean)
478
- .join(' ');
479
-
480
- if (textContent && !this.text) {
481
- this.text = textContent;
482
- }
483
-
484
- if (iconElement) {
485
- const iconValue = iconElement.icon || iconElement.getAttribute('icon') || null;
486
-
487
- if (iconValue) {
488
- // Determine position based on DOM order
489
- const children = Array.from(this.childNodes);
490
- const iconIndex = children.indexOf(iconElement);
491
- const textNodes = children.filter(node =>
492
- node.nodeType === Node.TEXT_NODE && node.textContent?.trim()
493
- );
494
-
495
- if (textNodes.length > 0) {
496
- const firstTextIndex = children.indexOf(textNodes[0]);
497
- this.iconPosition = iconIndex < firstTextIndex ? 'left' : 'right';
486
+ private observeLightDom(): void {
487
+ this.lightDomObserver ??= new MutationObserver((mutationsArg) => {
488
+ const supportedContentChanged = mutationsArg.some((mutationArg) => {
489
+ if (mutationArg.type === 'childList') {
490
+ return mutationArg.target === this;
491
+ }
492
+ if (mutationArg.type === 'characterData') {
493
+ return mutationArg.target.parentNode === this;
498
494
  }
495
+ return mutationArg.target.parentNode === this
496
+ && (mutationArg.target as Element).localName === 'dees-icon';
497
+ });
498
+ if (supportedContentChanged) {
499
+ this.adoptLightDom();
500
+ }
501
+ });
502
+ this.lightDomObserver.observe(this, {
503
+ attributes: true,
504
+ attributeFilter: ['icon'],
505
+ characterData: true,
506
+ childList: true,
507
+ subtree: true,
508
+ });
509
+ this.adoptLightDom();
510
+ }
499
511
 
500
- // Set the icon property
501
- this.icon = iconValue;
502
- }
512
+ /** Mirrors supported direct light content without taking ownership of consumer nodes. */
513
+ private adoptLightDom(): void {
514
+ const children = Array.from(this.childNodes);
515
+ const textNodes = children.filter(
516
+ (nodeArg) => nodeArg.nodeType === Node.TEXT_NODE && nodeArg.textContent?.trim(),
517
+ );
518
+ const iconElement = children.find(
519
+ (nodeArg): nodeArg is HTMLElement & { icon?: string } =>
520
+ nodeArg instanceof HTMLElement && nodeArg.localName === 'dees-icon',
521
+ );
503
522
 
504
- // Remove the light DOM icon element
505
- iconElement.remove();
523
+ const nextAdoptedText = textNodes
524
+ .map((nodeArg) => nodeArg.textContent!.trim())
525
+ .join(' ');
526
+ const nextAdoptedIcon = iconElement?.icon || iconElement?.getAttribute('icon') || '';
527
+ const nextAdoptedIconPosition =
528
+ iconElement && textNodes.length > 0
529
+ ? children.indexOf(iconElement) < children.indexOf(textNodes[0])
530
+ ? 'left'
531
+ : 'right'
532
+ : 'left';
533
+
534
+ const textFollowsLightDom = this.text === this.adoptedText;
535
+ const iconFollowsLightDom = (this.icon || '') === this.adoptedIcon;
536
+ const iconPositionFollowsLightDom = this.iconPosition === this.adoptedIconPosition;
537
+ const previousAdoptedIcon = this.adoptedIcon;
538
+
539
+ this.adoptedText = nextAdoptedText;
540
+ this.adoptedIcon = nextAdoptedIcon;
541
+ this.adoptedIconPosition = nextAdoptedIconPosition;
542
+
543
+ if (textFollowsLightDom && this.text !== nextAdoptedText) {
544
+ this.text = nextAdoptedText;
545
+ }
546
+ if (
547
+ iconFollowsLightDom
548
+ && this.icon !== nextAdoptedIcon
549
+ && Boolean(nextAdoptedIcon || previousAdoptedIcon)
550
+ ) {
551
+ this.icon = nextAdoptedIcon;
552
+ }
553
+ if (iconPositionFollowsLightDom && this.iconPosition !== nextAdoptedIconPosition) {
554
+ this.iconPosition = nextAdoptedIconPosition;
506
555
  }
507
-
508
- // Clear all remaining light DOM
509
- this.innerHTML = '';
510
556
  }
511
557
 
512
558
  public render(): TemplateResult {
@@ -520,17 +566,20 @@ export class DeesButton extends DeesElement {
520
566
 
521
567
  const actualType = typeMap[this.type] || this.type;
522
568
  const actualSize = this.type === 'big' ? 'lg' : this.size;
569
+ const effectiveText = this.text;
570
+ const effectiveIcon = this.icon;
571
+ const effectiveIconPosition = this.iconPosition;
523
572
 
524
- const leftIcon = this.iconPosition === 'left' && this.icon
525
- ? html`<dees-icon .icon=${this.icon}></dees-icon>`
573
+ const leftIcon = effectiveIconPosition === 'left' && effectiveIcon
574
+ ? html`<dees-icon .icon=${effectiveIcon}></dees-icon>`
526
575
  : '';
527
- const rightIcon = this.iconPosition === 'right' && this.icon
528
- ? html`<dees-icon .icon=${this.icon}></dees-icon>`
576
+ const rightIcon = effectiveIconPosition === 'right' && effectiveIcon
577
+ ? html`<dees-icon .icon=${effectiveIcon}></dees-icon>`
529
578
  : '';
530
579
 
531
580
  // No placeholder label: a button without text renders icon-only.
532
581
  // size-icon additionally suppresses text to keep its square geometry.
533
- const showText = Boolean(this.text) && !(actualSize === 'icon' && this.icon);
582
+ const showText = Boolean(effectiveText) && !(actualSize === 'icon' && effectiveIcon);
534
583
 
535
584
  return html`
536
585
  <div
@@ -541,7 +590,7 @@ export class DeesButton extends DeesElement {
541
590
  tabindex=${this.disabled ? -1 : 0}
542
591
  aria-disabled=${this.disabled ? 'true' : 'false'}
543
592
  aria-busy=${this.status === 'pending' ? 'true' : 'false'}
544
- .ariaLabel=${showText ? null : this.text || null}
593
+ .ariaLabel=${showText ? null : effectiveText || null}
545
594
  @click="${this.dispatchClick}"
546
595
  @keydown=${this.handleKeydown}
547
596
  @keyup=${this.handleKeyup}
@@ -554,7 +603,7 @@ export class DeesButton extends DeesElement {
554
603
  ></dees-spinner>
555
604
  `}
556
605
  ${leftIcon}
557
- ${showText ? html`<div class="textbox">${this.text}</div>` : ''}
606
+ ${showText ? html`<div class="textbox">${effectiveText}</div>` : ''}
558
607
  ${rightIcon}
559
608
  </div>
560
609
  `;
@@ -644,9 +693,4 @@ export class DeesButton extends DeesElement {
644
693
  }
645
694
  this.faceElement?.click();
646
695
  }
647
-
648
- public async firstUpdated() {
649
- // Extract light DOM content (icon + text) and set as properties
650
- this.extractLightDom();
651
- }
652
696
  }
@@ -91,10 +91,7 @@ export class DeesFormSubmit extends DeesElement {
91
91
  }
92
92
 
93
93
  public async firstUpdated() {
94
- // Capture light DOM text content as the button label. dees-button wipes
95
- // its own light DOM during extractLightDom(), so we cannot simply forward
96
- // a <slot> into it — we have to hoist the text onto the .text property
97
- // ourselves before handing it to dees-button.
94
+ // Capture this wrapper's light-DOM text as the controlled label for its nested button.
98
95
  if (!this.text) {
99
96
  const slotText = this.textContent?.trim();
100
97
  if (slotText) {
@@ -82,6 +82,7 @@ export class DeesIcon extends DeesElement {
82
82
  */
83
83
  @property({
84
84
  type: String,
85
+ reflect: true,
85
86
  converter: {
86
87
  fromAttribute: (value: string): IconWithPrefix => value as IconWithPrefix,
87
88
  toAttribute: (value: IconWithPrefix): string => value