Skip to content

[docs] add avatar upload example #45131

@Demianeen

Description

@Demianeen

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

Metadata

Metadata

Assignees

Labels

scope: avatarChanges related to the avatar.support: docs-feedbackFeedback from documentation page.type: enhancementIt’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.waiting for 👍Waiting for upvotes. Open for community feedback and needs more interest to be worked on.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions