From 5b90fe87adcdd37adba15c6a8190aa8f8de6b71d Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Tue, 30 Nov 2021 10:52:36 +0000 Subject: [PATCH] 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 --- ghost/admin/app/services/tenor.js | 7 + .../koenig-card-image/selector-tenor.hbs | 3 +- .../koenig-card-image/selector-tenor.js | 146 +++++++++++------- 3 files changed, 97 insertions(+), 59 deletions(-) diff --git a/ghost/admin/app/services/tenor.js b/ghost/admin/app/services/tenor.js index f41347aeaf..2f7223be75 100644 --- a/ghost/admin/app/services/tenor.js +++ b/ghost/admin/app/services/tenor.js @@ -200,6 +200,9 @@ export default class TenorService extends Service { // add to general gifs list this.gifs.push(gif); + // store index for use in templates and keyboard nav + gif.index = this.gifs.indexOf(gif); + // add to least populated column 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 this._columnHeights[columnIndex] += 300 * gif.ratio; 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() { diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.hbs b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.hbs index 02f2fb80fb..d35f216999 100644 --- a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.hbs +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.hbs @@ -33,7 +33,8 @@ @gif={{gif}} @select={{fn this.select gif}} @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}} diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.js index 013d0ad584..10e1ceae1b 100644 --- a/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.js +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-card-image/selector-tenor.js @@ -12,20 +12,7 @@ const THREE_COLUMN_WIDTH = 940; export default class KoenigCardImageTenorSelector extends Component { @service tenor; - @tracked highlightedColumnIndex; - @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]; - } + @tracked highlightedGif; willDestroy() { super.willDestroy(...arguments); @@ -91,74 +78,117 @@ export default class KoenigCardImageTenorSelector extends Component { @action clearHighlight() { - this.highlightedColumnIndex = undefined; - this.highlightedRowIndex = undefined; + this.highlightedGif = undefined; } @action highlightFirst() { - this.highlightedColumnIndex = 0; - this.highlightedRowIndex = 0; + this.highlightedGif = this.tenor.gifs[0]; } @action highlightNext() { - if (this.highlightedColumnIndex === this.tenor.columns.length - 1) { - // at the end of a row, drop down to the next one - const newColumn = 0; - 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; + if (this.highlightedGif === this.tenor.gifs[this.tenor.gifs.length - 1]) { + // reached the end, do nothing + return; } + + this.highlightedGif = this.tenor.gifs[this.highlightedGif.index + 1]; } @action highlightPrev() { - if (this.highlightedColumnIndex === 0) { - // at the start of a row, jump up to the prev one - const newColumn = this.tenor.columns.length - 1; - 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; + if (this.highlightedGif.index === 0) { + // reached the beginning, focus the search bar + return this.focusSearch(); } + + this.highlightedGif = this.tenor.gifs[this.highlightedGif.index - 1]; } @action moveHighlightDown() { - if (this.highlightedRowIndex === this.highlightedColumn.length - 1) { - // aready at bottom, do nothing - return; - } + const nextGif = this.tenor.columns[this.highlightedGif.columnIndex][this.highlightedGif.columnRowIndex + 1]; - this.highlightedRowIndex += 1; + if (nextGif) { + this.highlightedGif = nextGif; + } } @action moveHighlightUp() { - if (this.highlightedRowIndex === 0) { - // already at top, focus to the search bar + const nextGif = this.tenor.columns[this.highlightedGif.columnIndex][this.highlightedGif.columnRowIndex - 1]; + + 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(); } - 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') @@ -186,7 +216,7 @@ export default class KoenigCardImageTenorSelector extends Component { handleLeft(event) { if (this.highlightedGif) { event.preventDefault(); - this.highlightPrev(); + this.moveHighlightLeft(); } } @@ -194,7 +224,7 @@ export default class KoenigCardImageTenorSelector extends Component { handleRight(event) { if (this.highlightedGif) { event.preventDefault(); - this.highlightNext(); + this.moveHighlightRight(); } }