mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[bugfix] Fixes to tablist, fileinput, checkbox (#4139)
Some fixes to various frontend things: - Fix signup checkbox being height 0 on webkit - closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4136 - Fix wonky file input on chrome and webkit - closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4138 - Make tablist in interaction policies keyboard accessible with proper left/right + focus handling, see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tablist_role Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4139 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
@ -493,9 +493,8 @@ section.with-form {
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
|
|
||||||
& > input {
|
& > input {
|
||||||
height: 100%;
|
height: 1rem;
|
||||||
width: 5%;
|
width: 1rem;
|
||||||
min-width: 1.2rem;
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,10 +122,6 @@ export function FileInput({ label, field, ...props }: FileInputProps) {
|
|||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
const { onChange, infoComponent } = field;
|
const { onChange, infoComponent } = field;
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const onClick = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
ref.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-field file">
|
<div className="form-field file">
|
||||||
@ -133,11 +129,9 @@ export function FileInput({ label, field, ...props }: FileInputProps) {
|
|||||||
className="label-wrapper"
|
className="label-wrapper"
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={onClick}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
ref.current?.click();
|
||||||
onClick(e);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -71,9 +71,6 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
|
|||||||
}, [exportResult]);
|
}, [exportResult]);
|
||||||
|
|
||||||
const importFileRef = useRef<HTMLInputElement>(null);
|
const importFileRef = useRef<HTMLInputElement>(null);
|
||||||
const importFileOnClick = () => {
|
|
||||||
importFileRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -109,11 +106,9 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
|
|||||||
<label
|
<label
|
||||||
className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}
|
className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={importFileOnClick}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
importFileRef.current?.click();
|
||||||
importFileOnClick();
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
role="button"
|
role="button"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { forwardRef, useCallback, useMemo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
useDefaultInteractionPoliciesQuery,
|
useDefaultInteractionPoliciesQuery,
|
||||||
useResetDefaultInteractionPoliciesMutation,
|
useResetDefaultInteractionPoliciesMutation,
|
||||||
@ -191,57 +191,109 @@ function InteractionPoliciesForm({ defaultPolicies }: InteractionPoliciesFormPro
|
|||||||
|
|
||||||
// A tablist of tab buttons, one for each visibility.
|
// A tablist of tab buttons, one for each visibility.
|
||||||
function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) {
|
function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) {
|
||||||
|
const publicRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const unlistedRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const privateRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tab-buttons" role="tablist">
|
<div className="tab-buttons" role="tablist">
|
||||||
<Tab
|
<Tab
|
||||||
thisVisibility="public"
|
|
||||||
label="Public"
|
label="Public"
|
||||||
selectedVis={selectedVis}
|
selectedVis={selectedVis}
|
||||||
|
prevVis="private"
|
||||||
|
thisVis="public"
|
||||||
|
nextVis="unlisted"
|
||||||
|
prevRef={privateRef}
|
||||||
|
thisRef={publicRef}
|
||||||
|
nextRef={unlistedRef}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
thisVisibility="unlisted"
|
|
||||||
label="Unlisted"
|
label="Unlisted"
|
||||||
selectedVis={selectedVis}
|
selectedVis={selectedVis}
|
||||||
|
prevVis="public"
|
||||||
|
thisVis="unlisted"
|
||||||
|
nextVis="private"
|
||||||
|
prevRef={publicRef}
|
||||||
|
thisRef={unlistedRef}
|
||||||
|
nextRef={privateRef}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
thisVisibility="private"
|
|
||||||
label="Followers-only"
|
label="Followers-only"
|
||||||
selectedVis={selectedVis}
|
selectedVis={selectedVis}
|
||||||
|
prevVis="unlisted"
|
||||||
|
thisVis="private"
|
||||||
|
nextVis="public"
|
||||||
|
prevRef={unlistedRef}
|
||||||
|
thisRef={privateRef}
|
||||||
|
nextRef={publicRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
thisVisibility: string;
|
label: string;
|
||||||
label: string,
|
selectedVis: TextFormInputHook;
|
||||||
selectedVis: TextFormInputHook
|
prevVis: string;
|
||||||
|
thisVis: string;
|
||||||
|
nextVis: string;
|
||||||
|
prevRef: React.RefObject<HTMLButtonElement>;
|
||||||
|
thisRef: React.RefObject<HTMLButtonElement>;
|
||||||
|
nextRef: React.RefObject<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One tab in a tablist, corresponding to the given thisVisibility.
|
// One tab in a tablist, corresponding to the given thisVisibility.
|
||||||
function Tab({ thisVisibility, label, selectedVis }: TabProps) {
|
const Tab = forwardRef(
|
||||||
const selected = useMemo(() => {
|
function Tab({
|
||||||
return selectedVis.value === thisVisibility;
|
label,
|
||||||
}, [selectedVis, thisVisibility]);
|
selectedVis,
|
||||||
|
prevVis,
|
||||||
|
thisVis,
|
||||||
|
nextVis,
|
||||||
|
prevRef,
|
||||||
|
thisRef,
|
||||||
|
nextRef,
|
||||||
|
}: TabProps) {
|
||||||
|
const selected = useMemo(() => {
|
||||||
|
return selectedVis.value === thisVis;
|
||||||
|
}, [selectedVis, thisVis]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
id={`tab-${thisVisibility}`}
|
id={`tab-${thisVis}`}
|
||||||
title={label}
|
title={label}
|
||||||
role="tab"
|
role="tab"
|
||||||
className={`tab-button ${selected && "active"}`}
|
ref={thisRef}
|
||||||
onClick={(e) => {
|
className={`tab-button ${selected && "active"}`}
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
selectedVis.setter(thisVisibility);
|
// Allow tab to be clicked.
|
||||||
}}
|
e.preventDefault();
|
||||||
aria-selected={selected}
|
selectedVis.setter(thisVis);
|
||||||
aria-controls={`panel-${thisVisibility}`}
|
}}
|
||||||
tabIndex={selected ? 0 : -1}
|
onKeyDown={(e) => {
|
||||||
>
|
// Allow cycling through
|
||||||
{label}
|
// tabs with arrow keys.
|
||||||
</button>
|
if (e.key === "ArrowLeft") {
|
||||||
);
|
// Select and set
|
||||||
}
|
// focus on previous tab.
|
||||||
|
selectedVis.setter(prevVis);
|
||||||
|
prevRef.current?.focus();
|
||||||
|
} else if (e.key === "ArrowRight") {
|
||||||
|
// Select and set
|
||||||
|
// focus on next tab.
|
||||||
|
selectedVis.setter(nextVis);
|
||||||
|
nextRef.current?.focus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
aria-selected={selected}
|
||||||
|
aria-controls={`panel-${thisVis}`}
|
||||||
|
tabIndex={selected ? 0 : -1}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interface PolicyPanelProps {
|
interface PolicyPanelProps {
|
||||||
policyForm: PolicyForm;
|
policyForm: PolicyForm;
|
||||||
|
Reference in New Issue
Block a user