Skip to content

Commit fea941a

Browse files
authored
fix(ui5-combobox): Improve arrow navigation functionality (#3928)
* Enable arrow up on the first item to move the focus to the input * Include valuestate header in the arrow chain * On ENTER move the focus to the input, bug fixes * Fix the big with the undesired closing of the popover when navigating fast through the items + refactoring of the arrow navigation logic * Restore hash.txt * Restore hash file * Add tests * Add more test, linting * Do not focus selected suggestion after opening * Restore visible focus on the input when typing and suggestions are opened * Merge with master, converts the new tests to be async * Fix arrow nav when the suggestions popover is closed * Fix lint errors * Refactoring * Add more tests * Retrigger build * Fix item announcement * Fix focus on closing with alt+arr * Fix double focus on value state header when ESC is pressed and on user input
1 parent 55eb7ba commit fea941a

File tree

5 files changed

+285
-29
lines changed

5 files changed

+285
-29
lines changed

packages/main/src/ComboBox.js

Lines changed: 113 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ const metadata = {
189189
type: Boolean,
190190
},
191191

192+
/**
193+
* Indicates whether the visual focus is on the value state header
194+
* @private
195+
*/
196+
_isValueStateFocused: {
197+
type: Boolean,
198+
},
199+
192200
/**
193201
* Sets the accessible aria name of the component.
194202
*
@@ -390,13 +398,6 @@ class ComboBox extends UI5Element {
390398
}
391399

392400
this._selectMatchingItem();
393-
394-
if (this._isKeyNavigation && this.responsivePopover && this.responsivePopover.opened) {
395-
this.focused = false;
396-
} else if (this.shadowRoot.activeElement) {
397-
this.focused = this.shadowRoot.activeElement.id === "ui5-combobox-input";
398-
}
399-
400401
this._initialRendering = false;
401402
this._isKeyNavigation = false;
402403
}
@@ -410,15 +411,16 @@ class ComboBox extends UI5Element {
410411

411412
if (this.shouldClosePopover() && !isPhone()) {
412413
this.responsivePopover.close(false, false, true);
414+
this._clearFocus();
415+
this._itemFocused = false;
413416
}
414417

415-
this._itemFocused = false;
416418
this.toggleValueStatePopover(this.shouldOpenValueStateMessagePopover);
417419
this.storeResponsivePopoverWidth();
418420
}
419421

420422
shouldClosePopover() {
421-
return this.responsivePopover.opened && !this.focused && !this._itemFocused;
423+
return this.responsivePopover.opened && !this.focused && !this._itemFocused && !this._isValueStateFocused;
422424
}
423425

424426
_focusin(event) {
@@ -519,6 +521,8 @@ class ComboBox extends UI5Element {
519521
if (event.target === this.inner) {
520522
// stop the native event, as the semantic "input" would be fired.
521523
event.stopImmediatePropagation();
524+
this.focused = true;
525+
this._isValueStateFocused = false;
522526
}
523527

524528
this._filteredItems = this._filterItems(value);
@@ -538,8 +542,6 @@ class ComboBox extends UI5Element {
538542
});
539543

540544
this._selectionChanged = false;
541-
542-
item.focused = true;
543545
}
544546
}
545547

@@ -573,40 +575,62 @@ class ComboBox extends UI5Element {
573575
return;
574576
}
575577

578+
const isOpen = this.open;
576579
const isArrowDown = isDown(event);
577580
const isArrowUp = isUp(event);
578581
const currentItem = this._filteredItems.find(item => {
579-
return this.responsivePopover.opened ? item.focused : item.selected;
582+
return isOpen ? item.focused : item.selected;
580583
});
581-
let indexOfItem = this._filteredItems.indexOf(currentItem);
584+
const indexOfItem = this._filteredItems.indexOf(currentItem);
582585

583586
event.preventDefault();
584587

585-
if ((indexOfItem === 0 && isArrowUp) || (this._filteredItems.length - 1 === indexOfItem && isArrowDown)) {
588+
if ((this.focused === true && isArrowUp && isOpen) || (this._filteredItems.length - 1 === indexOfItem && isArrowDown)) {
586589
return;
587590
}
588591

589-
this._clearFocus();
592+
this._isKeyNavigation = true;
590593

591-
indexOfItem += isArrowDown ? 1 : -1;
592-
indexOfItem = indexOfItem < 0 ? 0 : indexOfItem;
593-
this._filteredItems[indexOfItem].focused = true;
594+
if (isArrowDown) {
595+
this._handleArrowDown(event, indexOfItem);
596+
}
594597

595-
if (this.responsivePopover.opened) {
596-
this.announceSelectedItem(indexOfItem);
598+
if (isArrowUp) {
599+
this._handleArrowUp(event, indexOfItem);
597600
}
601+
}
598602

599-
this.value = this._filteredItems[indexOfItem].isGroupItem ? this.filterValue : this._filteredItems[indexOfItem].text;
603+
_handleItemNavigation(event, indexOfItem, isForward) {
604+
const isOpen = this.open;
605+
const currentItem = this._filteredItems[indexOfItem];
606+
const nextItem = isForward ? this._filteredItems[indexOfItem + 1] : this._filteredItems[indexOfItem - 1];
607+
const isGroupItem = currentItem && currentItem.isGroupItem;
600608

601-
this._isKeyNavigation = true;
602-
this._itemFocused = true;
609+
if ((!isOpen) && ((isGroupItem && !nextItem) || (!isGroupItem && !currentItem))) {
610+
return;
611+
}
612+
613+
this._clearFocus();
614+
615+
if (isOpen) {
616+
this._itemFocused = true;
617+
this.value = isGroupItem ? this.filterValue : currentItem.text;
618+
this.focused = false;
619+
currentItem.focused = true;
620+
} else {
621+
this.focused = true;
622+
this.value = isGroupItem ? nextItem.text : currentItem.text;
623+
currentItem.focused = false;
624+
}
625+
626+
this._isValueStateFocused = false;
603627
this._selectionChanged = true;
604628

605-
if (this._filteredItems[indexOfItem].isGroupItem) {
629+
if (isGroupItem && isOpen) {
606630
return;
607631
}
608632

609-
this._filteredItems[indexOfItem].selected = true;
633+
this._announceSelectedItem(indexOfItem);
610634

611635
// autocomplete
612636
const item = this._autoCompleteValue(this.value);
@@ -621,6 +645,48 @@ class ComboBox extends UI5Element {
621645
this._fireChangeEvent();
622646
}
623647

648+
_handleArrowDown(event, indexOfItem) {
649+
const isOpen = this.open;
650+
651+
if (this.focused && indexOfItem === -1 && this.hasValueStateText && isOpen) {
652+
this._isValueStateFocused = true;
653+
this.focused = false;
654+
return;
655+
}
656+
657+
indexOfItem = !isOpen && this.hasValueState && indexOfItem === -1 ? 0 : indexOfItem;
658+
659+
this._handleItemNavigation(event, ++indexOfItem, true /* isForward */);
660+
}
661+
662+
_handleArrowUp(event, indexOfItem) {
663+
const isOpen = this.open;
664+
665+
if (indexOfItem === 0 && !this.hasValueStateText) {
666+
this._clearFocus();
667+
this.focused = true;
668+
this._itemFocused = false;
669+
return;
670+
}
671+
672+
if (indexOfItem === 0 && this.hasValueStateText && isOpen) {
673+
this._clearFocus();
674+
this._itemFocused = false;
675+
this._isValueStateFocused = true;
676+
this._filteredItems[0].selected = false;
677+
return;
678+
}
679+
680+
if (this._isValueStateFocused) {
681+
this.focused = true;
682+
this._isValueStateFocused = false;
683+
return;
684+
}
685+
686+
indexOfItem = !isOpen && this.hasValueState && indexOfItem === -1 ? 0 : indexOfItem;
687+
this._handleItemNavigation(event, --indexOfItem, false /* isForward */);
688+
}
689+
624690
_keydown(event) {
625691
const isArrowKey = isDown(event) || isUp(event);
626692
this._autocomplete = !(isBackSpace(event) || isDelete(event));
@@ -632,16 +698,32 @@ class ComboBox extends UI5Element {
632698
if (isEnter(event)) {
633699
this._fireChangeEvent();
634700
this._closeRespPopover();
701+
this.focused = true;
635702
}
636703

637-
if (isEscape(event) && !this.open) {
638-
this.value = this._lastValue;
704+
if (isEscape(event)) {
705+
this.focused = true;
706+
this.value = !this.open ? this._lastValue : this.value;
707+
this._isValueStateFocused = false;
639708
}
640709

641710
if (isShow(event) && !this.readonly && !this.disabled) {
642711
event.preventDefault();
712+
643713
this._resetFilter();
644714
this._toggleRespPopover();
715+
716+
const selectedItem = this._filteredItems.find(item => {
717+
return item.selected;
718+
});
719+
720+
if (selectedItem && this.open) {
721+
this._itemFocused = true;
722+
selectedItem.focused = true;
723+
this.focused = false;
724+
} else {
725+
this.focused = true;
726+
}
645727
}
646728
}
647729

@@ -657,6 +739,9 @@ class ComboBox extends UI5Element {
657739
this.filterValue = this._selectedItemText;
658740
}
659741

742+
this._isValueStateFocused = false;
743+
this._clearFocus();
744+
660745
this.responsivePopover.close();
661746
}
662747

@@ -786,7 +871,7 @@ class ComboBox extends UI5Element {
786871
this._itemFocused = true;
787872
}
788873

789-
announceSelectedItem(indexOfItem) {
874+
_announceSelectedItem(indexOfItem) {
790875
const itemPositionText = this.i18nBundle.getText(LIST_ITEM_POSITION, [indexOfItem + 1], [this._filteredItems.length]);
791876
const itemSelectionText = this.i18nBundle.getText(LIST_ITEM_SELECTED);
792877

packages/main/src/ComboBoxPopover.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555
{{#unless _isPhone}}
5656
{{#if hasValueStateText}}
57-
<div class="ui5-responsive-popover-header {{classes.popoverValueState}}" style="{{styles.suggestionPopoverHeader}}">
57+
<div class="ui5-responsive-popover-header {{classes.popoverValueState}}" ?focused={{_isValueStateFocused}} tabindex="0" style="{{styles.suggestionPopoverHeader}}">
5858
{{> valueStateMessage}}
5959
</div>
6060
{{/if}}

packages/main/src/themes/ValueStateMessage.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,10 @@ ui5-responsive-popover .ui5-valuestatemessage-header {
3636
.ui5-valuestatemessage--information {
3737
background: var(--sapInformationBackground);
3838
}
39+
40+
.ui5-responsive-popover-header[focused] {
41+
outline-width: 0.0625rem;
42+
outline-style: dotted;
43+
outline-offset: -0.125rem;
44+
outline-color: initial;
45+
}

packages/main/test/pages/ComboBox.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,25 @@
9999
<ui5-combobox id="" style="width: 360px;" value-state="Success"></ui5-combobox>
100100
</div>
101101

102+
<div class="demo-section">
103+
<ui5-label id="combo-vs-grouping-label">Items with grouping and value state:</ui5-label>
104+
105+
<ui5-combobox id="value-state-grouping" value-state="Warning">
106+
<ui5-cb-group-item text="A"></ui5-cb-group-item>
107+
<ui5-cb-item text="Argentina"></ui5-cb-item>
108+
<ui5-cb-item text="Australia"></ui5-cb-item>
109+
<ui5-cb-item text="Austria"></ui5-cb-item>
110+
<ui5-cb-group-item text="B"></ui5-cb-group-item>
111+
<ui5-cb-item text="Bahrain"></ui5-cb-item>
112+
<ui5-cb-item text="Belgium"></ui5-cb-item>
113+
<ui5-cb-item text="Brazil"></ui5-cb-item>
114+
<ui5-cb-group-item text="C"></ui5-cb-group-item>
115+
<ui5-cb-item text="Canada"></ui5-cb-item>
116+
<ui5-cb-item text="Chile"></ui5-cb-item>
117+
</ui5-combobox>
118+
</div>
119+
120+
102121
<div class="demo-section">
103122
<ui5-label id="combo-grouping-label">Items with grouping:</ui5-label>
104123
<ui5-combobox filter="StartsWith" id="combo-grouping" style="width: 360px;" aria-label="Select destination:">

0 commit comments

Comments
 (0)