@haloduck/ui 2.0.20 → 2.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.
@@ -345,6 +345,7 @@ class SelectDropdownComponent {
345
345
  manualInputValues = {};
346
346
  activeManualKey = null;
347
347
  selectedChange = new EventEmitter();
348
+ closeDropdown = new EventEmitter();
348
349
  useFilter = false;
349
350
  multiselect = true;
350
351
  atLeastOne = false;
@@ -400,8 +401,13 @@ class SelectDropdownComponent {
400
401
  this.activeManualKey = null;
401
402
  }
402
403
  else {
403
- // open manual input without changing selection yet
404
+ // open manual input and handle selection based on mode
404
405
  this.activeManualKey = key;
406
+ // In single select mode, clear existing selections first
407
+ if (!this.multiselect) {
408
+ const sentinel = option.id || option.value;
409
+ this.setSelected([sentinel]);
410
+ }
405
411
  // ensure there is an entry in manualInputValues
406
412
  if (this.manualInputValues[key] === undefined) {
407
413
  this.manualInputValues[key] = '';
@@ -457,15 +463,19 @@ class SelectDropdownComponent {
457
463
  isOptionSelected(option) {
458
464
  const id = option.id || option.value;
459
465
  if (option.shouldManualInput) {
466
+ const key = this.getManualKey(option);
460
467
  const prefix = option.manualPrefix;
468
+ // Check if this manual input option is currently active
469
+ const isActive = this.activeManualKey === key;
461
470
  if (prefix) {
462
- // selected only if there exists a value with more than prefix
463
- return this._selectedOptionIds.some((selectedId) => typeof selectedId === 'string' && selectedId.startsWith(prefix) && selectedId.length > prefix.length);
471
+ // selected if there exists a value with more than prefix, OR if it's currently active
472
+ const hasValueWithPrefix = this._selectedOptionIds.some((selectedId) => typeof selectedId === 'string' && selectedId.startsWith(prefix) && selectedId.length > prefix.length);
473
+ return hasValueWithPrefix || isActive;
464
474
  }
465
- // no prefix: consider selected if any selected id equals current typed value (non-empty)
466
- const key = this.getManualKey(option);
475
+ // no prefix: consider selected if any selected id equals current typed value (non-empty) OR if it's active
467
476
  const typed = (this.manualInputValues[key] || '').trim();
468
- return typed !== '' && this._selectedOptionIds.includes(typed);
477
+ const hasTypedValue = typed !== '' && this._selectedOptionIds.includes(typed);
478
+ return hasTypedValue || isActive;
469
479
  }
470
480
  return this._selectedOptionIds.includes(id);
471
481
  }
@@ -500,14 +510,15 @@ class SelectDropdownComponent {
500
510
  onManualInputChange(option, value) {
501
511
  const key = this.getManualKey(option);
502
512
  this.manualInputValues[key] = value;
503
- // Dynamically reflect selection as user types, only when non-empty
513
+ // Dynamically reflect selection as user types
504
514
  const prefix = option.manualPrefix || '';
505
515
  const sentinel = option.id || option.value;
506
516
  const trimmed = (value || '').trim();
507
517
  if (this.multiselect) {
508
518
  if (option.isExclusive) {
509
519
  if (trimmed === '') {
510
- this.setSelected([]);
520
+ // Keep the sentinel selected to maintain the manual input option as "active"
521
+ this.setSelected([sentinel]);
511
522
  }
512
523
  else {
513
524
  this.setSelected([`${prefix}${trimmed}`]);
@@ -523,13 +534,20 @@ class SelectDropdownComponent {
523
534
  next = [toAdd, ...next];
524
535
  }
525
536
  }
537
+ else {
538
+ // Keep the sentinel to maintain selection state when text is empty
539
+ if (!next.includes(sentinel)) {
540
+ next = [sentinel, ...next];
541
+ }
542
+ }
526
543
  this.setSelected(next);
527
544
  }
528
545
  }
529
546
  else {
530
547
  // single select
531
548
  if (trimmed === '') {
532
- this.setSelected([]);
549
+ // Keep the sentinel selected to maintain the manual input option as "active"
550
+ this.setSelected([sentinel]);
533
551
  }
534
552
  else {
535
553
  this.setSelected([`${prefix}${trimmed}`]);
@@ -541,6 +559,10 @@ class SelectDropdownComponent {
541
559
  const raw = (this.manualInputValues[key] || '').trim();
542
560
  // Use same logic as onManualInputChange
543
561
  this.onManualInputChange(option, raw);
562
+ // Close dropdown in single select mode after confirming input
563
+ if (!this.multiselect) {
564
+ this.closeDropdown.emit();
565
+ }
544
566
  }
545
567
  onManualInputBlur(option) {
546
568
  const key = this.getManualKey(option);
@@ -548,6 +570,25 @@ class SelectDropdownComponent {
548
570
  if (raw === '') {
549
571
  // hide input on blur when empty
550
572
  this.activeManualKey = null;
573
+ // Also deselect the manual input option completely
574
+ const prefix = option.manualPrefix || '';
575
+ const sentinel = option.id || option.value;
576
+ if (this.multiselect) {
577
+ let next = [...this._selectedOptionIds];
578
+ // Remove sentinel and any values with the prefix
579
+ next = next.filter((id) => {
580
+ if (id === sentinel)
581
+ return false;
582
+ if (prefix && id.startsWith(prefix))
583
+ return false;
584
+ return true;
585
+ });
586
+ this.setSelected(next);
587
+ }
588
+ else {
589
+ // Single select: deselect completely
590
+ this.setSelected([]);
591
+ }
551
592
  }
552
593
  }
553
594
  emitSelectedChange() {
@@ -562,13 +603,15 @@ class SelectDropdownComponent {
562
603
  });
563
604
  }
564
605
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SelectDropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
565
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: SelectDropdownComponent, isStandalone: true, selector: "haloduck-select-dropdown", inputs: { useFilter: "useFilter", multiselect: "multiselect", atLeastOne: "atLeastOne", asButton: "asButton", options: "options", selectedOptionIds: "selectedOptionIds" }, outputs: { selectedChange: "selectedChange" }, providers: [provideTranslocoScope('haloduck')], ngImport: i0, template: "<div id=\"dropdown\"\n class=\"max-w-full mt-2 absolute z-40 bg-light-background dark:bg-dark-background text-light-on-background dark:text-dark-on-background border border-light-inactive dark:border-dark-inactive rounded max-h-60 flex flex-col gap-2\">\n @if (useFilter && _options.length >= 5) {\n <input #inputFilter\n id=\"inputFilter\"\n type=\"text\"\n [placeholder]=\"'haloduck.ui.select.Keyword...' | transloco\"\n (input)=\"onFilterInput($event)\"\n class=\"text-light-inactive dark:text-dark-inactive rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-3 py-1.5 text-sm/6 bg-light-control dark:bg-dark-control m-2\" />\n }\n <div class=\"overflow-y-auto\">\n @for ( option of _filteredOptions(); track (option.id) ? option.id : option.value) {\n <div class=\"px-3 py-2 text-sm/6 hover:bg-light-secondary/60 dark:hover:bg-dark-secondary/60 flex items-center justify-start whitespace-nowrap\"\n [class.cursor-pointer]=\"!option.shouldManualInput\"\n (click)=\"onToggleOption(option)\">\n @if(!asButton) {\n @if( isOptionSelected(option)) {\n <svg class=\"w-4 h-4 text-light-primary dark:text-dark-primary inline-block mr-2\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\">\n <path fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z\"\n clip-rule=\"evenodd\" />\n </svg>\n } @else {\n <div class=\"w-4 h-4 inline-block mr-2\"></div>\n }\n }\n @if(option.shouldManualInput && (isOptionSelected(option) || (activeManualKey === (option.manualPrefix ? option.manualPrefix : (option.id || option.value))))) {\n <input type=\"text\"\n [(ngModel)]=\"manualInputValues[option.manualPrefix ? option.manualPrefix : (option.id || option.value)]\"\n [attr.data-manual-key]=\"option.manualPrefix ? option.manualPrefix : (option.id || option.value)\"\n (ngModelChange)=\"onManualInputChange(option, $event)\"\n (click)=\"$event.stopPropagation()\"\n (blur)=\"onManualInputBlur(option)\"\n (keydown.enter)=\"onManualInputConfirm(option)\"\n class=\"text-light-on-control dark:text-dark-on-control rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-2 py-1 text-sm/6 bg-light-control dark:bg-dark-control w-full\" />\n } @else {\n {{ option.value }}\n }\n </div>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: TranslocoModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
606
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.4", type: SelectDropdownComponent, isStandalone: true, selector: "haloduck-select-dropdown", inputs: { useFilter: "useFilter", multiselect: "multiselect", atLeastOne: "atLeastOne", asButton: "asButton", options: "options", selectedOptionIds: "selectedOptionIds" }, outputs: { selectedChange: "selectedChange", closeDropdown: "closeDropdown" }, providers: [provideTranslocoScope('haloduck')], ngImport: i0, template: "<div id=\"dropdown\"\n class=\"max-w-full mt-2 absolute z-40 bg-light-background dark:bg-dark-background text-light-on-background dark:text-dark-on-background border border-light-inactive dark:border-dark-inactive rounded max-h-60 flex flex-col gap-2\">\n @if (useFilter && _options.length >= 5) {\n <input #inputFilter\n id=\"inputFilter\"\n type=\"text\"\n [placeholder]=\"'haloduck.ui.select.Keyword...' | transloco\"\n (input)=\"onFilterInput($event)\"\n class=\"text-light-inactive dark:text-dark-inactive rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-3 py-1.5 text-sm/6 bg-light-control dark:bg-dark-control m-2\" />\n }\n <div class=\"overflow-y-auto\">\n @for ( option of _filteredOptions(); track (option.id) ? option.id : option.value) {\n <div class=\"px-3 py-2 text-sm/6 hover:bg-light-secondary/60 dark:hover:bg-dark-secondary/60 flex items-center justify-start whitespace-nowrap\"\n [class.cursor-pointer]=\"!option.shouldManualInput\"\n (click)=\"onToggleOption(option)\">\n @if(!asButton) {\n @if( isOptionSelected(option)) {\n <svg class=\"w-4 h-4 text-light-primary dark:text-dark-primary inline-block mr-2\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\">\n <path fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z\"\n clip-rule=\"evenodd\" />\n </svg>\n } @else {\n <div class=\"w-4 h-4 inline-block mr-2\"></div>\n }\n }\n @if(option.shouldManualInput && (isOptionSelected(option) || (activeManualKey === (option.manualPrefix ? option.manualPrefix : (option.id || option.value))))) {\n <input type=\"text\"\n [(ngModel)]=\"manualInputValues[option.manualPrefix ? option.manualPrefix : (option.id || option.value)]\"\n [attr.data-manual-key]=\"option.manualPrefix ? option.manualPrefix : (option.id || option.value)\"\n (ngModelChange)=\"onManualInputChange(option, $event)\"\n (click)=\"$event.stopPropagation()\"\n (blur)=\"onManualInputBlur(option)\"\n (keydown.enter)=\"onManualInputConfirm(option)\"\n class=\"text-light-on-control dark:text-dark-on-control rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-2 py-1 text-sm/6 bg-light-control dark:bg-dark-control w-full\" />\n } @else {\n {{ option.value }}\n }\n </div>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: TranslocoModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i2$1.TranslocoPipe, name: "transloco" }] });
566
607
  }
567
608
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: SelectDropdownComponent, decorators: [{
568
609
  type: Component,
569
610
  args: [{ selector: 'haloduck-select-dropdown', imports: [TranslocoModule, FormsModule], providers: [provideTranslocoScope('haloduck')], template: "<div id=\"dropdown\"\n class=\"max-w-full mt-2 absolute z-40 bg-light-background dark:bg-dark-background text-light-on-background dark:text-dark-on-background border border-light-inactive dark:border-dark-inactive rounded max-h-60 flex flex-col gap-2\">\n @if (useFilter && _options.length >= 5) {\n <input #inputFilter\n id=\"inputFilter\"\n type=\"text\"\n [placeholder]=\"'haloduck.ui.select.Keyword...' | transloco\"\n (input)=\"onFilterInput($event)\"\n class=\"text-light-inactive dark:text-dark-inactive rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-3 py-1.5 text-sm/6 bg-light-control dark:bg-dark-control m-2\" />\n }\n <div class=\"overflow-y-auto\">\n @for ( option of _filteredOptions(); track (option.id) ? option.id : option.value) {\n <div class=\"px-3 py-2 text-sm/6 hover:bg-light-secondary/60 dark:hover:bg-dark-secondary/60 flex items-center justify-start whitespace-nowrap\"\n [class.cursor-pointer]=\"!option.shouldManualInput\"\n (click)=\"onToggleOption(option)\">\n @if(!asButton) {\n @if( isOptionSelected(option)) {\n <svg class=\"w-4 h-4 text-light-primary dark:text-dark-primary inline-block mr-2\"\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n aria-hidden=\"true\">\n <path fill-rule=\"evenodd\"\n d=\"M16.707 5.293a1 1 0 00-1.414 0L8 12.586 4.707 9.293a1 1 0 00-1.414 1.414l4 4a1 1 0 001.414 0l8-8a1 1 0 000-1.414z\"\n clip-rule=\"evenodd\" />\n </svg>\n } @else {\n <div class=\"w-4 h-4 inline-block mr-2\"></div>\n }\n }\n @if(option.shouldManualInput && (isOptionSelected(option) || (activeManualKey === (option.manualPrefix ? option.manualPrefix : (option.id || option.value))))) {\n <input type=\"text\"\n [(ngModel)]=\"manualInputValues[option.manualPrefix ? option.manualPrefix : (option.id || option.value)]\"\n [attr.data-manual-key]=\"option.manualPrefix ? option.manualPrefix : (option.id || option.value)\"\n (ngModelChange)=\"onManualInputChange(option, $event)\"\n (click)=\"$event.stopPropagation()\"\n (blur)=\"onManualInputBlur(option)\"\n (keydown.enter)=\"onManualInputConfirm(option)\"\n class=\"text-light-on-control dark:text-dark-on-control rounded-md outline -outline-offset-1 outline-light-inactive dark:outline-dark-inactive focus:outline-2 focus:outline-offset-2 focus:outline-light-primary dark:focus:outline-dark-primary px-2 py-1 text-sm/6 bg-light-control dark:bg-dark-control w-full\" />\n } @else {\n {{ option.value }}\n }\n </div>\n }\n </div>\n</div>\n" }]
570
611
  }], propDecorators: { selectedChange: [{
571
612
  type: Output
613
+ }], closeDropdown: [{
614
+ type: Output
572
615
  }], useFilter: [{
573
616
  type: Input
574
617
  }], multiselect: [{
@@ -685,6 +728,10 @@ class SelectComponent {
685
728
  }
686
729
  });
687
730
  });
731
+ componentRef.instance.closeDropdown.subscribe(() => {
732
+ this.isDropdownOpen.set(false);
733
+ this.overlayRef?.detach();
734
+ });
688
735
  this.isDropdownOpen.set(true);
689
736
  }
690
737
  else {