@flywheel-io/vision 19.2.0 → 19.3.0

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.
@@ -6253,7 +6253,7 @@ class FwSelectMenuComponent {
6253
6253
  }
6254
6254
  }
6255
6255
  get disabledClass() {
6256
- return this.disabled;
6256
+ return this.disabled();
6257
6257
  }
6258
6258
  get value() {
6259
6259
  return this._value;
@@ -6261,36 +6261,59 @@ class FwSelectMenuComponent {
6261
6261
  set value(newValue) {
6262
6262
  this.updateValue(newValue);
6263
6263
  }
6264
- constructor(_changeDetectorRef, ngControl) {
6265
- this._changeDetectorRef = _changeDetectorRef;
6264
+ constructor(ngControl) {
6266
6265
  this.ngControl = ngControl;
6267
- // bind it for the template
6268
- this.JSON = JSON;
6269
- this.options = [];
6270
- this.valueProperty = 'value';
6271
- this.useFullOptionAsValue = false;
6272
- this.titleProperty = 'title';
6273
- this.iconProperty = 'icon';
6274
- this.descriptionProperty = 'description';
6275
- this.showFilter = false;
6276
- this.showReset = false;
6277
- this.disabled = false;
6278
- this.errored = false;
6279
- this.width = '200px';
6280
- this.size = 'medium';
6281
- this.placeholder = 'Select something...';
6266
+ this.options = input([]);
6267
+ this.valueProperty = input('value');
6268
+ this.useFullOptionAsValue = input(false);
6269
+ this.titleProperty = input('title');
6270
+ this.iconProperty = input('icon');
6271
+ this.staticIcon = input(undefined);
6272
+ this.descriptionProperty = input('description');
6273
+ this.showFilter = input(false);
6274
+ this.showReset = input(false);
6275
+ this.disabled = input(false);
6276
+ this.errored = input(false);
6277
+ this.width = input('200px');
6278
+ this.optionsWidth = input(undefined);
6279
+ this.minOptionsHeight = input(undefined);
6280
+ this.maxOptionsHeight = input(undefined);
6281
+ this.size = input('medium');
6282
+ this.placeholder = input('Select something...');
6282
6283
  // eslint-disable-next-line @angular-eslint/no-output-native
6283
6284
  this.change = new EventEmitter();
6284
6285
  this.filterChanged = new EventEmitter();
6285
6286
  this.selectValue = '';
6286
- this.selectTitle = '';
6287
+ this.selectTitle = signal('');
6287
6288
  this.selectIcon = '';
6288
- this.filterValue = '';
6289
+ this.filterValue = signal('');
6289
6290
  this.subscriptions = [];
6290
6291
  this._isOpen = false;
6291
6292
  this.focused = 0;
6292
6293
  this.inFocusOpen = false;
6294
+ this.isTyping = signal(false);
6295
+ // Computed signal for the input display value
6296
+ this.inputDisplayValue = computed(() => this.isTyping() ? this.filterValue() : this.selectTitle());
6293
6297
  this._value = '';
6298
+ this.filteredOptions = computed(() => {
6299
+ const filter = this.filterValue();
6300
+ const opts = this.options();
6301
+ const tProp = this.titleProperty();
6302
+ if (!filter || filter.trim() === '') {
6303
+ return opts;
6304
+ }
6305
+ return opts.filter(opt => opt[tProp]?.toString().toLowerCase()
6306
+ .includes(filter.toLowerCase()));
6307
+ });
6308
+ this.optionsWithValues = computed(() => {
6309
+ const useFull = this.useFullOptionAsValue();
6310
+ const valProp = this.valueProperty();
6311
+ return this.filteredOptions().map(item => ({
6312
+ raw: item,
6313
+ trackingId: useFull ? JSON.stringify(item) : item?.[valProp]?.toString(),
6314
+ value: useFull ? JSON.stringify(item) : item?.[valProp]?.toString(),
6315
+ }));
6316
+ });
6294
6317
  this.onTouched = () => {
6295
6318
  };
6296
6319
  // this is just a different way of binding the controlValueAccessor
@@ -6298,31 +6321,29 @@ class FwSelectMenuComponent {
6298
6321
  if (this.ngControl) {
6299
6322
  this.ngControl.valueAccessor = this;
6300
6323
  }
6301
- }
6302
- ngOnChanges(changes) {
6303
- const currentOptions = changes.options?.currentValue;
6304
- // if the options change check if the title we should be displaying has changed
6305
- if (currentOptions && currentOptions !== changes.options?.previousValue) {
6306
- const selectedOption = currentOptions.find(item => item[this.valueProperty]?.toString() === this.selectValue);
6324
+ // Watch for options changes to update the displayed title
6325
+ effect(() => {
6326
+ const currentOptions = this.options();
6327
+ const vProp = this.valueProperty();
6328
+ const tProp = this.titleProperty();
6329
+ if (!currentOptions || currentOptions.length === 0) {
6330
+ return;
6331
+ }
6332
+ const selectedOption = currentOptions.find(item => item[vProp]?.toString() === this.selectValue);
6307
6333
  if (selectedOption) {
6308
- this.selectTitle = selectedOption[this.titleProperty];
6334
+ this.selectTitle.set(selectedOption[tProp]);
6309
6335
  }
6310
- }
6336
+ });
6311
6337
  }
6312
6338
  ngAfterContentInit() {
6313
- if (!this.options || (this.options.length === 0 && this.menuItems && this.menuItems.length > 0)) {
6314
- this.options = [];
6339
+ // When using content projection with <fw-menu-item> components,
6340
+ // subscribe to their click events and set initial selected state
6341
+ if (this.menuItems && this.menuItems.length > 0) {
6315
6342
  this.menuItems.forEach(item => {
6316
- this.options.push({
6317
- value: item.value.toString(),
6318
- title: item.title.toString(),
6319
- icon: item.icon,
6320
- description: item.description,
6321
- });
6322
6343
  const sub = item.click.subscribe(value => this.menu.writeValue(value));
6323
6344
  this.subscriptions.push(sub);
6324
6345
  if (item.value.toString() === this.selectValue) {
6325
- this.selectTitle = item.title.toString();
6346
+ this.selectTitle.set(item.title.toString());
6326
6347
  this.selectIcon = item.icon;
6327
6348
  }
6328
6349
  });
@@ -6351,11 +6372,6 @@ class FwSelectMenuComponent {
6351
6372
  registerOnTouched(fn) {
6352
6373
  this.onTouched = fn;
6353
6374
  }
6354
- setDisabledState(isDisabled) {
6355
- this.disabled = isDisabled;
6356
- // eslint-disable-next-line @rx-angular/no-explicit-change-detection-apis
6357
- this._changeDetectorRef.markForCheck();
6358
- }
6359
6375
  writeValue(value) {
6360
6376
  value = value ?? '';
6361
6377
  if (value === this.value) {
@@ -6369,20 +6385,80 @@ class FwSelectMenuComponent {
6369
6385
  this.close();
6370
6386
  return;
6371
6387
  }
6372
- if (this.useFullOptionAsValue) {
6388
+ if (this.useFullOptionAsValue()) {
6373
6389
  const parsedValue = JSON.parse(e);
6374
6390
  this.updateValue(parsedValue);
6375
6391
  }
6376
6392
  else {
6377
6393
  this.updateValue(e);
6378
6394
  }
6395
+ this.isTyping.set(false);
6379
6396
  this.close();
6397
+ this.inFocusOpen = false;
6398
+ }
6399
+ /**
6400
+ * Get all available items for navigation, either from options or menuItems
6401
+ */
6402
+ getAvailableItems() {
6403
+ // If using options input, return filtered options
6404
+ if (this.options().length > 0) {
6405
+ return this.filteredOptions();
6406
+ }
6407
+ // If using content projection, return non-disabled menu items
6408
+ if (this.menuItems && this.menuItems.length > 0) {
6409
+ return this.menuItems.filter(item => !item.disabled);
6410
+ }
6411
+ return [];
6412
+ }
6413
+ /**
6414
+ * Update highlighting for the currently selected item
6415
+ */
6416
+ updateHighlighting() {
6417
+ const currentValue = this.selectValue;
6418
+ // Update highlighting for content-projected menu items
6419
+ if (this.menuItems && this.menuItems.length > 0) {
6420
+ this.menuItems.forEach(item => {
6421
+ item.selected = item.value === currentValue;
6422
+ });
6423
+ }
6424
+ // Update highlighting for options-based menu items via menu component
6425
+ if (this.menu && this.options().length > 0) {
6426
+ this.menu.value = currentValue;
6427
+ this.menu.updateLayout();
6428
+ }
6429
+ }
6430
+ /**
6431
+ * Initialize focused index to the currently selected item
6432
+ */
6433
+ initializeFocusedIndex() {
6434
+ const availableItems = this.getAvailableItems();
6435
+ const currentValue = this.selectValue;
6436
+ // Find the index of the currently selected item
6437
+ const selectedIndex = availableItems.findIndex(item => {
6438
+ if (item instanceof FwMenuItemComponent) {
6439
+ return item.value === currentValue;
6440
+ }
6441
+ else {
6442
+ // For options array items
6443
+ const vProp = this.valueProperty();
6444
+ const itemValue = this.useFullOptionAsValue()
6445
+ ? JSON.stringify(item)
6446
+ : item?.[vProp]?.toString();
6447
+ return itemValue === currentValue;
6448
+ }
6449
+ });
6450
+ // Set focused to the selected index
6451
+ // If no item is selected (selectedIndex is -1), keep it at -1 so first arrow down goes to index 0
6452
+ this.focused = selectedIndex;
6453
+ // Also update the highlighting to show the current selection (if any)
6454
+ this.updateHighlighting();
6380
6455
  }
6381
6456
  moveFocused(direction) {
6457
+ const availableItems = this.getAvailableItems();
6382
6458
  switch (direction) {
6383
6459
  case 'down': {
6384
6460
  this.focused++;
6385
- if (this.focused >= this.options.length) {
6461
+ if (this.focused >= availableItems.length) {
6386
6462
  this.focused = 0;
6387
6463
  }
6388
6464
  break;
@@ -6390,7 +6466,7 @@ class FwSelectMenuComponent {
6390
6466
  case 'up': {
6391
6467
  this.focused--;
6392
6468
  if (this.focused < 0) {
6393
- this.focused = this.options.length - 1;
6469
+ this.focused = availableItems.length - 1;
6394
6470
  }
6395
6471
  break;
6396
6472
  }
@@ -6400,32 +6476,205 @@ class FwSelectMenuComponent {
6400
6476
  }
6401
6477
  }
6402
6478
  }
6479
+ handleFocus() {
6480
+ // Select all text when focusing the input
6481
+ if (this.textInput?.inputRef?.nativeElement && !this.isTyping()) {
6482
+ const input = this.textInput.inputRef.nativeElement;
6483
+ // Select all text immediately - this will select the current value
6484
+ input.select();
6485
+ }
6486
+ }
6487
+ handleInputClick() {
6488
+ // When clicking into the input, select all text if not already typing
6489
+ if (this.textInput?.inputRef?.nativeElement && !this.isTyping()) {
6490
+ const input = this.textInput.inputRef.nativeElement;
6491
+ // Use setTimeout to override the click's cursor positioning
6492
+ setTimeout(() => {
6493
+ input.select();
6494
+ }, 0);
6495
+ }
6496
+ // Initialize highlighting when dropdown opens via click
6497
+ // Use setTimeout to ensure menu is rendered before we try to update it
6498
+ setTimeout(() => {
6499
+ if (this.trigger?.isOpen()) {
6500
+ this.inFocusOpen = true;
6501
+ this.preFocusValue = this.value;
6502
+ this.initializeFocusedIndex();
6503
+ }
6504
+ }, 0);
6505
+ }
6403
6506
  handleKeyDown(event) {
6507
+ // Handle Enter key when typing
6508
+ if (event.key === 'Enter' && this.isTyping()) {
6509
+ event.preventDefault();
6510
+ this.inFocusOpen = false;
6511
+ // Select the currently focused option (respects arrow key navigation)
6512
+ const availableItems = this.getAvailableItems();
6513
+ let newValue;
6514
+ if (availableItems.length > 0) {
6515
+ // Select the item at the focused index (which may have been changed by arrow keys)
6516
+ const selectedItem = availableItems[this.focused];
6517
+ // Handle both options (objects) and menuItems (FwMenuItemComponent)
6518
+ if (selectedItem instanceof FwMenuItemComponent) {
6519
+ newValue = selectedItem.value;
6520
+ }
6521
+ else {
6522
+ newValue = selectedItem;
6523
+ }
6524
+ }
6525
+ else {
6526
+ // No available items, keep current value
6527
+ if (this.useFullOptionAsValue()) {
6528
+ try {
6529
+ newValue = JSON.parse(this.value);
6530
+ }
6531
+ catch {
6532
+ // If parse fails, keep the raw value
6533
+ newValue = this.value;
6534
+ }
6535
+ }
6536
+ else {
6537
+ newValue = this.value;
6538
+ }
6539
+ }
6540
+ // Clear filter and exit typing mode FIRST
6541
+ // This prevents onInputChange from interfering
6542
+ this.filterValue.set('');
6543
+ this.isTyping.set(false);
6544
+ // Update the value (this will set selectTitle signal)
6545
+ this.updateValue(newValue);
6546
+ this.close();
6547
+ return;
6548
+ }
6549
+ // Handle backspace to enter typing mode - only if text is selected or at the beginning
6550
+ if (event.key === 'Backspace' && !this.isTyping() && this.selectTitle()) {
6551
+ // Check if there's a text selection
6552
+ let hasSelection = false;
6553
+ if (this.textInput?.inputRef?.nativeElement) {
6554
+ const input = this.textInput.inputRef.nativeElement;
6555
+ const selectionStart = input.selectionStart ?? 0;
6556
+ const selectionEnd = input.selectionEnd ?? 0;
6557
+ hasSelection = selectionStart !== selectionEnd;
6558
+ }
6559
+ // Only enter typing mode if there's a selection (e.g., from clicking/tabbing in)
6560
+ // Otherwise, let the browser handle backspace naturally without entering typing mode
6561
+ if (hasSelection) {
6562
+ this.isTyping.set(true);
6563
+ this.trigger.open();
6564
+ this._isOpen = true;
6565
+ this.inFocusOpen = true;
6566
+ this.preFocusValue = this.value;
6567
+ this.initializeFocusedIndex();
6568
+ }
6569
+ // Let the default backspace behavior happen regardless
6570
+ return;
6571
+ }
6572
+ // Handle backspace while already typing
6573
+ if (event.key === 'Backspace' && this.isTyping()) {
6574
+ // Let default behavior happen, onInputChange will handle it
6575
+ return;
6576
+ }
6577
+ // Handle regular characters when entering typing mode (not already typing)
6578
+ // Exclude navigation keys like Arrow keys, Home, End, etc.
6579
+ const isNavigationKey = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown', 'Delete'].includes(event.key);
6580
+ if (!this.trigger.isOpen() && !this.isTyping() && this.selectTitle() && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey && !isNavigationKey) {
6581
+ // Don't prevent default - let the browser handle the character insertion naturally
6582
+ // Just switch to typing mode and the input event will update filterValue
6583
+ this.isTyping.set(true);
6584
+ this.trigger.open();
6585
+ this._isOpen = true;
6586
+ this.inFocusOpen = true;
6587
+ this.preFocusValue = this.value;
6588
+ this.initializeFocusedIndex();
6589
+ // The input event will fire after this keydown, which will update filterValue
6590
+ // and trigger the dropdown to show filtered options
6591
+ return;
6592
+ }
6593
+ // Handle regular characters while already typing (including space)
6594
+ if (this.isTyping() && event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
6595
+ // Let default behavior happen, onInputChange will handle it
6596
+ return;
6597
+ }
6404
6598
  if (this.trigger.isOpen()) {
6405
6599
  if (this.inFocusOpen) {
6406
6600
  if (event.key === 'ArrowDown') {
6601
+ event.preventDefault();
6407
6602
  this.moveFocused('down');
6408
- this.updateValue(this.options[this.focused]);
6603
+ // Defer updateValue to avoid change detection errors
6604
+ setTimeout(() => {
6605
+ const availableItems = this.getAvailableItems();
6606
+ if (availableItems.length > 0) {
6607
+ const selectedItem = availableItems[this.focused];
6608
+ // Handle both options (objects) and menuItems (FwMenuItemComponent)
6609
+ if (selectedItem instanceof FwMenuItemComponent) {
6610
+ this.selectValue = selectedItem.value;
6611
+ // Manually update selected state for all menu items
6612
+ this.menuItems?.forEach(item => {
6613
+ item.selected = item.value === this.selectValue;
6614
+ });
6615
+ this.updateValue(selectedItem.value);
6616
+ }
6617
+ else {
6618
+ // For options array items, set selectValue first for immediate highlighting
6619
+ const vProp = this.valueProperty();
6620
+ this.selectValue = this.useFullOptionAsValue()
6621
+ ? JSON.stringify(selectedItem)
6622
+ : selectedItem?.[vProp]?.toString();
6623
+ if (this.menu) {
6624
+ this.menu.value = this.selectValue;
6625
+ this.menu.updateLayout();
6626
+ }
6627
+ this.updateValue(selectedItem);
6628
+ }
6629
+ }
6630
+ }, 0);
6409
6631
  }
6410
- if (event.key === 'ArrowUp') {
6632
+ else if (event.key === 'ArrowUp') {
6633
+ event.preventDefault();
6411
6634
  this.moveFocused('up');
6412
- this.updateValue(this.options[this.focused]);
6413
- }
6414
- if (event.key === 'Tab') {
6415
- this.close();
6416
- this.inFocusOpen = false;
6417
- this.updateValue(this.preFocusValue);
6635
+ // Defer updateValue to avoid change detection errors
6636
+ setTimeout(() => {
6637
+ const availableItems = this.getAvailableItems();
6638
+ if (availableItems.length > 0) {
6639
+ const selectedItem = availableItems[this.focused];
6640
+ // Handle both options (objects) and menuItems (FwMenuItemComponent)
6641
+ if (selectedItem instanceof FwMenuItemComponent) {
6642
+ this.selectValue = selectedItem.value;
6643
+ // Manually update selected state for all menu items
6644
+ this.menuItems?.forEach(item => {
6645
+ item.selected = item.value === this.selectValue;
6646
+ });
6647
+ this.updateValue(selectedItem.value);
6648
+ }
6649
+ else {
6650
+ // For options array items, set selectValue first for immediate highlighting
6651
+ const vProp = this.valueProperty();
6652
+ this.selectValue = this.useFullOptionAsValue()
6653
+ ? JSON.stringify(selectedItem)
6654
+ : selectedItem?.[vProp]?.toString();
6655
+ if (this.menu) {
6656
+ this.menu.value = this.selectValue;
6657
+ this.menu.updateLayout();
6658
+ }
6659
+ this.updateValue(selectedItem);
6660
+ }
6661
+ }
6662
+ }, 0);
6418
6663
  }
6419
- if (event.key === 'Enter') {
6664
+ else if (event.key === 'Tab') {
6665
+ this.isTyping.set(false);
6420
6666
  this.close();
6421
6667
  this.inFocusOpen = false;
6422
- const newValue = this.useFullOptionAsValue ? this.JSON.parse(this.value) : this.value;
6423
- this.updateValue(newValue);
6668
+ // Defer updateValue to avoid change detection errors
6669
+ setTimeout(() => {
6670
+ this.updateValue(this.preFocusValue);
6671
+ }, 0);
6424
6672
  }
6425
6673
  }
6426
6674
  else {
6427
6675
  this.inFocusOpen = true;
6428
6676
  this.preFocusValue = this.value;
6677
+ this.initializeFocusedIndex();
6429
6678
  }
6430
6679
  }
6431
6680
  else {
@@ -6438,6 +6687,7 @@ class FwSelectMenuComponent {
6438
6687
  handleKeyUp(event) {
6439
6688
  if (this.trigger.isOpen()) {
6440
6689
  if (event.key === 'Escape') {
6690
+ this.isTyping.set(false);
6441
6691
  this.close();
6442
6692
  this.inFocusOpen = false;
6443
6693
  this.updateValue(this.preFocusValue);
@@ -6454,57 +6704,118 @@ class FwSelectMenuComponent {
6454
6704
  updateValue(newValue) {
6455
6705
  // do housekeeping first
6456
6706
  this.onTouched();
6707
+ const vProp = this.valueProperty();
6708
+ const tProp = this.titleProperty();
6709
+ const iProp = this.iconProperty();
6710
+ const fullOption = this.useFullOptionAsValue();
6457
6711
  // null guard
6458
6712
  if (!newValue) {
6459
6713
  this.selectValue = '';
6460
- this.selectTitle = '';
6714
+ this.selectTitle.set('');
6461
6715
  this.selectIcon = '';
6462
6716
  return this.onChange(newValue);
6463
6717
  }
6464
6718
  if (typeof newValue === 'object') {
6465
- this.selectValue = newValue?.[this.valueProperty]?.toString();
6466
- this.selectTitle = newValue?.[this.titleProperty]?.toString();
6467
- this.selectIcon = newValue?.[this.iconProperty];
6468
- const changeToEmit = this.useFullOptionAsValue ? newValue : newValue[this.valueProperty];
6719
+ this.selectValue = newValue?.[vProp]?.toString();
6720
+ this.selectTitle.set(newValue?.[tProp]?.toString());
6721
+ this.selectIcon = newValue?.[iProp];
6722
+ const changeToEmit = fullOption ? newValue : newValue[vProp];
6469
6723
  return this.onChange(changeToEmit);
6470
6724
  }
6471
- // try and find a matching option
6472
- const matchedOption = this.options.find(option => {
6473
- const matchesValue = option[this.valueProperty] === newValue;
6474
- const matchesTitle = option[this.titleProperty] === newValue;
6725
+ // try and find a matching option in the options array
6726
+ const matchedOption = this.options().find(option => {
6727
+ const matchesValue = option[vProp] === newValue || option[vProp]?.toString() === newValue?.toString();
6728
+ const matchesTitle = option[tProp] === newValue || option[tProp]?.toString() === newValue?.toString();
6475
6729
  return matchesValue || matchesTitle;
6476
6730
  });
6477
6731
  if (matchedOption) {
6478
- this.selectValue = matchedOption[this.valueProperty]?.toString();
6479
- this.selectTitle = matchedOption[this.titleProperty]?.toString();
6480
- this.selectIcon = matchedOption[this.iconProperty];
6481
- const changeToEmit = this.useFullOptionAsValue ? matchedOption : matchedOption[this.valueProperty];
6732
+ this.selectValue = matchedOption[vProp]?.toString();
6733
+ this.selectTitle.set(matchedOption[tProp]?.toString());
6734
+ this.selectIcon = matchedOption[iProp];
6735
+ const changeToEmit = fullOption ? matchedOption : matchedOption[vProp];
6482
6736
  return this.onChange(changeToEmit);
6483
6737
  }
6484
- else {
6485
- // fall back to stringify
6486
- const stringified = newValue.toString() || '';
6487
- this.selectValue = stringified;
6488
- this.selectTitle = stringified;
6489
- return this.onChange(stringified);
6738
+ // try and find a matching menu item in content projected items
6739
+ const matchedMenuItem = this.menuItems?.find(item => item.value === newValue);
6740
+ if (matchedMenuItem) {
6741
+ this.selectValue = matchedMenuItem.value?.toString();
6742
+ this.selectTitle.set(matchedMenuItem.title);
6743
+ this.selectIcon = matchedMenuItem.icon;
6744
+ return this.onChange(matchedMenuItem.value);
6490
6745
  }
6746
+ // fall back to stringify
6747
+ const stringified = newValue.toString() || '';
6748
+ this.selectValue = stringified;
6749
+ this.selectTitle.set(stringified);
6750
+ return this.onChange(stringified);
6491
6751
  }
6492
6752
  handleReset() {
6493
- if (this.showReset) {
6753
+ if (this.showReset()) {
6494
6754
  this.updateValue(null);
6495
6755
  }
6496
6756
  }
6497
6757
  close() {
6498
6758
  this.trigger.close();
6499
- this.filterValue = '';
6500
- this.filterChanged.emit(this.filterValue);
6759
+ this.filterValue.set('');
6760
+ this.filterChanged.emit(this.filterValue());
6761
+ this._isOpen = false;
6762
+ this.isTyping.set(false);
6501
6763
  }
6502
6764
  onFilterChanged(value) {
6503
- this.filterValue = value;
6765
+ // Don't let the menu-container overwrite our filterValue when we're typing
6766
+ if (this.isTyping()) {
6767
+ return;
6768
+ }
6769
+ this.filterValue.set(value);
6504
6770
  this.filterChanged.emit(value);
6505
6771
  }
6506
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: FwSelectMenuComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i1$4.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component }); }
6507
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.18", type: FwSelectMenuComponent, isStandalone: true, selector: "fw-select", inputs: { options: "options", valueProperty: "valueProperty", useFullOptionAsValue: "useFullOptionAsValue", titleProperty: "titleProperty", iconProperty: "iconProperty", staticIcon: "staticIcon", descriptionProperty: "descriptionProperty", showFilter: "showFilter", showReset: "showReset", disabled: "disabled", errored: "errored", width: "width", optionsWidth: "optionsWidth", minOptionsHeight: "minOptionsHeight", maxOptionsHeight: "maxOptionsHeight", size: "size", placeholder: "placeholder", value: "value" }, outputs: { change: "change", filterChanged: "filterChanged" }, host: { listeners: { "document:click": "outsideClick($event.target)" }, properties: { "class.disabled": "this.disabledClass" } }, queries: [{ propertyName: "menuItems", predicate: FwMenuItemComponent, descendants: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: CdkMenuTrigger, descendants: true }, { propertyName: "menu", first: true, predicate: FwMenuComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div #wrapper [ngStyle]=\"{width: width, cursor: 'pointer'}\">\n <fw-text-input\n fwMenuRegister\n [cdkMenuTriggerFor]=\"selectMenu\"\n [value]=\"selectTitle\"\n [leftIcon]=\"staticIcon || selectIcon || null\"\n [rightIcon]=\"(selectTitle&&showReset)?'close-circled':'chevron-down'\"\n (rightIconAction)=\"handleReset()\"\n [useActionableIcons]=\"true\"\n [placeholder]=\"placeholder\"\n [size]=\"size\"\n [error]=\"errored || (invalid && touched)\"\n (keyup)=\"handleKeyUp($event)\"\n (keydown)=\"handleKeyDown($event)\"\n [readOnly]=\"true\">\n </fw-text-input>\n <ng-template #selectMenu>\n <fw-menu-container\n [filterText]=\"filterValue\"\n *ngIf=\"!disabled\" [showFilter]=\"showFilter\" [width]=\"optionsWidth || wrapper.offsetWidth + 'px'\"\n [maxHeight]=\"maxOptionsHeight\" [minHeight]=\"minOptionsHeight\" (filterChanged)=\"onFilterChanged($event)\">\n <fw-menu [disabled]=\"disabled\" [value]=\"selectValue\" (change)=\"handleClick($any($event))\">\n <ng-container *ngIf=\"menuItems.length===0\">\n <fw-menu-item\n *ngFor=\"let item of options\"\n [title]=\"item[titleProperty]?.toString()\"\n [description]=\"item[descriptionProperty]\"\n [value]=\"useFullOptionAsValue ? JSON.stringify(item) : item?.[valueProperty]?.toString()\"\n [icon]=\"item[iconProperty]\"\n [disabled]=\"$any(item).disabled\"\n >\n </fw-menu-item>\n </ng-container>\n <div #menuContentWrapper>\n <ng-content select=\"[fw-menu-item, fw-menu-separator, fw-menu-item-group]\"></ng-content>\n </div>\n </fw-menu>\n </fw-menu-container>\n </ng-template>\n</div>\n", styles: [":host{box-sizing:border-box;max-width:100%}:host.disabled{opacity:.4;cursor:not-allowed}:host.disabled>div{pointer-events:none}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: FwTextInputComponent, selector: "fw-text-input", inputs: ["disabled", "useActionableIcons", "leftIcon", "rightIcon", "prefix", "context", "helperText", "errorText", "errorInIconTooltip", "placeholder", "readOnly", "size", "type", "maxLength", "autofocus", "autocomplete", "value", "error", "width"], outputs: ["leftIconAction", "rightIconAction"] }, { kind: "directive", type: MenuRegisterDirective, selector: "[fwMenuRegister]" }, { kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FwMenuContainerComponent, selector: "fw-menu-container, fw-menu-filter", inputs: ["width", "maxHeight", "minHeight", "border", "shadow", "showFilter", "filterText", "focusFilterOnMount", "offset", "emptyText", "filterFn", "additionalMenuItems", "keyHandler"], outputs: ["filteredMenuItemChange", "filterChanged"] }, { kind: "component", type: FwMenuComponent, selector: "fw-menu", inputs: ["disabled", "size", "multiSelect", "useCheckbox", "value"], outputs: ["change"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "component", type: FwMenuItemComponent, selector: "fw-menu-item", inputs: ["value", "size", "title", "description", "icon", "iconColor", "disabled", "showCheckbox", "checkboxColor", "multiSelect", "hidden", "collapsed", "href", "target", "subItemsOpen", "mouseEnterHandler", "focused", "selected"], outputs: ["mouseEnterHandlerChange", "click"] }] }); }
6772
+ onInputChange(event) {
6773
+ const inputElement = event.target;
6774
+ const value = inputElement.value;
6775
+ // Update filterValue with the current input value
6776
+ this.filterValue.set(value);
6777
+ // If there's a filter value and we're not already typing, enter typing mode
6778
+ if (value && !this.isTyping()) {
6779
+ this.isTyping.set(true);
6780
+ this.inFocusOpen = true;
6781
+ this.preFocusValue = this.value;
6782
+ this.initializeFocusedIndex();
6783
+ }
6784
+ else {
6785
+ // Reset focused index to 0 when filter changes (only if already typing)
6786
+ this.focused = 0;
6787
+ }
6788
+ // Defer selectValue update to avoid ExpressionChangedAfterItHasBeenCheckedError
6789
+ // This happens because we're updating state during change detection
6790
+ setTimeout(() => {
6791
+ // Update selectValue to first available item for highlighting
6792
+ const availableItems = this.getAvailableItems();
6793
+ if (availableItems.length > 0) {
6794
+ const firstItem = availableItems[0];
6795
+ // Handle both options (objects) and menuItems (FwMenuItemComponent)
6796
+ if (firstItem instanceof FwMenuItemComponent) {
6797
+ this.selectValue = firstItem.value;
6798
+ }
6799
+ else {
6800
+ this.selectValue = this.useFullOptionAsValue()
6801
+ ? JSON.stringify(firstItem)
6802
+ : firstItem[this.valueProperty()]?.toString();
6803
+ }
6804
+ }
6805
+ else {
6806
+ // Clear selection when no items match
6807
+ this.selectValue = '';
6808
+ }
6809
+ }, 0);
6810
+ this.filterChanged.emit(this.filterValue());
6811
+ // Auto-open dropdown when user starts typing
6812
+ if (this.filterValue() && !this.trigger.isOpen()) {
6813
+ this.trigger.open();
6814
+ this._isOpen = true;
6815
+ }
6816
+ }
6817
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: FwSelectMenuComponent, deps: [{ token: i1$4.NgControl, optional: true, self: true }], target: i0.ɵɵFactoryTarget.Component }); }
6818
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.18", type: FwSelectMenuComponent, isStandalone: true, selector: "fw-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, valueProperty: { classPropertyName: "valueProperty", publicName: "valueProperty", isSignal: true, isRequired: false, transformFunction: null }, useFullOptionAsValue: { classPropertyName: "useFullOptionAsValue", publicName: "useFullOptionAsValue", isSignal: true, isRequired: false, transformFunction: null }, titleProperty: { classPropertyName: "titleProperty", publicName: "titleProperty", isSignal: true, isRequired: false, transformFunction: null }, iconProperty: { classPropertyName: "iconProperty", publicName: "iconProperty", isSignal: true, isRequired: false, transformFunction: null }, staticIcon: { classPropertyName: "staticIcon", publicName: "staticIcon", isSignal: true, isRequired: false, transformFunction: null }, descriptionProperty: { classPropertyName: "descriptionProperty", publicName: "descriptionProperty", isSignal: true, isRequired: false, transformFunction: null }, showFilter: { classPropertyName: "showFilter", publicName: "showFilter", isSignal: true, isRequired: false, transformFunction: null }, showReset: { classPropertyName: "showReset", publicName: "showReset", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, errored: { classPropertyName: "errored", publicName: "errored", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, optionsWidth: { classPropertyName: "optionsWidth", publicName: "optionsWidth", isSignal: true, isRequired: false, transformFunction: null }, minOptionsHeight: { classPropertyName: "minOptionsHeight", publicName: "minOptionsHeight", isSignal: true, isRequired: false, transformFunction: null }, maxOptionsHeight: { classPropertyName: "maxOptionsHeight", publicName: "maxOptionsHeight", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { change: "change", filterChanged: "filterChanged" }, host: { listeners: { "document:click": "outsideClick($event.target)" }, properties: { "class.disabled": "this.disabledClass" } }, queries: [{ propertyName: "menuItems", predicate: FwMenuItemComponent, descendants: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: CdkMenuTrigger, descendants: true }, { propertyName: "menu", first: true, predicate: FwMenuComponent, descendants: true }, { propertyName: "textInput", first: true, predicate: FwTextInputComponent, descendants: true }], ngImport: i0, template: "<div #wrapper [style.width]=\"width()\">\n <fw-text-input\n fwMenuRegister\n [cdkMenuTriggerFor]=\"selectMenu\"\n [value]=\"inputDisplayValue()\"\n [leftIcon]=\"staticIcon() || selectIcon || null\"\n [rightIcon]=\"(selectTitle()&&showReset())?'close-circled':'chevron-down'\"\n (rightIconAction)=\"handleReset()\"\n [useActionableIcons]=\"true\"\n [placeholder]=\"placeholder()\"\n [size]=\"size()\"\n [error]=\"errored() || (invalid && touched)\"\n (input)=\"onInputChange($event)\"\n (keyup)=\"handleKeyUp($event)\"\n (keydown)=\"handleKeyDown($event)\"\n (focus)=\"handleFocus()\"\n (click)=\"handleInputClick()\"\n [readOnly]=\"false\">\n </fw-text-input>\n <ng-template #selectMenu>\n @if (!disabled()) {\n <fw-menu-container\n [filterText]=\"filterValue()\"\n [showFilter]=\"showFilter()\" [width]=\"optionsWidth() || wrapper.offsetWidth + 'px'\"\n [maxHeight]=\"maxOptionsHeight()\" [minHeight]=\"minOptionsHeight()\" (filterChanged)=\"onFilterChanged($event)\">\n <fw-menu [disabled]=\"disabled()\" [value]=\"selectValue\" (change)=\"handleClick($any($event))\">\n @if (menuItems.length === 0) {\n @if (optionsWithValues().length > 0) {\n @for (item of optionsWithValues(); track item.trackingId) {\n <fw-menu-item\n [title]=\"item.raw[titleProperty()]?.toString()\"\n [description]=\"item.raw[descriptionProperty()]\"\n [value]=\"item.value\"\n [icon]=\"item.raw[iconProperty()]\"\n [disabled]=\"$any(item.raw).disabled\"\n >\n </fw-menu-item>\n }\n } @else {\n @if (isTyping() && filterValue()) {\n <fw-menu-item\n title=\"No matches found...\"\n [disabled]=\"true\"\n >\n </fw-menu-item>\n }\n }\n }\n <div #menuContentWrapper>\n <ng-content select=\"[fw-menu-item, fw-menu-separator, fw-menu-item-group]\"></ng-content>\n </div>\n </fw-menu>\n </fw-menu-container>\n }\n </ng-template>\n</div>\n", styles: [":host{box-sizing:border-box;max-width:100%}:host>div{cursor:pointer}:host.disabled{opacity:.4;cursor:not-allowed}:host.disabled>div{pointer-events:none}\n"], dependencies: [{ kind: "component", type: FwTextInputComponent, selector: "fw-text-input", inputs: ["disabled", "useActionableIcons", "leftIcon", "rightIcon", "prefix", "context", "helperText", "errorText", "errorInIconTooltip", "placeholder", "readOnly", "size", "type", "maxLength", "autofocus", "autocomplete", "value", "error", "width"], outputs: ["leftIconAction", "rightIconAction"] }, { kind: "directive", type: MenuRegisterDirective, selector: "[fwMenuRegister]" }, { kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: FwMenuContainerComponent, selector: "fw-menu-container, fw-menu-filter", inputs: ["width", "maxHeight", "minHeight", "border", "shadow", "showFilter", "filterText", "focusFilterOnMount", "offset", "emptyText", "filterFn", "additionalMenuItems", "keyHandler"], outputs: ["filteredMenuItemChange", "filterChanged"] }, { kind: "component", type: FwMenuComponent, selector: "fw-menu", inputs: ["disabled", "size", "multiSelect", "useCheckbox", "value"], outputs: ["change"] }, { kind: "component", type: FwMenuItemComponent, selector: "fw-menu-item", inputs: ["value", "size", "title", "description", "icon", "iconColor", "disabled", "showCheckbox", "checkboxColor", "multiSelect", "hidden", "collapsed", "href", "target", "subItemsOpen", "mouseEnterHandler", "focused", "selected"], outputs: ["mouseEnterHandlerChange", "click"] }] }); }
6508
6819
  }
6509
6820
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImport: i0, type: FwSelectMenuComponent, decorators: [{
6510
6821
  type: Component,
@@ -6518,8 +6829,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
6518
6829
  FwMenuComponent,
6519
6830
  NgFor,
6520
6831
  FwMenuItemComponent,
6521
- ], template: "<div #wrapper [ngStyle]=\"{width: width, cursor: 'pointer'}\">\n <fw-text-input\n fwMenuRegister\n [cdkMenuTriggerFor]=\"selectMenu\"\n [value]=\"selectTitle\"\n [leftIcon]=\"staticIcon || selectIcon || null\"\n [rightIcon]=\"(selectTitle&&showReset)?'close-circled':'chevron-down'\"\n (rightIconAction)=\"handleReset()\"\n [useActionableIcons]=\"true\"\n [placeholder]=\"placeholder\"\n [size]=\"size\"\n [error]=\"errored || (invalid && touched)\"\n (keyup)=\"handleKeyUp($event)\"\n (keydown)=\"handleKeyDown($event)\"\n [readOnly]=\"true\">\n </fw-text-input>\n <ng-template #selectMenu>\n <fw-menu-container\n [filterText]=\"filterValue\"\n *ngIf=\"!disabled\" [showFilter]=\"showFilter\" [width]=\"optionsWidth || wrapper.offsetWidth + 'px'\"\n [maxHeight]=\"maxOptionsHeight\" [minHeight]=\"minOptionsHeight\" (filterChanged)=\"onFilterChanged($event)\">\n <fw-menu [disabled]=\"disabled\" [value]=\"selectValue\" (change)=\"handleClick($any($event))\">\n <ng-container *ngIf=\"menuItems.length===0\">\n <fw-menu-item\n *ngFor=\"let item of options\"\n [title]=\"item[titleProperty]?.toString()\"\n [description]=\"item[descriptionProperty]\"\n [value]=\"useFullOptionAsValue ? JSON.stringify(item) : item?.[valueProperty]?.toString()\"\n [icon]=\"item[iconProperty]\"\n [disabled]=\"$any(item).disabled\"\n >\n </fw-menu-item>\n </ng-container>\n <div #menuContentWrapper>\n <ng-content select=\"[fw-menu-item, fw-menu-separator, fw-menu-item-group]\"></ng-content>\n </div>\n </fw-menu>\n </fw-menu-container>\n </ng-template>\n</div>\n", styles: [":host{box-sizing:border-box;max-width:100%}:host.disabled{opacity:.4;cursor:not-allowed}:host.disabled>div{pointer-events:none}\n"] }]
6522
- }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i1$4.NgControl, decorators: [{
6832
+ ], template: "<div #wrapper [style.width]=\"width()\">\n <fw-text-input\n fwMenuRegister\n [cdkMenuTriggerFor]=\"selectMenu\"\n [value]=\"inputDisplayValue()\"\n [leftIcon]=\"staticIcon() || selectIcon || null\"\n [rightIcon]=\"(selectTitle()&&showReset())?'close-circled':'chevron-down'\"\n (rightIconAction)=\"handleReset()\"\n [useActionableIcons]=\"true\"\n [placeholder]=\"placeholder()\"\n [size]=\"size()\"\n [error]=\"errored() || (invalid && touched)\"\n (input)=\"onInputChange($event)\"\n (keyup)=\"handleKeyUp($event)\"\n (keydown)=\"handleKeyDown($event)\"\n (focus)=\"handleFocus()\"\n (click)=\"handleInputClick()\"\n [readOnly]=\"false\">\n </fw-text-input>\n <ng-template #selectMenu>\n @if (!disabled()) {\n <fw-menu-container\n [filterText]=\"filterValue()\"\n [showFilter]=\"showFilter()\" [width]=\"optionsWidth() || wrapper.offsetWidth + 'px'\"\n [maxHeight]=\"maxOptionsHeight()\" [minHeight]=\"minOptionsHeight()\" (filterChanged)=\"onFilterChanged($event)\">\n <fw-menu [disabled]=\"disabled()\" [value]=\"selectValue\" (change)=\"handleClick($any($event))\">\n @if (menuItems.length === 0) {\n @if (optionsWithValues().length > 0) {\n @for (item of optionsWithValues(); track item.trackingId) {\n <fw-menu-item\n [title]=\"item.raw[titleProperty()]?.toString()\"\n [description]=\"item.raw[descriptionProperty()]\"\n [value]=\"item.value\"\n [icon]=\"item.raw[iconProperty()]\"\n [disabled]=\"$any(item.raw).disabled\"\n >\n </fw-menu-item>\n }\n } @else {\n @if (isTyping() && filterValue()) {\n <fw-menu-item\n title=\"No matches found...\"\n [disabled]=\"true\"\n >\n </fw-menu-item>\n }\n }\n }\n <div #menuContentWrapper>\n <ng-content select=\"[fw-menu-item, fw-menu-separator, fw-menu-item-group]\"></ng-content>\n </div>\n </fw-menu>\n </fw-menu-container>\n }\n </ng-template>\n</div>\n", styles: [":host{box-sizing:border-box;max-width:100%}:host>div{cursor:pointer}:host.disabled{opacity:.4;cursor:not-allowed}:host.disabled>div{pointer-events:none}\n"] }]
6833
+ }], ctorParameters: () => [{ type: i1$4.NgControl, decorators: [{
6523
6834
  type: Optional
6524
6835
  }, {
6525
6836
  type: Self
@@ -6529,46 +6840,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.18", ngImpo
6529
6840
  }], disabledClass: [{
6530
6841
  type: HostBinding,
6531
6842
  args: ['class.disabled']
6532
- }], options: [{
6533
- type: Input
6534
- }], valueProperty: [{
6535
- type: Input
6536
- }], useFullOptionAsValue: [{
6537
- type: Input
6538
- }], titleProperty: [{
6539
- type: Input
6540
- }], iconProperty: [{
6541
- type: Input
6542
- }], staticIcon: [{
6543
- type: Input
6544
- }], descriptionProperty: [{
6545
- type: Input
6546
- }], showFilter: [{
6547
- type: Input
6548
- }], showReset: [{
6549
- type: Input
6550
- }], disabled: [{
6551
- type: Input
6552
- }], errored: [{
6553
- type: Input
6554
- }], width: [{
6555
- type: Input
6556
- }], optionsWidth: [{
6557
- type: Input
6558
- }], minOptionsHeight: [{
6559
- type: Input
6560
- }], maxOptionsHeight: [{
6561
- type: Input
6562
- }], size: [{
6563
- type: Input
6564
- }], placeholder: [{
6565
- type: Input
6566
6843
  }], trigger: [{
6567
6844
  type: ViewChild,
6568
6845
  args: [CdkMenuTrigger]
6569
6846
  }], menu: [{
6570
6847
  type: ViewChild,
6571
6848
  args: [FwMenuComponent]
6849
+ }], textInput: [{
6850
+ type: ViewChild,
6851
+ args: [FwTextInputComponent]
6572
6852
  }], menuItems: [{
6573
6853
  type: ContentChildren,
6574
6854
  args: [FwMenuItemComponent, { descendants: true }]