Replace remaining React ARIA components with Compound components (#2576)
* Fix issues detected by Knip Including cleaning up some unused code and dependencies, using a React hook that we unintentionally stopped using, and also adding some previously undeclared dependencies. * Replace remaining React ARIA components with Compound components * fix button position * disable scrollbars to resolve overlapping button --------- Co-authored-by: Timo <toger5@hotmail.de>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
Copyright 2022-2024 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,21 +14,25 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useObjectRef } from "@react-aria/utils";
|
||||
import {
|
||||
AllHTMLAttributes,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useState,
|
||||
forwardRef,
|
||||
ChangeEvent,
|
||||
useRef,
|
||||
FC,
|
||||
} from "react";
|
||||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Menu, MenuItem } from "@vector-im/compound-web";
|
||||
import {
|
||||
DeleteIcon,
|
||||
EditIcon,
|
||||
ShareIcon,
|
||||
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||
|
||||
import { Avatar, Size } from "../Avatar";
|
||||
import { Button } from "../button";
|
||||
import EditIcon from "../icons/Edit.svg?react";
|
||||
import styles from "./AvatarInputField.module.css";
|
||||
|
||||
interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
||||
@@ -40,89 +44,115 @@ interface Props extends AllHTMLAttributes<HTMLInputElement> {
|
||||
onRemoveAvatar: () => void;
|
||||
}
|
||||
|
||||
export const AvatarInputField = forwardRef<HTMLInputElement, Props>(
|
||||
(
|
||||
{
|
||||
id,
|
||||
label,
|
||||
className,
|
||||
avatarUrl,
|
||||
userId,
|
||||
displayName,
|
||||
onRemoveAvatar,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
export const AvatarInputField: FC<Props> = ({
|
||||
id,
|
||||
label,
|
||||
className,
|
||||
avatarUrl,
|
||||
userId,
|
||||
displayName,
|
||||
onRemoveAvatar,
|
||||
...rest
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [objUrl, setObjUrl] = useState<string | undefined>(undefined);
|
||||
const [removed, setRemoved] = useState(false);
|
||||
const [objUrl, setObjUrl] = useState<string | undefined>(undefined);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const fileInputRef = useObjectRef(ref);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const currentInput = fileInputRef.current;
|
||||
useEffect(() => {
|
||||
const currentInput = fileInputRef.current!;
|
||||
|
||||
const onChange = (e: Event): void => {
|
||||
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
|
||||
if (inputEvent.target.files && inputEvent.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
|
||||
setRemoved(false);
|
||||
} else {
|
||||
setObjUrl(undefined);
|
||||
}
|
||||
};
|
||||
const onChange = (e: Event): void => {
|
||||
const inputEvent = e as unknown as ChangeEvent<HTMLInputElement>;
|
||||
if (inputEvent.target.files && inputEvent.target.files.length > 0) {
|
||||
setObjUrl(URL.createObjectURL(inputEvent.target.files[0]));
|
||||
setRemoved(false);
|
||||
} else {
|
||||
setObjUrl(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
currentInput.addEventListener("change", onChange);
|
||||
currentInput.addEventListener("change", onChange);
|
||||
|
||||
return (): void => {
|
||||
currentInput?.removeEventListener("change", onChange);
|
||||
};
|
||||
});
|
||||
return (): void => {
|
||||
currentInput?.removeEventListener("change", onChange);
|
||||
};
|
||||
});
|
||||
|
||||
const onPressRemoveAvatar = useCallback(() => {
|
||||
setRemoved(true);
|
||||
onRemoveAvatar();
|
||||
}, [onRemoveAvatar]);
|
||||
const onSelectUpload = useCallback(() => {
|
||||
fileInputRef.current!.click();
|
||||
}, [fileInputRef]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.avatarInputField, className)}>
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
id={userId}
|
||||
name={displayName}
|
||||
size={Size.XL}
|
||||
src={removed ? undefined : objUrl || avatarUrl}
|
||||
/>
|
||||
<input
|
||||
id={id}
|
||||
accept="image/png, image/jpeg"
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className={styles.fileInput}
|
||||
role="button"
|
||||
aria-label={label}
|
||||
{...rest}
|
||||
/>
|
||||
{/* https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/issues/966 */}
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label htmlFor={id} className={styles.fileInputButton}>
|
||||
<EditIcon />
|
||||
</label>
|
||||
</div>
|
||||
{(avatarUrl || objUrl) && !removed && (
|
||||
<Button
|
||||
className={styles.removeButton}
|
||||
variant="icon"
|
||||
onPress={onPressRemoveAvatar}
|
||||
const onSelectRemove = useCallback(() => {
|
||||
setRemoved(true);
|
||||
onRemoveAvatar();
|
||||
}, [onRemoveAvatar]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.avatarInputField, className)}>
|
||||
<Avatar
|
||||
id={userId}
|
||||
className={styles.avatar}
|
||||
name={displayName}
|
||||
size={Size.XL}
|
||||
src={removed ? undefined : objUrl || avatarUrl}
|
||||
/>
|
||||
<input
|
||||
id={id}
|
||||
accept="image/*"
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className={styles.fileInput}
|
||||
role="button"
|
||||
aria-label={label}
|
||||
{...rest}
|
||||
/>
|
||||
<div className={styles.edit}>
|
||||
{(avatarUrl || objUrl) && !removed ? (
|
||||
<Menu
|
||||
title={t("action.edit")}
|
||||
showTitle={false}
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
trigger={
|
||||
<Button
|
||||
iconOnly
|
||||
Icon={EditIcon}
|
||||
kind="tertiary"
|
||||
size="sm"
|
||||
aria-label={t("action.edit")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t("action.remove")}
|
||||
</Button>
|
||||
<MenuItem
|
||||
Icon={ShareIcon}
|
||||
label={t("action.upload_file")}
|
||||
onSelect={onSelectUpload}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={DeleteIcon}
|
||||
label={t("action.remove")}
|
||||
kind="critical"
|
||||
onSelect={onSelectRemove}
|
||||
/>
|
||||
</Menu>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
iconOnly
|
||||
Icon={EditIcon}
|
||||
kind="tertiary"
|
||||
size="sm"
|
||||
aria-label={t("action.edit")}
|
||||
onClick={onSelectUpload}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
AvatarInputField.displayName = "AvatarInputField";
|
||||
|
||||
Reference in New Issue
Block a user