[#4607] fixed the keyboard-accebility of the Admin UI dropdowns
This commit is contained in:
@@ -230,17 +230,27 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
{#if !isNew}
|
||||
<button type="button" aria-label="More" class="btn btn-sm btn-circle btn-transparent">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="More admin options"
|
||||
class="btn btn-sm btn-circle btn-transparent"
|
||||
>
|
||||
<!-- empty span for alignment -->
|
||||
<span />
|
||||
<i class="ri-more-line" />
|
||||
<span aria-hidden="true" />
|
||||
<i class="ri-more-line" aria-hidden="true" />
|
||||
<Toggler class="dropdown dropdown-upside dropdown-left dropdown-nowrap">
|
||||
<button type="button" class="dropdown-item txt-danger" on:click={() => deleteConfirm()}>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger"
|
||||
role="menuitem"
|
||||
on:click={() => deleteConfirm()}
|
||||
>
|
||||
<i class="ri-delete-bin-7-line" aria-hidden="true" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-fill" />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<i
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
class={copyTimeout ? successClasses : idleClasses}
|
||||
aria-label={"Copy to clipboard"}
|
||||
use:tooltipAction={!copyTimeout ? tooltip : undefined}
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
aria-label="Generate"
|
||||
use:tooltip={togglerActive ? "" : "Generate"}
|
||||
class="btn btn-circle {classes}"
|
||||
use:tooltip={togglerActive ? "" : "Generate"}
|
||||
>
|
||||
<i class="ri-sparkling-line" />
|
||||
<i class="ri-sparkling-line" aria-hidden="true" />
|
||||
<Toggler
|
||||
class="dropdown dropdown-upside dropdown-center dropdown-nowrap"
|
||||
bind:active={togglerActive}
|
||||
@@ -49,6 +49,7 @@
|
||||
<span bind:this={secretElem} class="secret">{secret}</span>
|
||||
<CopyIcon value={secret} />
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<i
|
||||
class="ri-refresh-line txt-sm link-hint"
|
||||
use:tooltip={"Refresh"}
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-item option"
|
||||
role="menuitem"
|
||||
class:closable
|
||||
class:selected={isSelected(item)}
|
||||
on:click={(e) => handleOptionSelect(e, item)}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
let containerChild;
|
||||
let activeTrigger;
|
||||
let scrollTimeoutId;
|
||||
let hideTimeoutId;
|
||||
let isOutsideMouseDown = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -30,16 +31,41 @@
|
||||
dispatch("hide");
|
||||
}
|
||||
|
||||
export function hideWithDelay(delay = 0) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(hideTimeoutId);
|
||||
hideTimeoutId = setTimeout(hide, delay);
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
if (!active) {
|
||||
return; // already hidden
|
||||
}
|
||||
|
||||
active = false;
|
||||
isOutsideMouseDown = false;
|
||||
clearTimeout(scrollTimeoutId);
|
||||
clearTimeout(hideTimeoutId);
|
||||
}
|
||||
|
||||
export function show() {
|
||||
clearTimeout(hideTimeoutId);
|
||||
clearTimeout(scrollTimeoutId);
|
||||
|
||||
if (active) {
|
||||
return; // already active
|
||||
}
|
||||
|
||||
active = true;
|
||||
|
||||
clearTimeout(scrollTimeoutId);
|
||||
// focus toggler container not nested into the trigger
|
||||
if (!activeTrigger?.contains(container)) {
|
||||
container?.focus();
|
||||
}
|
||||
|
||||
scrollTimeoutId = setTimeout(() => {
|
||||
if (!autoScroll) {
|
||||
return;
|
||||
@@ -68,29 +94,72 @@
|
||||
return (
|
||||
!container ||
|
||||
elem.classList.contains(closableClass) ||
|
||||
// is the trigger itself (or a direct child)
|
||||
(activeTrigger?.contains(elem) && !container.contains(elem)) ||
|
||||
// is closable toggler child
|
||||
(container.contains(elem) && elem.closest && elem.closest("." + closableClass))
|
||||
);
|
||||
}
|
||||
|
||||
function handleClickToggle(e) {
|
||||
if (!active || isClosable(e.target)) {
|
||||
function bindTrigger(newTrigger) {
|
||||
cleanup();
|
||||
|
||||
container?.addEventListener("click", handleContainerClick);
|
||||
container?.addEventListener("keydown", handleContainerKeydown);
|
||||
|
||||
activeTrigger = newTrigger || container?.parentNode;
|
||||
activeTrigger?.addEventListener("click", handleTriggerClick);
|
||||
activeTrigger?.addEventListener("keydown", handleTriggerKeydown);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
clearTimeout(scrollTimeoutId);
|
||||
clearTimeout(hideTimeoutId);
|
||||
|
||||
container?.removeEventListener("click", handleContainerClick);
|
||||
container?.removeEventListener("keydown", handleContainerKeydown);
|
||||
|
||||
activeTrigger?.removeEventListener("click", handleTriggerClick);
|
||||
activeTrigger?.removeEventListener("keydown", handleTriggerKeydown);
|
||||
}
|
||||
|
||||
// toggler container handlers
|
||||
// ---------------------------------------------------------------
|
||||
function handleContainerClick(e) {
|
||||
e.stopPropagation(); // prevents firing the trigger click event in case it is nested
|
||||
|
||||
if (isClosable(e.target)) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleContainerKeydown(e) {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
e.stopPropagation(); // prevents firing the trigger keydown event in case it is nested
|
||||
|
||||
if (isClosable(e.target)) {
|
||||
// hide with a short delay since the button on:click events
|
||||
// doesn't fire if the element is not visible
|
||||
hideWithDelay(150);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trigger handlers
|
||||
// ---------------------------------------------------------------
|
||||
function handleTriggerClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggle();
|
||||
}
|
||||
|
||||
function handleTriggerKeydown(e) {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydownToggle(e) {
|
||||
if (
|
||||
(e.code === "Enter" || e.code === "Space") && // enter or spacebar
|
||||
(!active || isClosable(e.target))
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
function handleFocusChange(e) {
|
||||
if (active && !activeTrigger?.contains(e.target) && !container?.contains(e.target)) {
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
@@ -103,11 +172,11 @@
|
||||
}
|
||||
|
||||
function handleOutsideMousedown(e) {
|
||||
if (active && !container?.contains(e.target)) {
|
||||
isOutsideMouseDown = true;
|
||||
} else if (isOutsideMouseDown) {
|
||||
isOutsideMouseDown = false;
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
isOutsideMouseDown = !container?.contains(e.target);
|
||||
}
|
||||
|
||||
function handleOutsideClick(e) {
|
||||
@@ -122,29 +191,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusChange(e) {
|
||||
handleOutsideMousedown(e);
|
||||
handleOutsideClick(e);
|
||||
}
|
||||
|
||||
function bindTrigger(newTrigger) {
|
||||
cleanup();
|
||||
|
||||
container?.addEventListener("click", handleClickToggle);
|
||||
|
||||
activeTrigger = newTrigger || container?.parentNode;
|
||||
activeTrigger?.addEventListener("click", handleClickToggle);
|
||||
activeTrigger?.addEventListener("keydown", handleKeydownToggle);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
clearTimeout(scrollTimeoutId);
|
||||
|
||||
container?.removeEventListener("click", handleClickToggle);
|
||||
activeTrigger?.removeEventListener("click", handleClickToggle);
|
||||
activeTrigger?.removeEventListener("keydown", handleKeydownToggle);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
bindTrigger();
|
||||
|
||||
@@ -153,20 +199,15 @@
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:mousedown={handleOutsideMousedown}
|
||||
on:click={handleOutsideClick}
|
||||
on:mousedown={handleOutsideMousedown}
|
||||
on:keydown={handleEscPress}
|
||||
on:focusin={handleFocusChange}
|
||||
/>
|
||||
|
||||
<div bind:this={container} class="toggler-container" tabindex="-1">
|
||||
<div bind:this={container} class="toggler-container" tabindex="-1" role="menu">
|
||||
{#if active}
|
||||
<div
|
||||
bind:this={containerChild}
|
||||
class={classes}
|
||||
class:active
|
||||
transition:fly={{ duration: 150, y: 3 }}
|
||||
>
|
||||
<div bind:this={containerChild} class={classes} class:active transition:fly={{ duration: 150, y: 3 }}>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<OverlayPanel
|
||||
bind:this={collectionPanel}
|
||||
class="overlay-panel-lg colored-header collection-panel"
|
||||
@@ -288,23 +289,34 @@
|
||||
|
||||
{#if !!collection.id && !collection.system}
|
||||
<div class="flex-fill" />
|
||||
<button type="button" aria-label="More" class="btn btn-sm btn-circle btn-transparent flex-gap-0">
|
||||
<i class="ri-more-line" />
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="More collection options"
|
||||
class="btn btn-sm btn-circle btn-transparent flex-gap-0"
|
||||
>
|
||||
<i class="ri-more-line" aria-hidden="true" />
|
||||
<Toggler class="dropdown dropdown-right m-t-5">
|
||||
<button type="button" class="dropdown-item closable" on:click={() => duplicateConfirm()}>
|
||||
<i class="ri-file-copy-line" />
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
role="menuitem"
|
||||
on:click={() => duplicateConfirm()}
|
||||
>
|
||||
<i class="ri-file-copy-line" aria-hidden="true" />
|
||||
<span class="txt">Duplicate</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger closable"
|
||||
class="dropdown-item txt-danger"
|
||||
role="menuitem"
|
||||
on:click|preventDefault|stopPropagation={() => deleteConfirm()}
|
||||
>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<i class="ri-delete-bin-7-line" aria-hidden="true" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
@@ -337,31 +349,37 @@
|
||||
/>
|
||||
|
||||
<div class="form-field-addon">
|
||||
<button
|
||||
type="button"
|
||||
<div
|
||||
tabindex={!collection.id ? 0 : -1}
|
||||
role={!collection.id ? "button" : ""}
|
||||
aria-label="View types"
|
||||
class="btn btn-sm p-r-10 p-l-10 {!collection.id ? 'btn-outline' : 'btn-transparent'}"
|
||||
disabled={!!collection.id}
|
||||
class:btn-disabled={!!collection.id}
|
||||
>
|
||||
<!-- empty span for alignment -->
|
||||
<span />
|
||||
<span aria-hidden="true" />
|
||||
<span class="txt">Type: {collectionTypes[collection.type] || "N/A"}</span>
|
||||
{#if !collection.id}
|
||||
<i class="ri-arrow-down-s-fill" />
|
||||
<i class="ri-arrow-down-s-fill" aria-hidden="true" />
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap m-t-5">
|
||||
{#each Object.entries(collectionTypes) as [type, label]}
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class="dropdown-item closable"
|
||||
class:selected={type == collection.type}
|
||||
on:click={() => setCollectionType(type)}
|
||||
>
|
||||
<i class={CommonHelper.getCollectionTypeIcon(type)} />
|
||||
<i
|
||||
class={CommonHelper.getCollectionTypeIcon(type)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="txt">{label} collection</span>
|
||||
</button>
|
||||
{/each}
|
||||
</Toggler>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if collection.system}
|
||||
|
||||
@@ -71,29 +71,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button type="button" class="field-types-btn {classes}" on:click={dispatch}>
|
||||
<i class="ri-add-line" />
|
||||
<div tabindex="0" role="button" class="field-types-btn {classes}">
|
||||
<i class="ri-add-line" aria-hidden="true" />
|
||||
<div class="txt">New field</div>
|
||||
<Toggler class="dropdown field-types-dropdown">
|
||||
{#each types as item}
|
||||
<div
|
||||
tabindex="0"
|
||||
class="dropdown-item closable"
|
||||
on:click|stopPropagation={() => {
|
||||
select(item.value);
|
||||
}}
|
||||
on:keydown|stopPropagation={(e) => {
|
||||
if (e.code === "Enter" || e.code === "Space") {
|
||||
select(item.value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i class="icon {item.icon}" />
|
||||
<button type="button" role="menuitem" class="dropdown-item" on:click={() => select(item.value)}>
|
||||
<i class="icon {item.icon}" aria-hidden="true" />
|
||||
<span class="txt">{item.label}</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.field-types-btn.active {
|
||||
|
||||
@@ -244,22 +244,28 @@
|
||||
{#if !field.toDelete}
|
||||
<div class="m-l-auto txt-right">
|
||||
<div class="inline-flex flex-gap-sm flex-nowrap">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div tabindex="0" aria-label="More" class="btn btn-circle btn-sm btn-transparent">
|
||||
<i class="ri-more-line" />
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="More"
|
||||
class="btn btn-circle btn-sm btn-transparent"
|
||||
>
|
||||
<i class="ri-more-line" aria-hidden="true" />
|
||||
<Toggler
|
||||
class="dropdown dropdown-sm dropdown-upside dropdown-right dropdown-nowrap no-min-width"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-right"
|
||||
class="dropdown-item"
|
||||
role="menuitem"
|
||||
on:click|preventDefault={duplicate}
|
||||
>
|
||||
<span class="txt">Duplicate</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-right"
|
||||
class="dropdown-item"
|
||||
role="menuitem"
|
||||
on:click|preventDefault={remove}
|
||||
>
|
||||
<span class="txt">Remove</span>
|
||||
|
||||
@@ -117,13 +117,19 @@
|
||||
bind:keyOfSelected={field.options.mimeTypes}
|
||||
/>
|
||||
<div class="help-block">
|
||||
<button type="button" class="inline-flex flex-gap-0">
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="More collection options"
|
||||
class="inline-flex flex-gap-0"
|
||||
>
|
||||
<span class="txt link-primary">Choose presets</span>
|
||||
<i class="ri-arrow-drop-down-fill" />
|
||||
<i class="ri-arrow-drop-down-fill" aria-hidden="true" />
|
||||
<Toggler class="dropdown dropdown-sm dropdown-nowrap dropdown-left">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"image/jpeg",
|
||||
@@ -139,6 +145,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"application/pdf",
|
||||
@@ -154,6 +161,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"video/mp4",
|
||||
@@ -168,6 +176,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => {
|
||||
field.options.mimeTypes = [
|
||||
"application/zip",
|
||||
@@ -179,7 +188,7 @@
|
||||
<span class="txt">Archives (zip, 7zip, rar)</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
@@ -205,7 +214,7 @@
|
||||
<span class="txt">Use comma as separator.</span>
|
||||
<button type="button" class="inline-flex flex-gap-0">
|
||||
<span class="txt link-primary">Supported formats</span>
|
||||
<i class="ri-arrow-drop-down-fill" />
|
||||
<i class="ri-arrow-drop-down-fill" aria-hidden="true" />
|
||||
<Toggler class="dropdown dropdown-sm dropdown-center dropdown-nowrap p-r-10">
|
||||
<ul class="m-0">
|
||||
<li>
|
||||
|
||||
@@ -492,17 +492,19 @@
|
||||
|
||||
{#if !isNew}
|
||||
<div class="flex-fill" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="More"
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
aria-label="More record options"
|
||||
class="btn btn-sm btn-circle btn-transparent flex-gap-0"
|
||||
>
|
||||
<i class="ri-more-line" />
|
||||
<i class="ri-more-line" aria-hidden="true" />
|
||||
<Toggler class="dropdown dropdown-right dropdown-nowrap">
|
||||
{#if isAuthCollection && !original.verified && original.email}
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => sendVerificationEmail()}
|
||||
>
|
||||
<i class="ri-mail-check-line" />
|
||||
@@ -513,6 +515,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => sendPasswordResetEmail()}
|
||||
>
|
||||
<i class="ri-mail-lock-line" />
|
||||
@@ -522,6 +525,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item closable"
|
||||
role="menuitem"
|
||||
on:click={() => duplicateConfirm()}
|
||||
>
|
||||
<i class="ri-file-copy-line" />
|
||||
@@ -530,13 +534,14 @@
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item txt-danger closable"
|
||||
role="menuitem"
|
||||
on:click|preventDefault|stopPropagation={() => deleteConfirm()}
|
||||
>
|
||||
<i class="ri-delete-bin-7-line" />
|
||||
<span class="txt">Delete</span>
|
||||
</button>
|
||||
</Toggler>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user