-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
Description
Related page
https://mui.com/material-ui/react-avatar/
Kind of issue
Missing information
Issue description
I recently needed to implement avatar upload inside form during sign up flow, I ended up with a bit hacky solution where I need to have visually hidden input nearby the clickable avatar, and that avatar will be a label, so that click on label will cause click on input. I also needed to put another hack in place so that input navigation with tab would work. It seems like quite popular usecase. Potentially adding section in the docs on how to implement it could save a lot of time and hacks in code to other people like me.
Just as the reference, this is what I end up with:
import { memo, useId, useState } from 'react'
import type { AvatarProps, FormHelperTextProps } from '@mui/material'
import { Avatar, FormHelperText, Stack, styled } from '@mui/material'
import PersonIcon from '@mui/icons-material/Person'
import { avatarConstraints } from '../../model/contact-info-schema'
import { useFocus } from '@/shared/lib/react/useFocus'
import { useTab } from '@/shared/lib/react/useTab'
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
})
const ClickableAvatar = styled(Avatar)(({ theme }) => ({
width: 100,
height: 100,
cursor: 'pointer',
transition: 'all .1s',
'&[data-focused="true"]': {
outline: `4px solid ${theme.palette.primary.main}`, // Replace with your desired style
outlineOffset: '4px',
},
'&:hover': {
filter: 'brightness(90%)',
},
'&:active': {
scale: 0.9,
},
}))
type ClickableAvatarProps = Omit<AvatarProps<'label'>, 'component'>
interface AvatarUploadProps {
avatarProps?: ClickableAvatarProps
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
helperTextProps?: FormHelperTextProps
}
export const AvatarUpload = memo(function AvatarUpload({
avatarProps,
inputProps,
helperTextProps,
}: AvatarUploadProps) {
const [imageSrc, setImageSrc] = useState<string>()
const id = useId()
const [isInputFocused, bindFocus] = useFocus()
const { isTabLastKey } = useTab()
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (file) {
// Read the file as a data URL
const reader = new FileReader()
reader.onload = () => {
setImageSrc(reader.result as string)
}
reader.readAsDataURL(file)
}
inputProps?.onChange?.(event)
}
return (
<Stack alignItems={'center'}>
<ClickableAvatar
// @ts-expect-error it can't see component prop for some reason
component='label'
role={undefined}
variant='circular'
src={imageSrc}
htmlFor={id}
data-focused={isInputFocused && isTabLastKey.current}
{...avatarProps}
>
<PersonIcon
sx={{
fontSize: '40px',
}}
/>
</ClickableAvatar>
<VisuallyHiddenInput
id={id}
type='file'
accept={avatarConstraints.type.join(', ')}
multiple={false}
{...inputProps}
{...bindFocus}
onChange={handleImageChange}
/>
<FormHelperText {...helperTextProps} />
</Stack>
)
})useFocus.ts (just tracks focus of the elem):
import { useCallback, useMemo, useState } from 'react'
interface UseFocusBind {
onBlur: () => void
onFocus: () => void
}
export type UseFocusReturnType = [boolean, UseFocusBind]
/**
* Tracks if an element is focused or not.
* @returns {[boolean, {onBlur: () => void, onFocus: () => void}]}
*/
export const useFocus = (): UseFocusReturnType => {
const [isFocused, setIsFocused] = useState(false)
const onBlur = useCallback(() => {
setIsFocused(false)
}, [])
const onFocus = useCallback(() => {
setIsFocused(true)
}, [])
return useMemo(
() => [isFocused, { onBlur, onFocus }],
[isFocused, onBlur, onFocus],
)
}useTab(tracks if the tab key is the last pressed):
import { useEffect, useRef } from 'react'
/**
* Custom React Hook to track if the Tab key was the last key pressed.
*
* This hook sets up global event listeners for 'keydown' and 'mousedown' events.
* It updates a ref `isTabLastKey` to determine if the Tab key was the last key pressed.
* The use of a ref prevents unnecessary re-renders of your component when these events occur.
*
* @returns {Object} - An object containing:
* - `isTabLastKey` (Ref<boolean>): A ref that is `true` if the last key pressed was Tab, `false` otherwise.
*
* @example
* const { isTabLastKey } = useTab();
* // You can now use isTabLastKey.current in your component logic
*/
export const useTab = () => {
const isTabLastKey = useRef(false)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Tab') {
isTabLastKey.current = true
} else {
isTabLastKey.current = false
}
}
const handleMouseDown = () => {
isTabLastKey.current = false
}
window.addEventListener('keydown', handleKeyDown)
window.addEventListener('mousedown', handleMouseDown)
return () => {
window.removeEventListener('keydown', handleKeyDown)
window.removeEventListener('click', handleMouseDown)
}
}, [])
return { isTabLastKey }
}Context
No response
Search keywords: mui avatar profile upload input hidden