Fixed click issue with tag suggestions in safari

Updated selectable elements to be divs instead of buttons since Safari
akwardly does not focus on buttons on click.
Also standardised keyboard handling to our standard nav class.
Also addressed empty tag values showing in results.
For #4139
This commit is contained in:
Dan Brown 2023-04-07 17:47:46 +01:00
parent fd674d10e3
commit e722ee4268
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
3 changed files with 30 additions and 55 deletions

View File

@ -11,11 +11,9 @@ use Illuminate\Support\Facades\DB;
class TagRepo class TagRepo
{ {
protected PermissionApplicator $permissions; public function __construct(
protected PermissionApplicator $permissions
public function __construct(PermissionApplicator $permissions) ) {
{
$this->permissions = $permissions;
} }
/** /**
@ -90,6 +88,7 @@ class TagRepo
{ {
$query = Tag::query() $query = Tag::query()
->select('*', DB::raw('count(*) as count')) ->select('*', DB::raw('count(*) as count'))
->where('value', '!=', '')
->groupBy('value'); ->groupBy('value');
if ($searchTerm) { if ($searchTerm) {

View File

@ -8,11 +8,9 @@ use Illuminate\Http\Request;
class TagController extends Controller class TagController extends Controller
{ {
protected TagRepo $tagRepo; public function __construct(
protected TagRepo $tagRepo
public function __construct(TagRepo $tagRepo) ) {
{
$this->tagRepo = $tagRepo;
} }
/** /**

View File

@ -1,6 +1,7 @@
import {escapeHtml} from "../services/util"; import {escapeHtml} from "../services/util";
import {onChildEvent} from "../services/dom"; import {onChildEvent} from "../services/dom";
import {Component} from "./component"; import {Component} from "./component";
import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
const ajaxCache = {}; const ajaxCache = {};
@ -21,26 +22,31 @@ export class AutoSuggest extends Component {
} }
setupListeners() { setupListeners() {
const navHandler = new KeyboardNavigationHandler(
this.list,
event => {
this.input.focus();
setTimeout(() => this.hideSuggestions(), 1);
},
event => {
event.preventDefault();
this.selectSuggestion(event.target.textContent);
},
);
navHandler.shareHandlingToEl(this.input);
onChildEvent(this.list, '.text-item', 'click', (event, el) => {
this.selectSuggestion(el.textContent);
});
this.input.addEventListener('input', this.requestSuggestions.bind(this)); this.input.addEventListener('input', this.requestSuggestions.bind(this));
this.input.addEventListener('focus', this.requestSuggestions.bind(this)); this.input.addEventListener('focus', this.requestSuggestions.bind(this));
this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
this.input.addEventListener('keydown', event => { this.input.addEventListener('keydown', event => {
if (event.key === 'Tab') { if (event.key === 'Tab') {
this.hideSuggestions(); this.hideSuggestions();
} }
}); });
this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
this.container.addEventListener('keydown', this.containerKeyDown.bind(this));
onChildEvent(this.list, 'button', 'click', (event, el) => {
this.selectSuggestion(el.textContent);
});
onChildEvent(this.list, 'button', 'keydown', (event, el) => {
if (event.key === 'Enter') {
this.selectSuggestion(el.textContent);
}
});
} }
selectSuggestion(value) { selectSuggestion(value) {
@ -52,36 +58,6 @@ export class AutoSuggest extends Component {
this.hideSuggestions(); this.hideSuggestions();
} }
containerKeyDown(event) {
if (event.key === 'Enter') event.preventDefault();
if (this.list.classList.contains('hidden')) return;
// Down arrow
if (event.key === 'ArrowDown') {
this.moveFocus(true);
event.preventDefault();
}
// Up Arrow
else if (event.key === 'ArrowUp') {
this.moveFocus(false);
event.preventDefault();
}
// Escape key
else if (event.key === 'Escape') {
this.hideSuggestions();
event.preventDefault();
}
}
moveFocus(forward = true) {
const focusables = Array.from(this.container.querySelectorAll('input,button'));
const index = focusables.indexOf(document.activeElement);
const newFocus = focusables[index + (forward ? 1 : -1)];
if (newFocus) {
newFocus.focus()
}
}
async requestSuggestions() { async requestSuggestions() {
if (Date.now() - this.lastPopulated < 50) { if (Date.now() - this.lastPopulated < 50) {
return; return;
@ -132,9 +108,11 @@ export class AutoSuggest extends Component {
return this.hideSuggestions(); return this.hideSuggestions();
} }
this.list.innerHTML = suggestions.map(value => `<li><button type="button" class="text-item">${escapeHtml(value)}</button></li>`).join(''); // This used to use <button>s but was changed to div elements since Safari would not focus on buttons
// on which causes a range of other complexities related to focus handling.
this.list.innerHTML = suggestions.map(value => `<li><div tabindex="-1" class="text-item">${escapeHtml(value)}</div></li>`).join('');
this.list.style.display = 'block'; this.list.style.display = 'block';
for (const button of this.list.querySelectorAll('button')) { for (const button of this.list.querySelectorAll('.text-item')) {
button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this)); button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
} }
} }