mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
refactor: Replace Avatar internals with Radix primitives
- Replace custom Avatar implementation with @radix-ui/react-avatar - Maintain same API and styling for backward compatibility - Use Radix best practices with Root, Image, and Fallback components - Fix TypeScript and ESLint issues - Keep all existing functionality including AvatarWithPresence and GroupAvatar
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import Initials from "./Initials";
|
||||
|
||||
export enum AvatarSize {
|
||||
@@ -37,7 +37,7 @@ type Props = {
|
||||
/** The alt text for the image */
|
||||
alt?: string;
|
||||
/** Optional click handler */
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
/** Optional class name */
|
||||
className?: string;
|
||||
/** Optional style */
|
||||
@@ -50,28 +50,37 @@ function Avatar(props: Props) {
|
||||
style,
|
||||
variant = AvatarVariant.Round,
|
||||
className,
|
||||
...rest
|
||||
onClick,
|
||||
alt,
|
||||
size,
|
||||
} = props;
|
||||
const src = props.src || model?.avatarUrl;
|
||||
const [error, handleError] = useBoolean(false);
|
||||
|
||||
return (
|
||||
<Relative
|
||||
<StyledAvatarRoot
|
||||
style={style}
|
||||
$variant={variant}
|
||||
$size={props.size}
|
||||
$size={size}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
{src && !error ? (
|
||||
<Image onError={handleError} src={src} {...rest} />
|
||||
) : model ? (
|
||||
<Initials color={model.color} {...rest}>
|
||||
{model.initial}
|
||||
</Initials>
|
||||
) : (
|
||||
<Initials {...rest} />
|
||||
{src && (
|
||||
<StyledAvatarImage
|
||||
src={src}
|
||||
alt={alt || (model ? `${model.initial || "User"} avatar` : "Avatar")}
|
||||
$size={size}
|
||||
/>
|
||||
)}
|
||||
</Relative>
|
||||
<StyledAvatarFallback $size={size}>
|
||||
{model ? (
|
||||
<Initials color={model.color} size={size}>
|
||||
{model.initial}
|
||||
</Initials>
|
||||
) : (
|
||||
<Initials size={size} />
|
||||
)}
|
||||
</StyledAvatarFallback>
|
||||
</StyledAvatarRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +88,10 @@ Avatar.defaultProps = {
|
||||
size: AvatarSize.Medium,
|
||||
};
|
||||
|
||||
const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
|
||||
const StyledAvatarRoot = styled(AvatarPrimitive.Root)<{
|
||||
$variant: AvatarVariant;
|
||||
$size: AvatarSize;
|
||||
}>`
|
||||
position: relative;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
@@ -88,12 +100,27 @@ const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
|
||||
overflow: hidden;
|
||||
width: ${(props) => props.$size}px;
|
||||
height: ${(props) => props.$size}px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Image = styled.img<{ size: number }>`
|
||||
display: block;
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
const StyledAvatarImage = styled(AvatarPrimitive.Image)<{ $size: number }>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
`;
|
||||
|
||||
const StyledAvatarFallback = styled(AvatarPrimitive.Fallback)<{
|
||||
$size: number;
|
||||
}>`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: inherit;
|
||||
`;
|
||||
|
||||
export default Avatar;
|
||||
|
||||
@@ -22,7 +22,7 @@ type Props = {
|
||||
/** Whether this avatar represents the current user */
|
||||
isCurrentUser: boolean;
|
||||
/** Optional click handler for the avatar */
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
/** Size of the avatar, defaults to AvatarSize.Large */
|
||||
size?: AvatarSize;
|
||||
/** Optional inline styles to apply to the avatar wrapper */
|
||||
@@ -138,11 +138,11 @@ const AvatarPresence = styled.div<AvatarWrapperProps>`
|
||||
border: 2px solid transparent;
|
||||
pointer-events: none;
|
||||
|
||||
${(props) =>
|
||||
props.$isObserving &&
|
||||
${(innerProps) =>
|
||||
innerProps.$isObserving &&
|
||||
css`
|
||||
border: 2px solid ${props.$color};
|
||||
box-shadow: inset 0 0 0 2px ${props.theme.background};
|
||||
border: 2px solid ${innerProps.$color};
|
||||
box-shadow: inset 0 0 0 2px ${innerProps.theme.background};
|
||||
|
||||
&:hover {
|
||||
top: -1px;
|
||||
@@ -154,7 +154,7 @@ const AvatarPresence = styled.div<AvatarWrapperProps>`
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
border: 2px solid ${(props) => props.$color};
|
||||
border: 2px solid ${(innerProps) => innerProps.$color};
|
||||
box-shadow: inset 0 0 0 2px ${s("background")};
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -20,8 +20,6 @@ const Initials = styled(Flex)<{
|
||||
? s("black50")
|
||||
: s("white75")};
|
||||
background-color: ${(props) => props.color ?? props.theme.textTertiary};
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
flex-shrink: 0;
|
||||
|
||||
// adjust font size down for each additional character
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
"@outlinewiki/koa-passport": "^4.2.1",
|
||||
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
|
||||
@@ -3327,6 +3327,17 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
|
||||
"@radix-ui/react-avatar@^1.1.10":
|
||||
version "1.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz#c58a8800ef3d3ee783b3168fee7c76f6534bfd93"
|
||||
integrity sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==
|
||||
dependencies:
|
||||
"@radix-ui/react-context" "1.1.2"
|
||||
"@radix-ui/react-primitive" "2.1.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
"@radix-ui/react-use-is-hydrated" "0.1.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.1.1"
|
||||
|
||||
"@radix-ui/react-collection@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0"
|
||||
@@ -3719,6 +3730,13 @@
|
||||
dependencies:
|
||||
"@radix-ui/react-use-callback-ref" "1.1.1"
|
||||
|
||||
"@radix-ui/react-use-is-hydrated@0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz#544da73369517036c77659d7cdd019dc0f5ff9a0"
|
||||
integrity sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==
|
||||
dependencies:
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
||||
"@radix-ui/react-use-layout-effect@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
|
||||
@@ -15958,6 +15976,11 @@ use-sidecar@^1.1.3:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
|
||||
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
|
||||
|
||||
utf8-byte-length@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"
|
||||
|
||||
Reference in New Issue
Block a user