Skip to content

Commit

Permalink
fix(SelectionList): focused options are now scrolled into view
Browse files Browse the repository at this point in the history
ISSUES CLOSED: #1727
  • Loading branch information
benjamincharity committed Oct 3, 2019
1 parent c3d6868 commit 01e8b21
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<ng-template>
<div
class="ts-selection-list-panel__inner"
[class.ts-selection-list-visible]="showPanel"
[class.ts-selection-list--visible]="showPanel"
role="listbox"
[attr.id]="id"
#panel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,10 @@ $ts-selection-list-panel-border-radius: 4px !default;
}
}

&.ts-selection-list-visible {
&.ts-selection-list--visible {
visibility: visible;
}

&.ts-selection-list-hidden {
visibility: hidden;
}

.ts-selection-list-panel-above & {
border-radius: $ts-selection-list-panel-border-radius $ts-selection-list-panel-border-radius 0 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,29 @@ export class TsSelectionListPanelComponent implements AfterContentInit {
*/
public readonly uid = `ts-selection-list-panel-${nextUniqueId++}`;

/**
* Return the panel's scrollTop
*
* @return The scrolltop number
*/
public get scrollTop(): number {
return this.panel ? this.panel.nativeElement.scrollTop : 0;
}

/**
* Set the panel scrollTop
*
* This allows us to manually scroll to display options above or below the fold, as they are not actually being focused when active.
*
* @param scrollTop - The number of pixels to move
*/
public set scrollTop(scrollTop: number) {
// istanbul ignore else
if (this.panel) {
this.panel.nativeElement.scrollTop = scrollTop;
}
}

/**
* Access the template. Used by {@link TsSelectionListTriggerDirective}
*/
Expand All @@ -90,7 +113,7 @@ export class TsSelectionListPanelComponent implements AfterContentInit {
/**
* Access the element for the panel containing the options
*/
@ViewChild('panel', { static: true })
@ViewChild('panel', { static: false })
public panel!: ElementRef;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ import { ControlValueAccessor } from '@angular/forms';
import { TsDocumentService } from '@terminus/ngx-tools/browser';
import { coerceBooleanProperty } from '@terminus/ngx-tools/coercion';
import { KEYS } from '@terminus/ngx-tools/keycodes';
import {
isUnset,
untilComponentDestroyed,
} from '@terminus/ngx-tools/utilities';
import { untilComponentDestroyed } from '@terminus/ngx-tools/utilities';
import { TsFormFieldComponent } from '@terminus/ui/form-field';
import {
countGroupLabelsBeforeOption,
getOptionScrollPosition,
TsOptionComponent,
TsOptionSelectionChange,
} from '@terminus/ui/option';
Expand Down Expand Up @@ -73,6 +72,9 @@ export const TS_SELECTION_LIST_SCROLL_STRATEGY_FACTORY_PROVIDER = {
useFactory: TS_SELECTION_LIST_SCROLL_STRATEGY_FACTORY,
};

// The max height of the select's overlay panel
export const SELECTION_LIST_PANEL_MAX_HEIGHT = 256;

// Unique ID for each instance
let nextUniqueId = 0;

Expand Down Expand Up @@ -211,6 +213,18 @@ export class TsSelectionListTriggerDirective<ValueType = string> implements Cont
return null;
}

/**
* Calculates the height of the options
*
* Only called if at least one option exists
*/
private get itemHeight(): number {
// Try to use the 2nd option in case the first option is blank or a filter etc. Fall back to the first item if needed.
const options = this.selectionListPanel.options.toArray();
const option = options[1] || options[0];
return option.elementRef.nativeElement.offsetHeight;
}

/**
* A stream of actions that should close the panel, including when an option is selected, on blur, and when TAB is pressed.
*/
Expand Down Expand Up @@ -439,6 +453,10 @@ export class TsSelectionListTriggerDirective<ValueType = string> implements Cont
} else if (isArrowKey && this.canOpen()) {
this.openPanel();
}

if (isArrowKey || this.selectionListPanel.keyManager.activeItem !== prevActiveItem) {
this.scrollToOption();
}
}
}

Expand Down Expand Up @@ -782,4 +800,35 @@ export class TsSelectionListTriggerDirective<ValueType = string> implements Cont
this.canOpenOnNextFocus = this.document.activeElement !== this.elementRef.nativeElement || this.panelOpen;
}


/**
* Scroll to an option
*
* Given that we are not actually focusing active options, we must manually adjust scroll to reveal options below the fold. First, we find
* the offset of the option from the top of the panel. If that offset is below the fold, the new scrollTop will be the offset - the panel
* height + the option height, so the active option will be just visible at the bottom of the panel. If that offset is above the top of
* the visible panel, the new scrollTop will become the offset. If that offset is visible within the panel already, the scrollTop is not
* adjusted.
*/
private scrollToOption(): void {
const index: number = this.selectionListPanel.keyManager.activeItemIndex || 0;
const labelCount: number = countGroupLabelsBeforeOption(index, this.selectionListPanel.options, this.selectionListPanel.optionGroups);

if (index === 0 && labelCount === 1) {
// If we've got one group label before the option and we're at the top option,
// scroll the list to the top. This is better UX than scrolling the list to the
// top of the option, because it allows the user to read the top group's label.
this.selectionListPanel.scrollTop = 0;
} else {
const newScrollPosition = getOptionScrollPosition(
index + labelCount,
this.itemHeight,
this.selectionListPanel.scrollTop,
SELECTION_LIST_PANEL_MAX_HEIGHT,
);

this.selectionListPanel.scrollTop = newScrollPosition;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -965,4 +965,11 @@ describe(`TsSelectionListComponent`, function() {
expect(fixture.componentInstance.clicked).toHaveBeenCalled();
}));


describe(`scroll into view`, () => {

test.todo(`should update panel scroll position when focusing option out of view`);

});

});

0 comments on commit 01e8b21

Please sign in to comment.