[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:
tobi
2025-05-06 08:06:52 +00:00
committed by tobi
parent 73aa62581e
commit 4a6b357501
4 changed files with 85 additions and 45 deletions

View File

@ -493,9 +493,8 @@ section.with-form {
gap: 0.4rem;
& > input {
height: 100%;
width: 5%;
min-width: 1.2rem;
height: 1rem;
width: 1rem;
align-self: center;
}
}

View File

@ -122,10 +122,6 @@ export function FileInput({ label, field, ...props }: FileInputProps) {
const ref = useRef<HTMLInputElement>(null);
const { onChange, infoComponent } = field;
const id = nanoid();
const onClick = (e) => {
e.preventDefault();
ref.current?.click();
};
return (
<div className="form-field file">
@ -133,11 +129,9 @@ export function FileInput({ label, field, ...props }: FileInputProps) {
className="label-wrapper"
htmlFor={id}
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onClick(e);
ref.current?.click();
}
}}
role="button"

View File

@ -71,9 +71,6 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
}, [exportResult]);
const importFileRef = useRef<HTMLInputElement>(null);
const importFileOnClick = () => {
importFileRef.current?.click();
};
return (
<>
@ -109,11 +106,9 @@ export default function ImportExportForm({ form, submitParse, parseResult }: Imp
<label
className={`button with-icon${form.permType.value === undefined || form.permType.value.length === 0 ? " disabled" : ""}`}
tabIndex={0}
onClick={importFileOnClick}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
importFileOnClick();
importFileRef.current?.click();
}
}}
role="button"

View File

@ -17,7 +17,7 @@
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 {
useDefaultInteractionPoliciesQuery,
useResetDefaultInteractionPoliciesMutation,
@ -191,57 +191,109 @@ function InteractionPoliciesForm({ defaultPolicies }: InteractionPoliciesFormPro
// A tablist of tab buttons, one for each visibility.
function PolicyPanelsTablist({ selectedVis }: { selectedVis: TextFormInputHook}) {
const publicRef = useRef<HTMLButtonElement>(null);
const unlistedRef = useRef<HTMLButtonElement>(null);
const privateRef = useRef<HTMLButtonElement>(null);
return (
<div className="tab-buttons" role="tablist">
<Tab
thisVisibility="public"
label="Public"
selectedVis={selectedVis}
prevVis="private"
thisVis="public"
nextVis="unlisted"
prevRef={privateRef}
thisRef={publicRef}
nextRef={unlistedRef}
/>
<Tab
thisVisibility="unlisted"
label="Unlisted"
selectedVis={selectedVis}
prevVis="public"
thisVis="unlisted"
nextVis="private"
prevRef={publicRef}
thisRef={unlistedRef}
nextRef={privateRef}
/>
<Tab
thisVisibility="private"
label="Followers-only"
selectedVis={selectedVis}
prevVis="unlisted"
thisVis="private"
nextVis="public"
prevRef={unlistedRef}
thisRef={privateRef}
nextRef={publicRef}
/>
</div>
);
}
interface TabProps {
thisVisibility: string;
label: string,
selectedVis: TextFormInputHook
label: string;
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.
function Tab({ thisVisibility, label, selectedVis }: TabProps) {
const selected = useMemo(() => {
return selectedVis.value === thisVisibility;
}, [selectedVis, thisVisibility]);
const Tab = forwardRef(
function Tab({
label,
selectedVis,
prevVis,
thisVis,
nextVis,
prevRef,
thisRef,
nextRef,
}: TabProps) {
const selected = useMemo(() => {
return selectedVis.value === thisVis;
}, [selectedVis, thisVis]);
return (
<button
id={`tab-${thisVisibility}`}
title={label}
role="tab"
className={`tab-button ${selected && "active"}`}
onClick={(e) => {
e.preventDefault();
selectedVis.setter(thisVisibility);
}}
aria-selected={selected}
aria-controls={`panel-${thisVisibility}`}
tabIndex={selected ? 0 : -1}
>
{label}
</button>
);
}
return (
<button
id={`tab-${thisVis}`}
title={label}
role="tab"
ref={thisRef}
className={`tab-button ${selected && "active"}`}
onClick={(e) => {
// Allow tab to be clicked.
e.preventDefault();
selectedVis.setter(thisVis);
}}
onKeyDown={(e) => {
// Allow cycling through
// tabs with arrow keys.
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 {
policyForm: PolicyForm;