Adjusted keyboard nav in gif selector to reduce vertical scroll jumps

refs https://github.com/TryGhost/Team/issues/1225

- `TAB` / `SHIFT+TAB` now cycle through gifs in the search return order. It means the highlight gif will not always be in the next column over but it drastically reduces the vertical scroll jumping
- `LEFT` / `RIGHT` now select the gif to the left/right that visually lines up with the top third of the currently highlighted gif and will stop at the grid edges. The result is `UP` / `DOWN` / `LEFT` / `RIGHT` act more like spatial navigation with no unexpected scroll jumps
- switching to only storing the highlighted gif and relying on indexes added to each gif by the `tenor` service when assigning to columns means that column number changes when resizing the viewport are automatically handled
This commit is contained in:
Kevin Ansfield 2021-11-30 10:52:36 +00:00
parent a45e345a95
commit 5b90fe87ad
3 changed files with 97 additions and 59 deletions

View File

@ -200,6 +200,9 @@ export default class TenorService extends Service {
// add to general gifs list // add to general gifs list
this.gifs.push(gif); this.gifs.push(gif);
// store index for use in templates and keyboard nav
gif.index = this.gifs.indexOf(gif);
// add to least populated column // add to least populated column
this._addGifToColumns(gif); this._addGifToColumns(gif);
} }
@ -211,6 +214,10 @@ export default class TenorService extends Service {
// use a fixed width when calculating height to compensate for different overall sizes // use a fixed width when calculating height to compensate for different overall sizes
this._columnHeights[columnIndex] += 300 * gif.ratio; this._columnHeights[columnIndex] += 300 * gif.ratio;
this.columns[columnIndex].push(gif); this.columns[columnIndex].push(gif);
// store the column indexes on the gif for use in keyboard nav
gif.columnIndex = columnIndex;
gif.columnRowIndex = this.columns[columnIndex].length - 1;
} }
_resetColumns() { _resetColumns() {

View File

@ -33,7 +33,8 @@
@gif={{gif}} @gif={{gif}}
@select={{fn this.select gif}} @select={{fn this.select gif}}
@isHighlighted={{eq gif this.highlightedGif}} @isHighlighted={{eq gif this.highlightedGif}}
{{scroll-into-view (eq gif this.highlightedGif) offset=20}} /> {{scroll-into-view (eq gif this.highlightedGif) offset=20}}
data-tenor-index={{gif.index}}
{{/each}} {{/each}}
</div> </div>
{{/each}} {{/each}}

View File

@ -12,20 +12,7 @@ const THREE_COLUMN_WIDTH = 940;
export default class KoenigCardImageTenorSelector extends Component { export default class KoenigCardImageTenorSelector extends Component {
@service tenor; @service tenor;
@tracked highlightedColumnIndex; @tracked highlightedGif;
@tracked highlightedRowIndex;
get highlightedGif() {
if (this.highlightedColumnIndex === undefined || this.highlightedRowIndex === undefined) {
return null;
}
return this.tenor.columns[this.highlightedColumnIndex][this.highlightedRowIndex];
}
get highlightedColumn() {
return this.tenor.columns[this.highlightedColumnIndex];
}
willDestroy() { willDestroy() {
super.willDestroy(...arguments); super.willDestroy(...arguments);
@ -91,74 +78,117 @@ export default class KoenigCardImageTenorSelector extends Component {
@action @action
clearHighlight() { clearHighlight() {
this.highlightedColumnIndex = undefined; this.highlightedGif = undefined;
this.highlightedRowIndex = undefined;
} }
@action @action
highlightFirst() { highlightFirst() {
this.highlightedColumnIndex = 0; this.highlightedGif = this.tenor.gifs[0];
this.highlightedRowIndex = 0;
} }
@action @action
highlightNext() { highlightNext() {
if (this.highlightedColumnIndex === this.tenor.columns.length - 1) { if (this.highlightedGif === this.tenor.gifs[this.tenor.gifs.length - 1]) {
// at the end of a row, drop down to the next one // reached the end, do nothing
const newColumn = 0; return;
const newRow = this.highlightedRowIndex + 1;
if (newRow >= this.tenor.columns[newColumn].length) {
// reached the end, do nothing
return;
}
this.highlightedColumnIndex = newColumn;
this.highlightedRowIndex = newRow;
} else {
// mid-row, move to next column
this.highlightedColumnIndex += 1;
} }
this.highlightedGif = this.tenor.gifs[this.highlightedGif.index + 1];
} }
@action @action
highlightPrev() { highlightPrev() {
if (this.highlightedColumnIndex === 0) { if (this.highlightedGif.index === 0) {
// at the start of a row, jump up to the prev one // reached the beginning, focus the search bar
const newColumn = this.tenor.columns.length - 1; return this.focusSearch();
const newRow = this.highlightedRowIndex - 1;
if (newRow < 0) {
// reached the beginning, focus the search bar
return this.focusSearch();
}
this.highlightedColumnIndex = newColumn;
this.highlightedRowIndex = newRow;
} else {
// mid-row, move to prev column
this.highlightedColumnIndex -= 1;
} }
this.highlightedGif = this.tenor.gifs[this.highlightedGif.index - 1];
} }
@action @action
moveHighlightDown() { moveHighlightDown() {
if (this.highlightedRowIndex === this.highlightedColumn.length - 1) { const nextGif = this.tenor.columns[this.highlightedGif.columnIndex][this.highlightedGif.columnRowIndex + 1];
// aready at bottom, do nothing
return;
}
this.highlightedRowIndex += 1; if (nextGif) {
this.highlightedGif = nextGif;
}
} }
@action @action
moveHighlightUp() { moveHighlightUp() {
if (this.highlightedRowIndex === 0) { const nextGif = this.tenor.columns[this.highlightedGif.columnIndex][this.highlightedGif.columnRowIndex - 1];
// already at top, focus to the search bar
if (nextGif) {
this.highlightedGif = nextGif;
} else {
// already at top, focus the search bar
return this.focusSearch();
}
}
@action
moveHighlightRight() {
if (this.highlightedGif.columnIndex === this.tenor.columns.length - 1) {
// we don't wrap and we're on the last column, do nothing
return;
}
this._moveToNextHorizontalGif('right');
}
@action
moveHighlightLeft() {
if (this.highlightedGif.index === 0) {
// on the first Gif, focus the search bar
return this.focusSearch(); return this.focusSearch();
} }
this.highlightedRowIndex -= 1; if (this.highlightedGif.columnIndex === 0) {
// we don't wrap and we're on the first column, do nothing
return;
}
this._moveToNextHorizontalGif('left');
}
_moveToNextHorizontalGif(direction) {
const highlightedElem = document.querySelector(`[data-tenor-index="${this.highlightedGif.index}"]`);
const highlightedElemRect = highlightedElem.getBoundingClientRect();
let x;
if (direction === 'left') {
x = highlightedElemRect.left - (highlightedElemRect.width / 2);
} else {
x = highlightedElemRect.right + (highlightedElemRect.width / 2);
}
let y = highlightedElemRect.top + (highlightedElemRect.height / 3);
let foundGifElem;
let jumps = 0;
// we might hit spacing between gifs, keep moving up 5 px until we get a match
while (!foundGifElem) {
let possibleMatch = document.elementFromPoint(x, y)?.closest('[data-tenor-index]');
if (possibleMatch?.dataset.tenorIndex !== undefined) {
foundGifElem = possibleMatch;
break;
}
jumps += 1;
y -= 5;
if (jumps > 10) {
// give up to avoid infinite loop
break;
}
}
if (foundGifElem) {
this.highlightedGif = this.tenor.gifs[foundGifElem.dataset.tenorIndex];
}
} }
@onKey('Tab') @onKey('Tab')
@ -186,7 +216,7 @@ export default class KoenigCardImageTenorSelector extends Component {
handleLeft(event) { handleLeft(event) {
if (this.highlightedGif) { if (this.highlightedGif) {
event.preventDefault(); event.preventDefault();
this.highlightPrev(); this.moveHighlightLeft();
} }
} }
@ -194,7 +224,7 @@ export default class KoenigCardImageTenorSelector extends Component {
handleRight(event) { handleRight(event) {
if (this.highlightedGif) { if (this.highlightedGif) {
event.preventDefault(); event.preventDefault();
this.highlightNext(); this.moveHighlightRight();
} }
} }